Skip to content
File

Blob: tests/worker/helpers/do.ts

typescript86 lines
1import { env } from "cloudflare:workers";
2import { runInDurableObject } from "cloudflare:test";
3import { drizzle } from "drizzle-orm/durable-sqlite";
4import * as Y from "yjs";
5import * as docSyncSchema from "@/worker/db/docsync-do/schema";
6import { YJS_DOCUMENT_STORE, YJS_PAGE_TITLE } from "@/shared/constants";
7 
8const CHUNK_SIZE = 32 * 1024;
9 
10export async function runInDocSync<R>(
11 pageId: string,
12 fn: (instance: unknown, state: DurableObjectState) => R | Promise<R>,
13): Promise<R> {
14 const stub = env.DocSync.getByName(pageId);
15 return runInDurableObject(stub, fn);
16}
17 
18export async function runInWorkspaceIndexer<R>(
19 workspaceId: string,
20 fn: (instance: unknown, state: DurableObjectState) => R | Promise<R>,
21): Promise<R> {
22 const stub = env.WorkspaceIndexer.getByName(workspaceId);
23 return runInDurableObject(stub, fn);
24}
25 
26function chunkBuffer(buffer: Uint8Array, size: number): Uint8Array[] {
27 const chunks: Uint8Array[] = [];
28 for (let i = 0; i < buffer.byteLength; i += size) {
29 chunks.push(buffer.slice(i, Math.min(i + size, buffer.byteLength)));
30 }
31 return chunks;
32}
33 
34/**
35 * Builds a Yjs update payload with a title text and one paragraph of body
36 * text in the shape that `extractPlaintext` walks (title in the shared
37 * `YJS_PAGE_TITLE` Y.Text, body as a `paragraph` `Y.XmlElement` containing
38 * a `Y.XmlText` inside the `YJS_DOCUMENT_STORE` fragment).
39 */
40export function buildYjsDocBytes(title: string, bodyText: string): Uint8Array {
41 const doc = new Y.Doc();
42 try {
43 if (title) {
44 doc.getText(YJS_PAGE_TITLE).insert(0, title);
45 }
46 if (bodyText) {
47 const fragment = doc.getXmlFragment(YJS_DOCUMENT_STORE);
48 const paragraph = new Y.XmlElement("paragraph");
49 paragraph.insert(0, [new Y.XmlText(bodyText)]);
50 fragment.insert(0, [paragraph]);
51 }
52 return Y.encodeStateAsUpdate(doc);
53 } finally {
54 doc.destroy();
55 }
56}
57 
58/**
59 * Writes persisted Yjs snapshot bytes directly into the DocSync DO-local
60 * SQLite via `runInDurableObject`. The rows (`snapshot_chunks` +
61 * `snapshot_meta`) and the 32 KiB chunking match what `DocSync.onSave`
62 * writes, so `getSnapshotResponse` / `getIndexPayload` read them back the
63 * same way whether the bytes came from a live client session or this
64 * helper.
65 */
66export async function seedDocSyncSnapshot(pageId: string, bytes: Uint8Array): Promise<void> {
67 await runInDocSync(pageId, (_instance, state) => {
68 const doDb = drizzle(state.storage, { schema: docSyncSchema });
69 const chunks = chunkBuffer(bytes, CHUNK_SIZE);
70 const now = new Date().toISOString();
71 doDb.transaction((tx) => {
72 tx.delete(docSyncSchema.snapshotChunks).run();
73 for (let i = 0; i < chunks.length; i++) {
74 tx.insert(docSyncSchema.snapshotChunks).values({ chunk_index: i, data: chunks[i] }).run();
75 }
76 tx.insert(docSyncSchema.snapshotMeta)
77 .values({ id: 1, chunk_count: chunks.length, total_bytes: bytes.byteLength, snapshot_at: now })
78 .onConflictDoUpdate({
79 target: docSyncSchema.snapshotMeta.id,
80 set: { chunk_count: chunks.length, total_bytes: bytes.byteLength, snapshot_at: now },
81 })
82 .run();
83 });
84 });
85}