From 90ab38c98989188a356091f47adc4a7c1192f36a Mon Sep 17 00:00:00 2001 From: Charlotte Som Date: Thu, 6 Mar 2025 09:52:36 +0000 Subject: [PATCH] compact wire protocol --- client/main.ts | 4 +++- compression.ts | 33 +++++++++++++++++++-------------- proto.ts | 3 ++- server.ts | 9 +++++++-- 4 files changed, 31 insertions(+), 18 deletions(-) diff --git a/client/main.ts b/client/main.ts index c87b62d..5446b5a 100644 --- a/client/main.ts +++ b/client/main.ts @@ -1,4 +1,5 @@ import diff from "npm:fast-diff"; +import { decompressOps } from "../compression.ts"; import { Packet } from "../proto.ts"; import { CausalTree, CausalTreeOp } from "../sync/ordt/causal-tree.ts"; import { PlainTextOperation, PlainTextORDT } from "../sync/ordt/plain-text.ts"; @@ -31,7 +32,8 @@ const initialized = Promise.withResolvers(); socket.addEventListener("message", ev => { const packet = JSON.parse(ev.data) as Packet; if (packet.t === "init") { - for (const op of packet.ops) pt.apply(op); + const ops = decompressOps(packet.ops); + for (const op of ops) pt.apply(op); initialized.resolve(packet); } else if (packet.t === "op") { pt.apply(packet.op); diff --git a/compression.ts b/compression.ts index 0643745..7aa543e 100644 --- a/compression.ts +++ b/compression.ts @@ -2,14 +2,15 @@ import { Timestamp } from "./sync/common.ts"; import { CausalTree, CausalTreeOp, WeakCausalTreeOp } from "./sync/ordt/causal-tree.ts"; import { PlainTextOperation } from "./sync/ordt/plain-text.ts"; -type VerbatimRun = WeakCausalTreeOp; +type VerbatimOp = WeakCausalTreeOp; type CompressedRun = { st: Timestamp; p: Timestamp | undefined; seq: string }; -type Run = VerbatimRun | CompressedRun; -export function compressOps(ops: CausalTreeOp[]): Run[] { - if (ops.length === 0) throw new Error("can't create a compressed run!"); +export type CompactedOperations = (VerbatimOp | CompressedRun)[]; - const runs: Run[] = []; +export function compressOps(ops: CausalTreeOp[]): CompactedOperations { + if (ops.length === 0) return []; + + const runs: CompactedOperations = []; let seq = ""; let cnt = 0; let start: Timestamp | undefined; @@ -18,8 +19,10 @@ export function compressOps(ops: CausalTreeOp[]): Run[] { const op = ops[i]; const lastOp = ops[i - 1]; - if (!start) start = op.at; - if (!firstParent) firstParent = op.parent?.at; + if (!start) { + start = op.at; + firstParent = op.parent?.at; + } if ( op.type === "insert" && @@ -54,16 +57,18 @@ export function compressOps(ops: CausalTreeOp[]): Run[] { return runs; } -export function decompressOps(runs: Run[]): WeakCausalTreeOp[] { +export function decompressOps( + compacted: CompactedOperations, +): WeakCausalTreeOp[] { const ops: WeakCausalTreeOp[] = []; - for (const run of runs) { - if ("st" in run) { + for (const item of compacted) { + if ("st" in item) { let i = 0; - let parent: Timestamp | undefined = run.p; - for (const c of run.seq) { + let parent: Timestamp | undefined = item.p; + for (const c of item.seq) { const op = { - at: [run.st[0], run.st[1] + i] as Timestamp, + at: [item.st[0], item.st[1] + i] as Timestamp, parent, type: "insert", sequence: c, @@ -73,7 +78,7 @@ export function decompressOps(runs: Run[]): WeakCausalTreeOp i++; } } else { - ops.push(run); + ops.push(item); } } diff --git a/proto.ts b/proto.ts index 8ae481a..ed83186 100644 --- a/proto.ts +++ b/proto.ts @@ -1,6 +1,7 @@ +import { CompactedOperations } from "./compression.ts"; import { WeakCausalTreeOp } from "./sync/ordt/causal-tree.ts"; import { PlainTextOperation } from "./sync/ordt/plain-text.ts"; export type Packet = - | { t: "init"; ops: WeakCausalTreeOp[]; you: string } + | { t: "init"; ops: CompactedOperations; you: string } | { t: "op"; op: WeakCausalTreeOp }; diff --git a/server.ts b/server.ts index 42acb54..329d22b 100644 --- a/server.ts +++ b/server.ts @@ -1,4 +1,5 @@ import { Application, Router } from "@oak/oak"; +import { compressOps } from "./compression.ts"; import { Packet } from "./proto.ts"; import { CausalTree } from "./sync/ordt/causal-tree.ts"; import { PlainTextORDT } from "./sync/ordt/plain-text.ts"; @@ -13,10 +14,14 @@ const sockets = new Set(); router.get("/api/connect", ctx => { const socket = ctx.upgrade(); sockets.add(socket); + + const connectionId = crypto.randomUUID(); + socket.addEventListener("message", event => { if (typeof event.data !== "string") return; const op = JSON.parse(event.data); + op.at[0] = connectionId; pt.apply(op); // mutates op to strong const weakOp = CausalTree.toWeakOp(op); @@ -33,8 +38,8 @@ router.get("/api/connect", ctx => { socket.send( JSON.stringify({ t: "init", - ops: pt.operations.map(it => CausalTree.toWeakOp(it)), - you: crypto.randomUUID(), + ops: compressOps(pt.operations), + you: connectionId, } satisfies Packet), ); };