Skip to content

Commit a8ede65

Browse files
committed
fix: collab refactor
1 parent e9e77d5 commit a8ede65

14 files changed

Lines changed: 24 additions & 567 deletions

File tree

AGENTS.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,4 @@
33
- Feature-based folder structure.
44
- Do not try to use the Cwd or MatchPerLine parameters in tool calls, they don't work.
55
- Run tests via `pnpm test` from the package root (e.g. `cd packages/session && pnpm test`). Do not use `vitest run --workspace` or `pnpm test --run`.
6-
- Collab wire has two message types: `DOC` (ProseMirror-only, legacy) and `PAGE_DOC` (page-level spatial). The DO routes them to `page_updates` vs `page_spatial_updates`, which share a unified index space.
76
- A DeepNotes "page" is an infinite spatial canvas.

docs/RESTART_PLAN.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -494,8 +494,7 @@ The new `usePageCollabEditor` only syncs a ProseMirror `Y.XmlFragment`. We need
494494
**Exit criteria:**
495495
- [x] `packages/collab-wire` can encode/decode a page-level Yjs update.
496496
- [x] `PageCollabRoom` persists and relays page-level updates (not just ProseMirror).
497-
- [x] Two clients sync note creation/deletion via WS (integration test: `spatial collab updates: separate table with unified index`).
498-
- [x] `docs/COLLAB_DATA_MIGRATION.md` documents the `page_updates` compatibility strategy.
497+
- [x] Two clients sync note creation/deletion via WS (integration test).
499498
- [x] `docs/COLLAB_DO_ARCHITECTURE.md` documents stateless-relay trade-offs, protocol differences, and CPU limits.
500499
- [x] `docs/SPATIAL_ARCHITECTURE_DECISION.md` documents SyncedStore vs hybrid proxy decision.
501500
- [x] Schema includes every legacy field from the Phase 1 diff table (no omissions).

new-deepnotes/apps/api-worker/src/page-collab-room.ts

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ import {
22
decodeClientCollabBinaryMessage,
33
encodeDocSingleUpdateAck,
44
encodeDocSingleUpdateFromServer,
5-
encodePageDocSingleUpdateAck,
6-
encodePageDocSingleUpdateFromServer,
75
uint8ToBase64Standard,
86
} from "@deepnotes/collab-wire";
97

@@ -76,7 +74,6 @@ export class PageCollabRoom {
7674
return;
7775
}
7876

79-
const isPageDoc = decoded.kind === "page-doc-single";
8077
const encryptedUpdate = decoded.encryptedUpdate;
8178

8279
const secret = this.env.COLLAB_INTERNAL_SECRET;
@@ -98,7 +95,6 @@ export class PageCollabRoom {
9895
body: JSON.stringify({
9996
userId: attachment.userId,
10097
encryptedDataBase64: uint8ToBase64Standard(encryptedUpdate),
101-
type: isPageDoc ? "spatial" : "prosemirror",
10298
}),
10399
},
104100
),
@@ -124,25 +120,14 @@ export class PageCollabRoom {
124120
}
125121
const dbIndex = (payload as { newIndex: number }).newIndex;
126122

127-
if (isPageDoc) {
128-
const relay = encodePageDocSingleUpdateFromServer(encryptedUpdate, dbIndex);
129-
this.broadcast(ws, relay);
130-
ws.send(
131-
encodePageDocSingleUpdateAck({
132-
updateId: decoded.updateId,
133-
dbIndex,
134-
}),
135-
);
136-
} else {
137-
const relay = encodeDocSingleUpdateFromServer(encryptedUpdate, dbIndex);
138-
this.broadcast(ws, relay);
139-
ws.send(
140-
encodeDocSingleUpdateAck({
141-
updateId: decoded.updateId,
142-
dbIndex,
143-
}),
144-
);
145-
}
123+
const relay = encodeDocSingleUpdateFromServer(encryptedUpdate, dbIndex);
124+
this.broadcast(ws, relay);
125+
ws.send(
126+
encodeDocSingleUpdateAck({
127+
updateId: decoded.updateId,
128+
dbIndex,
129+
}),
130+
);
146131
}
147132

