File
Blob: tests/worker/helpers/do.ts
| 1 | import { env } from "cloudflare:workers"; |
| 2 | import { runInDurableObject } from "cloudflare:test"; |
| 3 | import { drizzle } from "drizzle-orm/durable-sqlite"; |
| 4 | import * as Y from "yjs"; |
| 5 | import * as docSyncSchema from "@/worker/db/docsync-do/schema"; |
| 6 | import { YJS_DOCUMENT_STORE, YJS_PAGE_TITLE } from "@/shared/constants"; |
| 7 | |
| 8 | const CHUNK_SIZE = 32 * 1024; |
| 9 | |
| 10 | export 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 | |
| 18 | export 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 | |
| 26 | function 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 | */ |
| 40 | export 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 | */ |
| 66 | export 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 | } |