Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/features/workspaces/collaborative-content-session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { YServer } from "y-partyserver";

/**
* Base for collaborative session Durable Objects whose canonical content lives
* in an in-memory Yjs document that `onLoad` hydrates from durable storage.
*
* These sessions are reached two ways. Browser editors connect over
* `fetch`/WebSocket, which runs `onStart` — and therefore `onLoad` — before any
* handler touches the document. Server tools instead call methods directly over
* native Durable Object RPC, which does not run `onStart`. A content method
* invoked cold via RPC would read or edit a freshly constructed, empty document:
* returning blank content, or checkpointing an empty document over real content.
*
* Any RPC method that reads or writes the document must `await
* this.ensureLoaded()` first. Methods that only touch durable storage (deletion,
* for example) must not, so hydration stays opt-in per method rather than a
* blanket cost. New editable item types extend this base so the guarantee — and
* the reasoning behind it — live in one place.
*/
export abstract class CollaborativeContentSession extends YServer {
/**
* Runs `onStart`/`onLoad` for this instance if they have not already. Cheap
* and idempotent once started. `__unsafe_ensureInitialized` is the framework's
* sanctioned escape hatch for Durable Objects reached via native RPC; keeping
* the single call here means content methods never touch an unhydrated
* document and the rationale is not repeated at every call site.
*/
protected async ensureLoaded(): Promise<void> {
await this.__unsafe_ensureInitialized();
}
}
8 changes: 6 additions & 2 deletions src/features/workspaces/documents/document-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import {
yDocToProsemirrorJSON,
} from "@tiptap/y-tiptap";
import type { Connection, ConnectionContext } from "partyserver";
import { YServer } from "y-partyserver";
import * as Y from "yjs";
import type { DocumentSessionRouteParams } from "#/features/workspaces/agent-routes";
import { CollaborativeContentSession } from "#/features/workspaces/collaborative-content-session";
import {
parseMarkdownToTiptapDocumentProjection,
serializeTiptapDocumentToMarkdown,
Expand Down Expand Up @@ -62,7 +62,7 @@ export interface DocumentSessionApplyMarkdownEditsResult {
warnings: string[];
}

export class DocumentSession extends YServer {
export class DocumentSession extends CollaborativeContentSession {
static override options = {
hibernate: true,
};
Expand Down Expand Up @@ -131,6 +131,7 @@ export class DocumentSession extends YServer {
async applyMarkdownEdits(
input: DocumentSessionApplyMarkdownEditsInput,
): Promise<DocumentSessionApplyMarkdownEditsResult> {
await this.ensureLoaded();
const currentDocument = this.getCurrentTiptapDocument();
const markdown = serializeTiptapDocumentToMarkdown(currentDocument);
const editResult = applyDocumentMarkdownEdits(markdown, input.edits);
Expand Down Expand Up @@ -176,6 +177,7 @@ export class DocumentSession extends YServer {
async readMarkdownChunk(
input: DocumentMarkdownChunkReadInput,
): Promise<DocumentMarkdownChunkReadResult> {
await this.ensureLoaded();
const stateVector = Uint8Array.from(Y.encodeStateVector(this.document));
let currentSnapshot = this.markdownSnapshot;
if (!currentSnapshot || !uint8ArraysEqual(currentSnapshot.stateVector, stateVector)) {
Expand All @@ -198,6 +200,8 @@ export class DocumentSession extends YServer {
}

async purgeForDeletion(): Promise<void> {
// Deliberately no ensureLoaded(): this only wipes durable storage, so
// hydrating the in-memory document first would be wasted work.
await this.ctx.storage.deleteAll();
}

Expand Down