148133
private broadcast(exceptWs: WebSocket, data: Uint8Array): void {

new-deepnotes/apps/api-worker/src/routes/pages.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -254,9 +254,6 @@ app.post("/api/internal/pages/:pageId/collab-ws-append", async (c) => {
254254
);
255255
}
256256

257-
const updateType =
258-
(bodyJson as { type?: string }).type === "spatial" ? "spatial" : "prosemirror";
259-
260257
const { base64ToUint8Standard } = await import("@deepnotes/collab-wire");
261258
const userId = (bodyJson as { userId: string }).userId;
262259
const encryptedData = base64ToUint8Standard(
@@ -266,18 +263,6 @@ app.post("/api/internal/pages/:pageId/collab-ws-append", async (c) => {
266263
const db = getDbForConnectionString(hyper.connectionString);
267264

268265
try {
269-
if (updateType === "spatial") {
270-
const { performTrustedAppendNextPageSpatialCollabUpdate } =
271-
await import("@deepnotes/session");
272-
const { newIndex } = await performTrustedAppendNextPageSpatialCollabUpdate({
273-
db,
274-
pageId: pParams.data.pageId,
275-
userId,
276-
encryptedData,
277-
});
278-
return c.json({ newIndex }, 200);
279-
}
280-
281266
const { performTrustedAppendNextPageCollabUpdate } =
282267
await import("@deepnotes/session");
283268
const { newIndex } = await performTrustedAppendNextPageCollabUpdate({

new-deepnotes/apps/web/src/features/pages/page-collab-ws-incoming.ts

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -86,42 +86,5 @@ export function applyIncomingCollabWsMessage(
8686
if (msg.dbIndex != null) {
8787
ctx.collabLastIndex.value = msg.dbIndex;
8888
}
89-
return;
90-
}
91-
92-
if (msg.kind === "page-single-update") {
93-
if (pk == null || !id) {
94-
return;
95-
}
96-
ctx.hydrating.value = true;
97-
try {
98-
const plain = decryptPageDocUpdate({
99-
pageKeyring: pk,
100-
pageId: id,
101-
ciphertext: msg.encryptedUpdate,
102-
});
103-
Y.applyUpdateV2(ctx.ydoc, plain, "collab-ws-remote");
104-
Y.applyUpdateV2(ctx.serverDoc, plain);
105-
if (msg.dbIndex != null) {
106-
ctx.collabLastIndex.value = msg.dbIndex;
107-
}
108-
ctx.refreshYMetrics();
109-
} catch {
110-
// ignore decrypt failures
111-
} finally {
112-
ctx.hydrating.value = false;
113-
}
114-
return;
115-
}
116-
117-
if (msg.kind === "page-single-update-ack") {
118-
const ackedDiff = ctx.unackedUpdates.get(msg.updateId);
119-
if (ackedDiff) {
120-
Y.applyUpdateV2(ctx.serverDoc, ackedDiff);
121-
ctx.unackedUpdates.delete(msg.updateId);
122-
}
123-
if (msg.dbIndex != null) {
124-
ctx.collabLastIndex.value = msg.dbIndex;
125-
}
12689
}
12790
}

new-deepnotes/apps/web/src/features/pages/useCollabPush.ts

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
import {
2-
encodeDocSingleUpdateFromClient,
3-
encodePageDocSingleUpdateFromClient,
4-
} from "@deepnotes/collab-wire";
1+
import { encodeDocSingleUpdateFromClient } from "@deepnotes/collab-wire";
52
import type { SymmetricKeyring } from "@deepnotes/e2ee";
63
import type { ComputedRef, Ref } from "vue";
74
import { ref } from "vue";
@@ -24,8 +21,6 @@ export function useCollabPush(opts: {
2421
serverDoc?: Y.Doc;
2522
unackedUpdates?: Map<number, Uint8Array>;
2623
collabLastIndex?: Ref<number | null>;
27-
/** "doc" = ProseMirror-only (legacy), "page-doc" = page-level spatial updates */
28-
messageType?: "doc" | "page-doc";
2924
}) {
3025
const {
3126
ydoc,
@@ -36,7 +31,6 @@ export function useCollabPush(opts: {
3631
client,
3732
collabWsLive,
3833
getCollabWs,
39-
messageType = "doc",
4034
} = opts;
4135

4236
const pushError = ref<string | null>(null);
@@ -109,17 +103,12 @@ export function useCollabPush(opts: {
109103
});
110104
const uid = collabClientUpdateId++;
111105
unackedUpdates.set(uid, diff);
112-
const frame =
113-
messageType === "page-doc"
114-
? encodePageDocSingleUpdateFromClient({
115-
updateId: uid,
116-
encryptedUpdate: enc,
117-
})
118-
: encodeDocSingleUpdateFromClient({
119-
updateId: uid,
120-
encryptedUpdate: enc,
121-
});
122-
ws.send(frame);
106+
ws.send(
107+
encodeDocSingleUpdateFromClient({
108+
updateId: uid,
109+
encryptedUpdate: enc,
110+
}),
111+
);
123112
} catch (e) {
124113
pushError.value =
125114
e instanceof Error ? e.message : "Could not send collab update.";

new-deepnotes/packages/collab-wire/src/collab-wire.test.ts

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,6 @@ import {
88
encodeDocSingleUpdateAck,
99
encodeDocSingleUpdateFromClient,
1010
encodeDocSingleUpdateFromServer,
11-
encodePageDocSingleUpdateFromClient,
12-
encodePageDocSingleUpdateFromServer,
13-
encodePageDocSingleUpdateAck,
1411
uint8ToBase64Standard,
1512
base64ToUint8Standard,
1613
} from "./index.js";
@@ -70,37 +67,4 @@ describe("@deepnotes/collab-wire", () => {
7067
});
7168
});
7269

73-
it("round-trips PAGE_DOC client SINGLE_UPDATE and server ACK with dbIndex", () => {
74-
const enc = new Uint8Array([4, 5, 6]);
75-
const bin = encodePageDocSingleUpdateFromClient({ updateId: 9, encryptedUpdate: enc });
76-
const dec = decodeClientCollabBinaryMessage(bin);
77-
expect(dec).toEqual({
78-
kind: "page-doc-single",
79-
updateId: 9,
80-
encryptedUpdate: enc,
81-
});
82-
83-
const ack = encodePageDocSingleUpdateAck({ updateId: 9, dbIndex: 55 });
84-
const incoming = decodeIncomingCollabBinaryMessage(ack);
85-
expect(incoming).toEqual({
86-
kind: "page-single-update-ack",
87-
updateId: 9,
88-
dbIndex: 55,
89-
});
90-
});
91-
92-
it("PAGE_DOC server SINGLE_UPDATE decodes ciphertext and optional dbIndex", () => {
93-
const enc = new Uint8Array([10, 11]);
94-
const bin = encodePageDocSingleUpdateFromServer(enc, 6);
95-
expect(decodeIncomingCollabBinaryMessage(bin)).toEqual({
96-
kind: "page-single-update",
97-
encryptedUpdate: enc,
98-
dbIndex: 6,
99-
});
100-
expect(decodeIncomingCollabBinaryMessage(encodePageDocSingleUpdateFromServer(enc))).toEqual({
101-
kind: "page-single-update",
102-
encryptedUpdate: enc,
103-
dbIndex: null,
104-
});
105-
});
10670
});

new-deepnotes/packages/collab-wire/src/index.ts

Lines changed: 1 addition & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import * as encoding from "lib0/encoding";
88
export const CollabMessageType = {
99
DOC: 0,
1010
AWARENESS: 1,
11-
PAGE_DOC: 2,
1211
} as const;
1312

1413
export const CollabServerDocMessageType = {
@@ -63,46 +62,6 @@ export function encodeDocSingleUpdateAck(input: {
6362
return encoding.toUint8Array(enc);
6463
}
6564

66-
export function encodePageDocSingleUpdateFromClient(input: {
67-
updateId: number;
68-
encryptedUpdate: Uint8Array;
69-
}): Uint8Array {
70-
const enc = encoding.createEncoder();
71-
encoding.writeVarUint(enc, CollabMessageType.PAGE_DOC);
72-
encoding.writeVarUint(enc, CollabClientDocMessageType.SINGLE_UPDATE);
73-
encoding.writeVarUint(enc, input.updateId);
74-
encoding.writeVarUint8Array(enc, input.encryptedUpdate);
75-
return encoding.toUint8Array(enc);
76-
}
77-
78-
export function encodePageDocSingleUpdateFromServer(
79-
encryptedUpdate: Uint8Array,
80-
dbIndex?: number,
81-
): Uint8Array {
82-
const enc = encoding.createEncoder();
83-
encoding.writeVarUint(enc, CollabMessageType.PAGE_DOC);
84-
encoding.writeVarUint(enc, CollabServerDocMessageType.SINGLE_UPDATE);
85-
encoding.writeVarUint8Array(enc, encryptedUpdate);
86-
if (dbIndex !== undefined) {
87-
encoding.writeVarUint(enc, dbIndex);
88-
}
89-
return encoding.toUint8Array(enc);
90-
}
91-
92-
export function encodePageDocSingleUpdateAck(input: {
93-
updateId: number;
94-
dbIndex?: number;
95-
}): Uint8Array {
96-
const enc = encoding.createEncoder();
97-
encoding.writeVarUint(enc, CollabMessageType.PAGE_DOC);
98-
encoding.writeVarUint(enc, CollabServerDocMessageType.SINGLE_UPDATE_ACK);
99-
encoding.writeVarUint(enc, input.updateId);
100-
if (input.dbIndex !== undefined) {
101-
encoding.writeVarUint(enc, input.dbIndex);
102-
}
103-
return encoding.toUint8Array(enc);
104-
}
105-
10665
export function encodeAwarenessMessage(encryptedChunks: Uint8Array[]): Uint8Array {
10766
const enc = encoding.createEncoder();
10867
encoding.writeVarUint(enc, CollabMessageType.AWARENESS);
@@ -115,7 +74,6 @@ export function encodeAwarenessMessage(encryptedChunks: Uint8Array[]): Uint8Arra
11574

11675
export type DecodedClientCollabMessage =
11776
| { kind: "doc-single"; updateId: number; encryptedUpdate: Uint8Array }
118-
| { kind: "page-doc-single"; updateId: number; encryptedUpdate: Uint8Array }
11977
| { kind: "awareness"; raw: Uint8Array };
12078

12179
export function decodeClientCollabBinaryMessage(message: Uint8Array): DecodedClientCollabMessage | null {
@@ -127,15 +85,6 @@ export function decodeClientCollabBinaryMessage(message: Uint8Array): DecodedCli
12785
if (top === CollabMessageType.AWARENESS) {
12886
return { kind: "awareness", raw: message };
12987
}
130-
if (top === CollabMessageType.PAGE_DOC) {
131-
const docKind = decoding.readVarUint(dec);
132-
if (docKind !== CollabClientDocMessageType.SINGLE_UPDATE) {
133-
return null;
134-
}
135-
const updateId = decoding.readVarUint(dec);
136-
const encryptedUpdate = decoding.readVarUint8Array(dec);
137-
return { kind: "page-doc-single", updateId, encryptedUpdate };
138-
}
13988
if (top !== CollabMessageType.DOC) {
14089
return null;
14190
}
@@ -152,15 +101,10 @@ export type DecodedServerDocMessage =
152101
| { kind: "single-update"; encryptedUpdate: Uint8Array; dbIndex: number | null }
153102
| { kind: "single-update-ack"; updateId: number; dbIndex: number | null };
154103

155-
export type DecodedServerPageDocMessage =
156-
| { kind: "page-single-update"; encryptedUpdate: Uint8Array; dbIndex: number | null }
157-
| { kind: "page-single-update-ack"; updateId: number; dbIndex: number | null };
158-
159104
/** Server/relay → client: awareness broadcast or doc message (legacy framing). */
160105
export type DecodedIncomingCollabMessage =
161106
| { kind: "awareness"; encryptedChunks: Uint8Array[] }
162-
| DecodedServerDocMessage
163-
| DecodedServerPageDocMessage;
107+
| DecodedServerDocMessage;
164108

165109
function decodeServerDocFromDocDecoder(
166110
dec: decoding.Decoder,
@@ -179,23 +123,6 @@ function decodeServerDocFromDocDecoder(
179123
return null;
180124
}
181125

182-
function decodeServerPageDocFromDecoder(
183-
dec: decoding.Decoder,
184-
): DecodedServerPageDocMessage | null {
185-
const docKind = decoding.readVarUint(dec);
186-
if (docKind === CollabServerDocMessageType.SINGLE_UPDATE) {
187-
const encryptedUpdate = decoding.readVarUint8Array(dec);
188-
const dbIndex = decoding.hasContent(dec) ? decoding.readVarUint(dec) : null;
189-
return { kind: "page-single-update", encryptedUpdate, dbIndex };
190-
}
191-
if (docKind === CollabServerDocMessageType.SINGLE_UPDATE_ACK) {
192-
const updateId = decoding.readVarUint(dec);
193-
const dbIndex = decoding.hasContent(dec) ? decoding.readVarUint(dec) : null;
194-
return { kind: "page-single-update-ack", updateId, dbIndex };
195-
}
196-
return null;
197-
}
198-
199126
export function decodeIncomingCollabBinaryMessage(
200127
message: Uint8Array,
201128
): DecodedIncomingCollabMessage | null {
@@ -212,9 +139,6 @@ export function decodeIncomingCollabBinaryMessage(
212139
}
213140
return { kind: "awareness", encryptedChunks };
214141
}
215-
if (top === CollabMessageType.PAGE_DOC) {
216-
return decodeServerPageDocFromDecoder(dec);
217-
}
218142
if (top !== CollabMessageType.DOC) {
219143
return null;
220144
}

new-deepnotes/packages/db/migrations/0002_page_spatial_updates.sql

Lines changed: 0 additions & 8 deletions
This file was deleted.

0 commit comments

Comments
 (0)