All checks were successful
CD / Build and push images (push) Successful in 2m51s
CI / Lint, typecheck, test (push) Successful in 1m55s
CI / Auth e2e pack (push) Successful in 2m0s
CI / Build container images (push) Has been skipped
CD / Deploy to Test (push) Successful in 9s
CD / Smoke tests against Test (push) Successful in 1m11s
CD / Promote to Int (push) Successful in 11s
The collaboration server becomes the writer of page state (ADR 0003, realtime-collaboration.md §lifecycle): - onLoadDocument reconstructs a page's Y.Doc from PostgreSQL by applying `pages.ydoc_state` and then every `page_updates` row in order, so a page with a long update log loads correctly. - onStoreDocument persists debounced (2 s, max 30 s): it appends the delta since the last flush to `page_updates`, periodically merges the log back into `ydoc_state` (inline threshold; the session-aware compaction of idle pages remains the separate job, #40), refreshes `page_content_cache` (plain text / Markdown / HTML / outline via the shared derivation, #24), bumps `pages.updated_at`, and keeps `Attachment.pageId` pointed at the embedding page (#31). Each flush runs in one transaction and its duration is logged. - The document size ceiling (MAX_PAGE_DOCUMENT_BYTES) is enforced on store: an oversize document is not persisted and the clients are notified with a stateless error so they can revert. Persistence is an injected port (PagePersistence): the Postgres implementation is covered by a DB-backed test (store/load round-trip, content-cache refresh, a 1000-entry update log, size-ceiling rejection, not-found), and the hook wiring — two-client sync, survival across a server restart, and the size-ceiling stateless notification — by an integration test using an in-memory fake. The collab package gains its own vitest setup that provisions an isolated `_collab` test database. The REST `PUT /pages/:id/state` write path stays in place for now and is retired (410) together with switching the editor to live collaboration in #36, so the deployed editor is never left unable to save between the two deploys. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PGdhRiwU1WRL4XxJfZYipY
44 lines
1.6 KiB
TypeScript
44 lines
1.6 KiB
TypeScript
import * as Y from 'yjs';
|
|
|
|
import type { PagePersistence, StoreResult } from '../persistence.js';
|
|
|
|
/**
|
|
* In-memory {@link PagePersistence} for tests: it keeps the last merged state
|
|
* per page so a "server restart" (destroy + recreate around the same instance)
|
|
* still reloads content, without needing a database. The Postgres implementation
|
|
* is covered separately by `persistence.db.test.ts`.
|
|
*/
|
|
export class InMemoryPagePersistence implements PagePersistence {
|
|
private readonly states = new Map<string, Uint8Array>();
|
|
/** Page ids that should report as missing (to exercise the not-found path). */
|
|
readonly missing = new Set<string>();
|
|
/** Overridable size ceiling so a test can trip it without a 5 MiB document. */
|
|
sizeLimit = Number.POSITIVE_INFINITY;
|
|
/** store() call count, for assertions. */
|
|
storeCalls = 0;
|
|
|
|
async loadInto(pageId: string, doc: Y.Doc): Promise<boolean> {
|
|
if (this.missing.has(pageId)) return false;
|
|
const state = this.states.get(pageId);
|
|
if (state) Y.applyUpdate(doc, state);
|
|
return true;
|
|
}
|
|
|
|
async store(pageId: string, doc: Y.Doc): Promise<StoreResult> {
|
|
this.storeCalls += 1;
|
|
const full = Y.encodeStateAsUpdate(doc);
|
|
if (full.byteLength > this.sizeLimit) {
|
|
return { outcome: 'too_large', bytes: full.byteLength, durationMs: 0, merged: false };
|
|
}
|
|
if (this.missing.has(pageId)) {
|
|
return { outcome: 'not_found', bytes: full.byteLength, durationMs: 0, merged: false };
|
|
}
|
|
this.states.set(pageId, full);
|
|
return { outcome: 'stored', bytes: full.byteLength, durationMs: 0, merged: false };
|
|
}
|
|
|
|
forget(): void {
|
|
// Nothing to release for the in-memory fake.
|
|
}
|
|
}
|