@ai-matrx/office 0.4.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/univer/isSnapshotMutation.ts","../src/docs/document-model.ts","../src/docs/document-write-validation.ts","../src/docs/fit-zoom-ratio.ts","../src/docs/documents-hub-display.ts","../src/docs/doc-symbols.ts","../src/docs/document-body-text.ts"],"sourcesContent":["/**\n * isSnapshotMutation — the autosave dirty-filter for the Univer editors.\n *\n * D97: `onCommandExecuted` fires for EVERY command — including viewport\n * scrolls and selection moves — so an unfiltered listener marked the doc\n * dirty on mere scrolling and wrote a new snapshot row (version history +\n * collab broadcast) 2.5s later. Only CommandType.MUTATION changes the data\n * that a snapshot persists (Univer's own docs: OPERATION = \"not saved to\n * snapshot, such as modifying scroll position\"). We therefore accept ONLY\n * mutations, and additionally deny-list any mutation whose id marks it as\n * scroll / selection / viewport bookkeeping, belt-and-braces.\n */\nimport type { ICommandInfo } from \"@univerjs/core\";\n\n/**\n * Univer's `CommandType.MUTATION`. A literal, so this filter (and the document\n * model that uses it outside the editor chunk) never pulls Univer's runtime —\n * which touches `window` at import — into a server-rendered chunk.\n */\nconst MUTATION = 2;\n\n/** Non-content command ids that must never mark the document dirty. */\nconst NON_CONTENT_ID_PATTERN =\n /(scroll|selection|set-selections|viewport|zoom|hover|cursor|focus|activate)/i;\n\nexport function isSnapshotMutation(command: ICommandInfo): boolean {\n if (command.type !== MUTATION) return false;\n if (NON_CONTENT_ID_PATTERN.test(command.id)) return false;\n return true;\n}\n\n/**\n * Mutations a SPREADSHEET fires on its own while it opens and recalculates —\n * they change nothing the person typed, so they must never mark the workbook\n * dirty. Observed on a plain open of a workbook with formulas (2026-10-10):\n * `formula.mutation.set-trigger-formula-calculation-start`,\n * `...set-formula-calculation-start`, `...set-formula-calculation-notification`\n * and a `doc.mutation.rich-text-editing` from the internal cell-editor document.\n * Each open then wrote a new snapshot (history bloat, and a false \"both changed\"\n * against Google). A real edit always arrives as a `sheet.mutation.*`.\n */\nconst WORKBOOK_SELF_MUTATION_PATTERN =\n /^(formula\\.mutation\\.[a-z-]*(calculation|trigger)[a-z-]*|doc\\.mutation\\.)/;\n\nexport function isWorkbookSnapshotMutation(command: ICommandInfo): boolean {\n if (!isSnapshotMutation(command)) return false;\n return !WORKBOOK_SELF_MUTATION_PATTERN.test(command.id);\n}\n","/**\n * THE DOCUMENT MODEL — the engine of one cloud document per tab.\n *\n * Before this, the document lived only inside each `DocumentEditor`'s Univer\n * instance. A remount (a board tile waking, a tile removed and undone, a route\n * change and back) rebuilt the editor from the SERVER copy, so whatever the\n * last save had not carried yet was gone; two mounts of one document were two\n * editors, each saving full snapshots of its own copy (last write wins), each\n * with its own realtime channel and its own collab room.\n *\n * The document is a record on THE one working-copy primitive\n * (`@ai-matrx/kit/working-copy`, kind `udt_document`,\n * `./documentModels.ts`): its save status, dirty and view count are Redux state\n * (`workingCopies[\"udt_document:<id>\"]`), its one coalesced save is the kind's.\n * What Redux cannot hold — Univer's body — is this engine, registered on the\n * record's session under the same key:\n *\n * - BODY. Every local mutation in one view is replayed into every other view\n * of the document (the same mutation stream collab sends between machines),\n * so two views show one document. A new view boots from the engine's latest\n * state — a live view's snapshot, or the snapshot the last view left behind\n * — never the stale server copy while the record's session is alive.\n * - SIDE EFFECTS, ONCE. The snapshot realtime channel, the collab room, and\n * the pagehide / visibility / beforeunload flush are opened once per\n * document and closed once.\n * - THE EDITOR ITSELF, KEPT. Univer keeps its undo / redo history inside the\n * editor instance, so rebuilding the editor on a remount threw the history\n * away. When the last view leaves, its editor (a Univer instance with its\n * own DOM host) is parked here — off the page, alive — and the next view of\n * the document RE-ATTACHES that same instance to its own container instead\n * of booting a new one (`parkEditor` / `takeParkedEditor`). Undo and redo\n * therefore work across hide / show, a remount and a removed tile undone,\n * for as long as the record's session is warm; `close()` disposes it.\n */\n\nimport type { ICommandInfo, IDocumentData } from \"@univerjs/core\";\nimport type { CommitReason, WorkingCopyHandle } from \"@ai-matrx/kit/working-copy\";\nimport type { AwarenessState } from \"@ai-matrx/data/realtime/collab\";\nimport { isSnapshotMutation } from \"../univer\";\nimport type {\n CollabMutationInfo,\n CommandServiceLike,\n} from \"../univer/collab\";\n\n/** Debounce from the last edit to the save. */\nexport const DOCUMENT_SAVE_DELAY_MS = 2500;\n\nexport type DocumentSnapshotData = Partial<IDocumentData>;\n\n/** What a view lends the model while it is mounted. */\nexport interface DocumentViewPort {\n /** Univer's command service for this view's instance. */\n commandService: CommandServiceLike;\n /** The live document of this view (`doc.save()`), or null if it has none. */\n snapshot: () => DocumentSnapshotData | null;\n /** Replace the whole document in this view (a collaborator's snapshot). */\n remount: (snapshot: DocumentSnapshotData) => void;\n /** Whether this view may change the document (read at edit time). */\n editable: () => boolean;\n /**\n * The Univer unit id of this view's mounted document. Every seat mounts its own unit, so a\n * mutation carries the SENDER's id and must be re-addressed to this view's unit before it is\n * applied. Falls back to the id of `snapshot()` when a view does not state it.\n */\n unitId?: () => string | null;\n}\n\n/** Everything the engine reaches outside itself — injected so it is testable. */\nexport interface DocumentModelDeps {\n /** The newest stored snapshot (its row id + body), or null for a document never saved. */\n loadLatest: (documentId: string) => Promise<{ id: string; snapshot: DocumentSnapshotData } | null>;\n save: (input: {\n documentId: string;\n snapshot: DocumentSnapshotData;\n origin: \"autosave\" | \"manual\";\n }) => Promise<{ id: string; createdBy: string | null }>;\n onSaved?: (documentId: string, origin: \"autosave\" | \"manual\") => void;\n /** Window-level listeners (pagehide, visibilitychange, beforeunload). */\n listenToPage?: (handlers: { flush: () => void; hasUnsaved: () => boolean }) => () => void;\n /** A mutation could not be applied to a view: the app says so (a notice); the model re-syncs from the saved version. */\n onApplyFailed?: (documentId: string, mutationId: string, error: unknown) => void;\n}\n\ninterface AttachedView {\n id: number;\n port: DocumentViewPort;\n dispose: () => void;\n}\n\n/** A collab room (WorkbookCollabSession) bound to the model's command stream. */\nexport interface DocumentCollabHandle {\n stop: () => void;\n isHost: () => boolean;\n /** This tab replaced the whole document and saved it as `snapshotId`: peers reload it. */\n announceReplaced: (snapshotId: string) => void;\n}\n\n/** Who else is in the document's room (shared by every view of it). */\nexport interface DocumentAwareness {\n states: Map<number, AwarenessState>;\n selfUid: string;\n}\n\n/**\n * An editor instance kept alive between views of the document: its DOM host\n * (re-parented into the next view's container) and the instance behind it,\n * whose undo history survives because the instance does. Opaque to the model\n * apart from what it calls.\n */\nexport interface ParkedDocumentEditor<T = unknown> {\n /** The element the editor renders into; the next view appends it to its container. */\n element: HTMLElement;\n instance: T;\n /** Replace the whole document (a collaborator's snapshot arrived while parked). */\n remount: (snapshot: DocumentSnapshotData) => void;\n /** Throw the instance away (the record's session closed). */\n dispose: () => void;\n}\n\nexport type DocumentCollabFactory = (\n commandService: CommandServiceLike,\n onAwareness: (awareness: DocumentAwareness) => void,\n /** A peer replaced the whole document (history restore, Google update). */\n onReplaced: () => void,\n) => Promise<DocumentCollabHandle | null>;\n\nlet viewIds = 0;\n\nexport class DocumentModel {\n readonly documentId: string;\n private readonly deps: DocumentModelDeps;\n private readonly handle: WorkingCopyHandle;\n private readonly views = new Map<number, AttachedView>();\n private lastEditedView: number | null = null;\n /** The document as the last view left it, or as last loaded / saved. */\n private stored: DocumentSnapshotData | null = null;\n private loading: Promise<DocumentSnapshotData | null> | null = null;\n private relaying = false;\n /** Snapshot rows this tab already holds (loaded or written) — never re-applied. */\n private readonly knownSnapshots = new Set<string>();\n private readonly collabListeners = new Set<(info: CollabMutationInfo) => void>();\n private collab: DocumentCollabHandle | null = null;\n private collabStarting: Promise<void> | null = null;\n private awareness: DocumentAwareness | null = null;\n private readonly awarenessListeners = new Set<() => void>();\n private realtimeClose: (() => void) | null = null;\n private pageClose: (() => void) | null = null;\n /** The last view's editor, alive and off the page until a view takes it back. */\n private parked: ParkedDocumentEditor | null = null;\n\n constructor(handle: WorkingCopyHandle, deps: DocumentModelDeps) {\n this.documentId = handle.id;\n this.handle = handle;\n this.deps = deps;\n }\n\n // ── body ────────────────────────────────────────────────────────────────\n\n /** The newest state of the body this tab holds, without a network read. */\n latest(): DocumentSnapshotData | null {\n const preferred = this.lastEditedView !== null ? this.views.get(this.lastEditedView) : undefined;\n for (const view of preferred ? [preferred, ...this.views.values()] : this.views.values()) {\n try {\n const snapshot = view.port.snapshot();\n if (snapshot) return snapshot;\n } catch (error) {\n console.warn(`[document-model] a view of ${this.documentId} could not report its document`, error);\n }\n }\n return this.stored;\n }\n\n /**\n * What a NEW view should boot from: the model's latest state, or (only when\n * this tab holds none) the newest server snapshot. Null = an empty document.\n * Re-read `latest()` synchronously right before mounting — another view may\n * have typed while the server read was in flight.\n */\n async openingSnapshot(createEmpty: () => DocumentSnapshotData): Promise<DocumentSnapshotData> {\n const held = this.latest();\n if (held) return held;\n if (!this.loading) {\n this.loading = this.deps.loadLatest(this.documentId).then(\n (row) => {\n if (row) {\n this.knownSnapshots.add(row.id);\n this.savedFingerprint = contentFingerprint(row.snapshot);\n }\n // A document never saved opens as ONE empty document for every view\n // (one unit id), so their edits replay into each other.\n if (!this.stored) this.stored = row ? row.snapshot : createEmpty();\n return this.latest() ?? this.stored;\n },\n (error: unknown) => {\n this.loading = null;\n throw error;\n },\n );\n }\n return (await this.loading) ?? createEmpty();\n }\n\n /** A view's Univer instance is ready: it shares the document from now on. */\n attachView(port: DocumentViewPort): () => void {\n const id = ++viewIds;\n const subscription = port.commandService.onMutationExecutedForCollab((info, options) =>\n this.onViewMutation(id, info, options),\n );\n this.views.set(id, { id, port, dispose: () => subscription.dispose() });\n // Nothing unsaved: what this view mounted IS the saved version (Univer and\n // the snapshot repair may restate it — page geometry, defaults — without\n // anyone changing a word). That is the baseline a save compares against.\n if (!this.handle.hasPending()) {\n try {\n const mounted = port.snapshot();\n if (mounted) this.savedFingerprint = contentFingerprint(mounted);\n } catch (error) {\n console.warn(`[document-model] could not read ${this.documentId}'s mounted document`, error);\n }\n }\n let detached = false;\n return () => {\n if (detached) return;\n detached = true;\n const view = this.views.get(id);\n if (!view) return;\n // Whatever this view shows is the newest body when it is the last one:\n // keep it, so a remount (or the flush on the way out) reads it.\n if (this.views.size === 1) {\n try {\n const snapshot = port.snapshot();\n if (snapshot) this.stored = snapshot;\n } catch (error) {\n console.warn(`[document-model] could not keep ${this.documentId}'s last view's document`, error);\n }\n }\n view.dispose();\n this.views.delete(id);\n if (this.lastEditedView === id) this.lastEditedView = null;\n };\n }\n\n /**\n * The last view of the document is leaving: keep its editor (and Univer's\n * undo history inside it) for the next view. Call AFTER the view's\n * `attachView` detach. Refused — the caller disposes its editor — while\n * another view is still attached (it carries the document) or one is\n * already kept.\n */\n parkEditor(editor: ParkedDocumentEditor): boolean {\n if (this.closed || this.views.size > 0 || this.parked) return false;\n this.parked = editor;\n return true;\n }\n\n /** The kept editor, handed to the view that mounts next (null when none is kept). */\n takeParkedEditor(): ParkedDocumentEditor | null {\n const editor = this.parked;\n this.parked = null;\n return editor;\n }\n\n private onViewMutation(\n viewId: number,\n info: Readonly<CollabMutationInfo>,\n options?: { fromCollab?: boolean; fromChangeset?: boolean; onlyLocal?: boolean },\n ): void {\n // Our own replay into this view, a collab peer's edit, or a load: not a\n // new local edit (each is accounted where it came from).\n if (this.relaying || options?.fromCollab || options?.fromChangeset) return;\n const view = this.views.get(viewId);\n if (!view || !view.port.editable()) return;\n this.lastEditedView = viewId;\n this.relay(info, viewId);\n for (const listener of this.collabListeners) listener(info as CollabMutationInfo);\n if (isContentMutation(info)) this.markDirty();\n }\n\n /** The unit id a view's document has (what a replayed mutation must be addressed to). */\n private unitIdOf(view: AttachedView): string | null {\n try {\n return view.port.unitId?.() ?? view.port.snapshot()?.id ?? null;\n } catch {\n return null;\n }\n }\n\n /** Replay one mutation into every view except `exceptViewId`, addressed to each view's own unit. */\n private relay(info: Readonly<CollabMutationInfo>, exceptViewId: number | null): void {\n if (this.views.size === 0 || (this.views.size === 1 && exceptViewId !== null)) return;\n this.relaying = true;\n let failure: unknown = null;\n try {\n for (const view of this.views.values()) {\n if (view.id === exceptViewId) continue;\n try {\n const params = info.params as unknown;\n const unitId = this.unitIdOf(view);\n const addressed =\n unitId && params && typeof params === \"object\" && !Array.isArray(params) && typeof (params as { unitId?: unknown }).unitId === \"string\" && (params as { unitId: string }).unitId !== unitId\n ? { ...(params as Record<string, unknown>), unitId }\n : info.params;\n view.port.commandService.syncExecuteCommand(info.id, addressed as never, { onlyLocal: true, fromCollab: true });\n } catch (error) {\n failure ??= error;\n console.error(\n `[document-model] a second view of ${this.documentId} could not apply \"${info.id}\" — it may show an older document until it reopens`,\n error,\n );\n }\n }\n } finally {\n this.relaying = false;\n }\n if (failure !== null) this.applyFailed(info.id, failure);\n }\n\n /** A view could not follow an edit: say so, then show the saved version (unless this tab has unsaved edits of its own). */\n private applyFailed(mutationId: string, error: unknown): void {\n try {\n this.deps.onApplyFailed?.(this.documentId, mutationId, error);\n } catch (notifyError) {\n console.warn(`[document-model] the notice for ${this.documentId}'s failed apply also failed`, notifyError);\n }\n if (this.hasUnsaved()) return;\n void this.deps\n .loadLatest(this.documentId)\n .then((row) => {\n if (row && !this.closed) this.show(row);\n })\n .catch((e) => console.warn(`[document-model] could not re-sync ${this.documentId} after a failed apply`, e));\n }\n\n // ── save (run by the record's one coalesced save — documentModels.ts) ─\n\n private markDirty(): void {\n this.handle.touch();\n }\n\n private saving = false;\n /**\n * The content of the version the server holds (loaded or last written). A\n * save of the same content is no save: attaching, detaching, waking or a\n * flush of an unchanged document never writes a snapshot row.\n */\n private savedFingerprint: string | null = null;\n\n /**\n * Write the body as it is now. Returns false when this tab is a collab peer\n * that does not write (the elected host writes the canonical snapshot; a\n * peer's edits reach it through the room). An explicit Save always writes.\n */\n async write(reason: CommitReason): Promise<boolean> {\n const snapshot = this.latest();\n if (!snapshot) throw new Error(\"There is no open document to save.\");\n if (this.collab && !this.collab.isHost() && reason !== \"manual\") return false;\n const fingerprint = contentFingerprint(snapshot);\n if (reason !== \"manual\" && fingerprint === this.savedFingerprint) return false;\n const origin = reason === \"manual\" ? \"manual\" : \"autosave\";\n this.saving = true;\n try {\n const { id } = await this.deps.save({ documentId: this.documentId, snapshot, origin });\n this.knownSnapshots.add(id);\n this.savedFingerprint = fingerprint;\n } finally {\n this.saving = false;\n }\n if (this.views.size === 0) this.stored = snapshot;\n this.deps.onSaved?.(this.documentId, origin);\n return true;\n }\n\n private hasUnsaved(): boolean {\n return this.handle.hasPending();\n }\n\n hasViews(): boolean {\n return this.views.size > 0;\n }\n\n // ── realtime (snapshot inserts) ─────────────────────────────────────────\n\n /** Opened once per document by the first view that asks; idempotent. */\n connectRealtime(open: (onSnapshot: (snapshotId: string) => void) => () => void): void {\n if (this.realtimeClose) return;\n this.realtimeClose = open((snapshotId) => void this.onRemoteSnapshot(snapshotId));\n }\n\n /**\n * A snapshot was committed (here or elsewhere): show a foreign one in every\n * view. Over unsaved edits it is a CONFLICT — the working copy holds it and\n * no save runs (one would overwrite the collaborator's snapshot) until the\n * person chooses; `overUnsaved` is \"Take theirs\" (`./documentModels.ts`).\n */\n async onRemoteSnapshot(snapshotId: string, { overUnsaved = false }: { overUnsaved?: boolean } = {}): Promise<void> {\n if (this.collab) return; // the room is the live channel; snapshots are checkpoints\n if (this.knownSnapshots.has(snapshotId)) return; // ours, or already shown\n if (this.saving) return; // our own save's echo, ahead of its response\n if (this.hasUnsaved() && !overUnsaved) {\n this.handle.conflict({ ref: snapshotId });\n return;\n }\n const row = await this.deps.loadLatest(this.documentId);\n if (!row || (this.hasUnsaved() && !overUnsaved) || this.knownSnapshots.has(row.id)) return;\n this.show(row);\n }\n\n /**\n * THE content swap (see @ai-matrx/office/univer openEditorContent): a SAVED snapshot\n * replaces the document in every view and the kept editor. It is the saved\n * version from now on, so the pending save compares equal and writes\n * nothing (it would put the old content back over the new). In a collab\n * room the peers reload it and the shared log is emptied.\n */\n replaceContent(row: { id: string; snapshot: DocumentSnapshotData }): void {\n this.show(row);\n this.collab?.announceReplaced(row.id);\n }\n\n /** A peer replaced the document: show the newest save (once). */\n private async onPeerReplaced(): Promise<void> {\n const row = await this.deps.loadLatest(this.documentId);\n if (!row || this.knownSnapshots.has(row.id)) return;\n this.show(row);\n }\n\n private show(row: { id: string; snapshot: DocumentSnapshotData }): void {\n this.knownSnapshots.add(row.id);\n this.savedFingerprint = contentFingerprint(row.snapshot);\n this.stored = row.snapshot;\n this.relaying = true;\n try {\n for (const view of this.views.values()) view.port.remount(row.snapshot);\n this.parked?.remount(row.snapshot);\n } finally {\n this.relaying = false;\n }\n }\n\n // ── collab (one room per document) ──────────────────────────────────────\n\n /**\n * The model's command stream as ONE command service — what the collab room\n * binds to. Local edits from any view go out once; a peer's edit is applied\n * to every view.\n */\n private readonly virtualCommandService: CommandServiceLike = {\n onMutationExecutedForCollab: (listener) => {\n this.collabListeners.add(listener);\n return { dispose: () => this.collabListeners.delete(listener) };\n },\n syncExecuteCommand: (id, params) => {\n const info = { id, type: 2, params } as CollabMutationInfo;\n this.relay(info, null);\n // The host writes what peers typed.\n if (isContentMutation(info)) this.markDirty();\n return true;\n },\n };\n\n /** Join the document's collab room once (the first view that asks). */\n startCollab(factory: DocumentCollabFactory): Promise<void> {\n if (this.collab) return Promise.resolve();\n if (!this.collabStarting) {\n this.collabStarting = factory(\n this.virtualCommandService,\n (awareness) => {\n this.awareness = awareness;\n for (const listener of Array.from(this.awarenessListeners)) listener();\n },\n () => {\n void this.onPeerReplaced().catch((error: unknown) =>\n console.warn(`[document-model] could not load ${this.documentId}'s replaced version`, error),\n );\n },\n )\n .then((handle) => {\n if (!handle) return;\n if (this.views.size === 0) {\n handle.stop();\n return;\n }\n this.collab = handle;\n })\n .finally(() => {\n this.collabStarting = null;\n });\n }\n return this.collabStarting;\n }\n\n isCollabHost(): boolean {\n return this.collab ? this.collab.isHost() : true;\n }\n\n getAwareness(): DocumentAwareness | null {\n return this.awareness;\n }\n\n subscribeAwareness(listener: () => void): () => void {\n this.awarenessListeners.add(listener);\n return () => this.awarenessListeners.delete(listener);\n }\n\n // ── lifecycle (driven by the record's session) ──────────────────────\n\n /** A view arrived on a document with none. */\n wake(): void {\n if (!this.pageClose && this.deps.listenToPage) {\n this.pageClose = this.deps.listenToPage({\n flush: () => void this.handle.flush(),\n hasUnsaved: () => this.hasUnsaved(),\n });\n }\n }\n\n /**\n * The last view left: leave the collab room (presence must not outlive the\n * person). The snapshot channel stays open while the session is kept alive,\n * so a collaborator's save still reaches the body a returning view boots from.\n */\n rest(): void {\n this.collab?.stop();\n this.collab = null;\n this.awareness = null;\n }\n\n private closed = false;\n\n close(): void {\n this.closed = true;\n const parked = this.parked;\n this.parked = null;\n if (parked) {\n try {\n parked.dispose();\n } catch (error) {\n console.warn(`[document-model] could not dispose ${this.documentId}'s kept editor`, error);\n }\n }\n this.realtimeClose?.();\n this.realtimeClose = null;\n this.pageClose?.();\n this.pageClose = null;\n }\n}\n\n/**\n * What a snapshot STORES, as a comparable string: everything but the unit id\n * and Univer's revision counter (both move without the content moving).\n */\nfunction contentFingerprint(snapshot: DocumentSnapshotData): string {\n const { id: _id, rev: _rev, ...content } = snapshot as DocumentSnapshotData & { rev?: unknown };\n return JSON.stringify(content);\n}\n\n/** Mutations that change what a snapshot stores (never scroll / selection) — the editors' one dirty filter. */\nfunction isContentMutation(info: Readonly<CollabMutationInfo>): boolean {\n return isSnapshotMutation({ id: info.id, type: info.type, params: info.params } as ICommandInfo);\n}\n","/**\n * Pure validation for the `matrx-user/documents` write targets.\n *\n * Deliberately OUTSIDE the React updater. `applySurfaceWrite`\n * (`features/surfaces/runtime/surface-writeback.ts`) turns a THROW from a\n * handler into the error envelope the agent reads back. Validating inside a\n * `setState` updater would instead throw during React's render/commit — after\n * the seam has already reported success — so the agent would be told its bad\n * value landed. Validate here, synchronously, before touching any state.\n *\n * `DOCUMENT_NAME_MAX_LENGTH` is the single source of truth for the limit: it is\n * enforced here, interpolated into the manifest target description, AND set as\n * the `maxLength` of the header rename field on `/documents/[id]` — so the\n * contract the agent reads, the rule the handler enforces, and the limit the\n * human's own control applies cannot drift apart. The last of those matters\n * for a non-obvious reason: the blur commit swallows service failures, so\n * before the field was bounded a pasted 300-character title came back as a\n * `varchar(255)` 400 and reverted with nothing on screen to explain it.\n */\n\n/**\n * `workbench.udt_documents.document_name` is `varchar(255) NOT NULL` — a\n * longer value is rejected by Postgres, not merely by taste. Verified against\n * `information_schema.columns` on 2026-08-11.\n */\nexport const DOCUMENT_NAME_MAX_LENGTH = 255;\n\n/**\n * `workbench.udt_documents.description` is unbounded `text`, so this is a\n * product bound rather than a database one: it matches the 2000 the sibling\n * `workbook_description` target enforces, keeping the two mirrored services\n * mirrored in behaviour too. The library renders this as a two-line blurb —\n * anything near the cap is already past what a reader sees.\n */\nexport const DOCUMENT_DESCRIPTION_MAX_LENGTH = 2000;\n\n/** How a rejected value is described back to the agent, without dumping it. */\nfunction describeValue(value: unknown): string {\n if (value === null) return \"null\";\n if (Array.isArray(value)) return \"an array\";\n if (typeof value === \"object\") return \"an object\";\n return `a ${typeof value}`;\n}\n\n/**\n * Validate an agent-supplied document title and return the exact string to\n * persist. Throws (never coerces) so the agent hears about its own mistake.\n *\n * The `typeof` guard is load-bearing: the inline-tool layer PARSES a\n * JSON-looking argument before the handler ever sees it, so a caller that\n * sends `{\"document_name\":\"...\"}` or `\"\\\"Quoted\\\"\"` arrives here as an object,\n * not text. The message names the fix explicitly, because an agent that only\n * hears \"invalid\" tends to retry by double-encoding — which lands escaped\n * quotes and `\\n` in the user's title box.\n */\nexport function validateDocumentName(value: unknown): string {\n if (typeof value !== \"string\") {\n throw new Error(\n `document_name must be plain text, not JSON and not JSON-encoded — received ${describeValue(\n value,\n )}. Send only the title itself (for example: Q3 Revenue Review) with no surrounding braces, brackets, quotes or escape sequences.`,\n );\n }\n\n const trimmed = value.trim();\n\n if (trimmed.length === 0) {\n throw new Error(\n \"document_name cannot be empty or whitespace — a document must keep a title. Send the title text you want, or leave the current one alone.\",\n );\n }\n\n if (/[\\r\\n]/.test(trimmed)) {\n throw new Error(\n \"document_name must be a SINGLE line — it renders in the page header, and line breaks are not a title. Send one line of plain text (this is a title, not the document body).\",\n );\n }\n\n if (trimmed.length > DOCUMENT_NAME_MAX_LENGTH) {\n throw new Error(\n `document_name must be ${DOCUMENT_NAME_MAX_LENGTH} characters or fewer (received ${trimmed.length}). Shorten it to a title; the document body is not stored in this field.`,\n );\n }\n\n return trimmed;\n}\n\n/**\n * Validate an agent-supplied document description and return the value to\n * persist — `null` for a deliberate clear, so the caller never has to decide\n * whether \"\" means \"empty string\" or \"no description\".\n *\n * Unlike the title this MAY span lines (it is a blurb, not a header), but it\n * carries the same not-JSON guard for the same reason: the inline-tool layer\n * parses a JSON-looking argument before the handler sees it.\n */\nexport function validateDocumentDescription(value: unknown): string | null {\n if (typeof value !== \"string\") {\n throw new Error(\n `document_description must be plain text, not JSON and not JSON-encoded — received ${describeValue(\n value,\n )}. Send only the description prose itself, with no surrounding braces, brackets, quotes or escape sequences.`,\n );\n }\n\n const trimmed = value.trim();\n\n // An empty string is the documented way to CLEAR the description, so it is\n // valid input rather than a bad shape — normalize it to null for the column.\n if (trimmed.length === 0) return null;\n\n if (trimmed.length > DOCUMENT_DESCRIPTION_MAX_LENGTH) {\n throw new Error(\n `document_description must be ${DOCUMENT_DESCRIPTION_MAX_LENGTH} characters or fewer (received ${trimmed.length}). This is the short blurb shown in the documents library, not the document body.`,\n );\n }\n\n return trimmed;\n}\n","/**\n * The zoom that fits a document page inside a narrow editor (a phone), so the\n * page reflows into view instead of scrolling sideways. Never above 100%, and\n * never so small the text is unreadable. Pure — guarded by fit-zoom-ratio.test.ts.\n */\nexport const MIN_FIT_ZOOM = 0.45;\n\nexport function fitZoomRatio(containerWidth: number, pageWidth: number, gutter = 8): number {\n if (!(containerWidth > 0) || !(pageWidth > 0)) return 1;\n const ratio = (containerWidth - gutter * 2) / pageWidth;\n return Math.min(1, Math.max(MIN_FIT_ZOOM, Math.round(ratio * 100) / 100));\n}\n","import type { DocumentSource } from \"../core\";\n\nconst SOURCE_LABELS: Record<DocumentSource, string> = {\n created: \"Created\",\n imported_docx: \"Imported DOCX\",\n imported_md: \"Imported Markdown\",\n imported_txt: \"Imported Text\",\n linked_gdoc: \"Imported from Google Docs\",\n};\n\nexport function documentSourceLabel(source: DocumentSource): string {\n return SOURCE_LABELS[source] ?? source;\n}\n","/** Insert, Emoji and Special characters: searchable sets, a name per entry. */\n\nexport interface Glyph {\n char: string;\n name: string;\n}\n\nconst e = (rows: string): Glyph[] =>\n rows.split(\"|\").map((r) => r.trim()).filter(Boolean).map((r) => {\n const [char, ...name] = r.split(\" \");\n return { char: char!, name: name.join(\" \") };\n });\n\nexport const EMOJI_GROUPS: Array<{ id: string; label: string; items: Glyph[] }> = [\n { id: \"faces\", label: \"Faces\", items: e(\"😀 grinning|😃 smile|😄 happy|😁 beaming|😆 laughing|😅 sweat smile|😂 tears of joy|🤣 rolling|🙂 slight smile|🙃 upside down|😉 wink|😊 blush|😇 angel|🥰 love|😍 heart eyes|🤩 star struck|😘 kiss|😋 yum|😎 cool|🤓 nerd|🤔 thinking|🤨 skeptical|😐 neutral|🙄 eye roll|😬 grimace|😴 sleeping|😢 cry|😭 sobbing|😤 huff|😡 angry|🤯 mind blown|😱 scream|🥳 party|🤗 hug|🫡 salute|😮 surprised|😳 flushed|🥺 pleading|😷 mask|🤒 sick\") },\n { id: \"people\", label: \"Gestures\", items: e(\"👍 thumbs up|👎 thumbs down|👏 clap|🙌 raised hands|🙏 pray thanks|🤝 handshake|💪 strong flex|👌 ok|✌ victory peace|🤞 fingers crossed|👋 wave|✋ raised hand|👆 point up|👉 point right|👈 point left|👇 point down|✍ writing|🤷 shrug|🙋 raise hand|👀 eyes|🧠 brain|❤ heart|💔 broken heart|💯 hundred\") },\n { id: \"objects\", label: \"Objects\", items: e(\"💡 idea light bulb|📌 pin|📎 paperclip|📝 memo note|📅 calendar|📈 chart up|📉 chart down|📊 bar chart|📁 folder|📄 page|📧 email|📞 phone|💻 laptop|📱 mobile|🔒 lock|🔑 key|🔍 search|🔔 bell|⏰ alarm clock|⌛ hourglass|🎯 target|🏆 trophy|🎁 gift|📦 package|🛠 tools|⚙ gear|💰 money bag|💵 dollar|🧾 receipt|📚 books\") },\n { id: \"symbols\", label: \"Marks\", items: e(\"✅ check mark|❌ cross|⭐ star|🌟 glowing star|✨ sparkles|🔥 fire|⚡ lightning|🚀 rocket|🎉 party popper|🎊 confetti|⚠ warning|❓ question|❗ exclamation|➕ plus|➖ minus|🔴 red circle|🟠 orange circle|🟡 yellow circle|🟢 green circle|🔵 blue circle|🟣 purple circle|⬆ up|⬇ down|➡ right|⬅ left|🔁 repeat|✔ heavy check\") },\n { id: \"nature\", label: \"Nature and food\", items: e(\"🌞 sun|🌙 moon|☁ cloud|🌧 rain|❄ snowflake|🌈 rainbow|🌱 seedling|🌳 tree|🌸 blossom|🌍 earth|🐶 dog|🐱 cat|🦊 fox|🐝 bee|🦋 butterfly|🍎 apple|🍕 pizza|🍔 burger|☕ coffee|🍺 beer|🍷 wine|🎂 cake|🍪 cookie|🥑 avocado\") },\n];\n\nconst s = (chars: string, names: string[]): Glyph[] => Array.from(chars).map((char, i) => ({ char, name: names[i] ?? char }));\n\nexport const SPECIAL_GROUPS: Array<{ id: string; label: string; items: Glyph[] }> = [\n { id: \"punctuation\", label: \"Punctuation\", items: s(\"©®™°§¶†‡•…‰№‘’“”«»–—¡¿·\", [\"copyright\", \"registered\", \"trademark\", \"degree\", \"section\", \"pilcrow\", \"dagger\", \"double dagger\", \"bullet\", \"ellipsis\", \"per mille\", \"numero\", \"left single quote\", \"right single quote\", \"left double quote\", \"right double quote\", \"left guillemet\", \"right guillemet\", \"en dash\", \"em dash\", \"inverted exclamation\", \"inverted question\", \"middle dot\"]) },\n { id: \"arrows\", label: \"Arrows\", items: s(\"←↑→↓↔↕⇐⇑⇒⇓⇔↩↪➜➤↖↗↘↙↺↻\", [\"left arrow\", \"up arrow\", \"right arrow\", \"down arrow\", \"left right arrow\", \"up down arrow\", \"left double arrow\", \"up double arrow\", \"right double arrow\", \"down double arrow\", \"left right double arrow\", \"return arrow\", \"hook arrow\", \"heavy arrow\", \"arrowhead\", \"north west arrow\", \"north east arrow\", \"south east arrow\", \"south west arrow\", \"anticlockwise arrow\", \"clockwise arrow\"]) },\n { id: \"math\", label: \"Math\", items: s(\"±×÷≈≠≤≥∞√∑∏∫∂∆∇∈∉∪∩⊂⊃∧∨¬∀∃∅∝∠¼½¾⅓⅔²³\", [\"plus minus\", \"times\", \"divide\", \"approximately\", \"not equal\", \"less or equal\", \"greater or equal\", \"infinity\", \"square root\", \"sum\", \"product\", \"integral\", \"partial\", \"increment\", \"nabla\", \"element of\", \"not element\", \"union\", \"intersection\", \"subset\", \"superset\", \"and\", \"or\", \"not\", \"for all\", \"exists\", \"empty set\", \"proportional\", \"angle\", \"one quarter\", \"one half\", \"three quarters\", \"one third\", \"two thirds\", \"squared\", \"cubed\"]) },\n { id: \"currency\", label: \"Currency\", items: s(\"$€£¥¢₹₩₽₺₿฿₫₪₴₦₱\", [\"dollar\", \"euro\", \"pound\", \"yen\", \"cent\", \"rupee\", \"won\", \"ruble\", \"lira\", \"bitcoin\", \"baht\", \"dong\", \"shekel\", \"hryvnia\", \"naira\", \"peso\"]) },\n { id: \"greek\", label: \"Greek\", items: s(\"αβγδεζηθικλμνξοπρστυφχψωΩΣΔΠΦΨΛΘ\", [\"alpha\", \"beta\", \"gamma\", \"delta\", \"epsilon\", \"zeta\", \"eta\", \"theta\", \"iota\", \"kappa\", \"lambda\", \"mu\", \"nu\", \"xi\", \"omicron\", \"pi\", \"rho\", \"sigma\", \"tau\", \"upsilon\", \"phi\", \"chi\", \"psi\", \"omega\", \"capital omega\", \"capital sigma\", \"capital delta\", \"capital pi\", \"capital phi\", \"capital psi\", \"capital lambda\", \"capital theta\"]) },\n { id: \"latin\", label: \"Accented letters\", items: s(\"àáâãäåæçèéêëìíîïñòóôõöøœùúûüýÿßÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÑÒÓÔÕÖØŒÙÚÛÜÝŸ\", []) },\n { id: \"shapes\", label: \"Shapes\", items: s(\"■□▲△▼▽◆◇●○★☆♠♣♥♦♪♫☑☒\", [\"black square\", \"white square\", \"black up triangle\", \"white up triangle\", \"black down triangle\", \"white down triangle\", \"black diamond\", \"white diamond\", \"black circle\", \"white circle\", \"black star\", \"white star\", \"spade\", \"club\", \"heart suit\", \"diamond suit\", \"eighth note\", \"beamed notes\", \"checked box\", \"crossed box\"]) },\n];\n\nexport function searchSymbols(groups: typeof EMOJI_GROUPS, query: string): Glyph[] {\n const q = query.trim().toLowerCase();\n if (!q) return [];\n const seen = new Set<string>();\n const out: Glyph[] = [];\n for (const g of groups) for (const it of g.items) if ((it.char === q || it.name.toLowerCase().includes(q)) && !seen.has(it.char)) (seen.add(it.char), out.push(it));\n return out;\n}\n","/**\n * The BODY TEXT of a Univer document, as agents read and write it (PURE).\n *\n * `matrx-user/documents` declares the body as a value (`document_body_text`)\n * and a write target (`document_body`). Univer owns the document model, so\n * both go through a small port the editor hands up (`DocumentEditor`\n * `onBodyPort`): the read is the body's data stream as plain text, and a write\n * is ONE changed span, applied through Univer's own facade (`deleteRange` /\n * `insertText`, which run `InsertTextCommand` on the command service). That\n * keeps every agent edit on the one editing path a keystroke takes: undo,\n * autosave snapshots, History and collab all see it, and formatting outside\n * the changed span is left exactly as it was.\n *\n * Univer's data stream encodes a paragraph end as `\\r`, a section break as\n * `\\n`, and custom blocks / tables with control characters below U+0020. The\n * plain text maps a paragraph end to `\\n` and leaves the other control tokens\n * out; `offsets` maps each text position back to its data-stream index so a\n * change found in the text lands at the right place in the stream.\n */\n\n\n/** The shape of a surface write handler (structurally the chat runtime's `SurfaceWriteHandlerEntry`; the package imports no chat). */\nexport interface DocumentBodyWriteHandler {\n validate: (value: unknown) => void;\n apply: (value: unknown) => { summary: string; data?: unknown };\n}\n\n/**\n * A product bound, not a database one (snapshots are jsonb): a body an agent\n * sends larger than this is almost certainly a mistake (a whole file pasted\n * twice), and past it the approval card stops being readable.\n */\nexport const DOCUMENT_BODY_MAX_LENGTH = 200_000;\n\n/** What the editor lends the surface. Each call resolves the live document. */\nexport interface DocumentBodyPort {\n /** The main body's Univer data stream (ends with `\\r\\n`). */\n getDataStream: () => string;\n /** The live document snapshot (body with its text runs), for reading the formatting back out. */\n getSnapshot?: () => unknown;\n /** Delete `[start, end)` of the data stream; false when Univer refused. */\n deleteRange: (start: number, end: number) => boolean;\n /** Insert plain text (a `\\n` starts a new paragraph) at a stream index. */\n insertText: (at: number, text: string) => boolean;\n /** Run a Univer command in the open editor (File menu: find and replace, comments). */\n runCommand?: (commandId: string) => Promise<void>;\n /** File → Page setup: open the page setup dialog in the open editor. */\n openPageSetup?: () => void;\n /** File → Language (and the other Docs tool dialogs): open one in the open editor. */\n openTool?: (id: \"language\") => void;\n}\n\nexport interface BodyText {\n text: string;\n /** `offsets[i]` = the stream index of text position `i`; length is text.length + 1. */\n offsets: number[];\n}\n\nconst PARAGRAPH = \"\\r\";\nconst SECTION = \"\\n\";\n\n/** The body's plain text and where each character sits in the stream. */\nexport function bodyTextOf(dataStream: string): BodyText {\n let text = \"\";\n const offsets: number[] = [];\n for (let i = 0; i < dataStream.length; i++) {\n const ch = dataStream[i];\n if (ch === PARAGRAPH) {\n text += \"\\n\";\n offsets.push(i);\n continue;\n }\n if (ch === SECTION) continue;\n if (ch !== \"\\t\" && ch!.charCodeAt(0) < 0x20) continue;\n text += ch;\n offsets.push(i);\n }\n // Every body ends with its last paragraph mark: it is not text a person\n // typed, and it can never be deleted (Univer protects it). Text that ends\n // there inserts before it.\n if (text.endsWith(\"\\n\")) {\n const end = offsets[offsets.length - 1];\n text = text.slice(0, -1);\n offsets.pop();\n offsets.push(end!);\n } else {\n offsets.push(dataStream.length);\n }\n return { text, offsets };\n}\n\nexport interface BodyEdit {\n /** Stream range to remove (may be empty). */\n start: number;\n end: number;\n /** Plain text to insert at `start` (may be empty). */\n insert: string;\n}\n\n/**\n * The ONE span that turns the current body into `next`: the longest common\n * prefix and suffix stay untouched (with their formatting), only the middle\n * changes. Null when nothing differs.\n */\nexport function planBodyEdit(current: BodyText, next: string): BodyEdit | null {\n const a = current.text;\n const b = next;\n if (a === b) return null;\n let prefix = 0;\n const max = Math.min(a.length, b.length);\n while (prefix < max && a[prefix] === b[prefix]) prefix++;\n let suffix = 0;\n while (\n suffix < max - prefix &&\n a[a.length - 1 - suffix] === b[b.length - 1 - suffix]\n )\n suffix++;\n const removeTo = a.length - suffix;\n return {\n start: current.offsets[prefix]!,\n end: current.offsets[removeTo]!,\n insert: b.slice(prefix, b.length - suffix),\n };\n}\n\n/** Apply `next` as the whole body text through the port. */\nexport function writeBodyText(\n port: DocumentBodyPort,\n next: string,\n): { changed: boolean; removed: number; inserted: number } {\n const current = bodyTextOf(port.getDataStream());\n const edit = planBodyEdit(current, next);\n if (!edit) return { changed: false, removed: 0, inserted: 0 };\n if (edit.end > edit.start && !port.deleteRange(edit.start, edit.end)) {\n throw new Error(\n \"The editor refused to remove the old text (the document may be read-only right now). Nothing was changed.\",\n );\n }\n if (edit.insert.length > 0 && !port.insertText(edit.start, edit.insert)) {\n throw new Error(\n edit.end > edit.start\n ? \"The editor removed the old text but refused the new text. Press Undo in the document to bring the old text back, then try again.\"\n : \"The editor refused the new text (the document may be read-only right now). Nothing was changed.\",\n );\n }\n return {\n changed: true,\n removed: edit.end - edit.start,\n inserted: edit.insert.length,\n };\n}\n\nfunction describeValue(value: unknown): string {\n if (value === null) return \"null\";\n if (Array.isArray(value)) return \"an array\";\n if (typeof value === \"object\") return \"an object\";\n return `a ${typeof value}`;\n}\n\n/** Shape check for an agent-sent body; throws a sentence the agent can act on. */\nexport function validateDocumentBody(value: unknown): string {\n if (typeof value !== \"string\") {\n throw new Error(\n `document_body must be the document's whole text as a plain string (or an anchored edit {command, old_str, new_str}) — received ${describeValue(\n value,\n )}. Read document_body_text, change it, and send the full new text.`,\n );\n }\n if (value.length > DOCUMENT_BODY_MAX_LENGTH) {\n throw new Error(\n `document_body must be ${DOCUMENT_BODY_MAX_LENGTH} characters or fewer (received ${value.length}). Nothing was changed.`,\n );\n }\n return value.replace(/\\r\\n?/g, \"\\n\");\n}\n\n/**\n * The `document_body` write handler. `getPort` answers the live editor's port\n * (null until Univer has mounted the document); `assertWritable` throws when\n * the record is not loaded or the person may only view it.\n */\nexport function documentBodyWriteHandler(deps: {\n getPort: () => DocumentBodyPort | null;\n assertWritable: (whatItBlocks: string) => void;\n}): DocumentBodyWriteHandler {\n return {\n validate: (value) => {\n validateDocumentBody(value);\n },\n apply: (value) => {\n const next = validateDocumentBody(value);\n deps.assertWritable(\"its text cannot be changed\");\n const port = deps.getPort();\n if (!port) {\n throw new Error(\n \"The document editor has not finished opening yet. Nothing was changed; try again in a moment.\",\n );\n }\n const result = writeBodyText(port, next);\n if (!result.changed) {\n return { summary: \"The document already has exactly this text; nothing changed.\" };\n }\n return {\n summary: `Changed the document's text in the editor (removed ${result.removed} characters, inserted ${result.inserted}); the editor saves it within a few seconds, and Undo in the document reverses it.`,\n data: { removed: result.removed, inserted: result.inserted },\n };\n },\n };\n}\n"],"mappings":"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","names":["e","describeValue"]}
@@ -253,8 +253,10 @@ var WorkbookCollabSession = class {
253
253
  yArray.push([{ id: info.id, type: info.type, params: safeParams, uid: this.options.uid }]);
254
254
  }
255
255
  applyRemoteMutation(op) {
256
+ const ownUnit = this.options.unitId?.() ?? null;
256
257
  const taggedParams = op.params && typeof op.params === "object" && !Array.isArray(op.params) ? {
257
258
  ...op.params,
259
+ ...ownUnit && typeof op.params.unitId === "string" ? { unitId: ownUnit } : {},
258
260
  [import_collab.REMOTE_PARAM_TAG]: op.uid ?? true
259
261
  } : op.params;
260
262
  try {
@@ -267,6 +269,7 @@ var WorkbookCollabSession = class {
267
269
  `[collab] could not apply remote mutation "${op.id}" \u2014 skipped`,
268
270
  err
269
271
  );
272
+ this.options.onApplyFailed?.(op.id, err);
270
273
  }
271
274
  }
272
275
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/univer/collab/index.ts","../src/univer/collab/WorkbookCollabSession.ts"],"sourcesContent":["/**\n * The collab session shared by `/sheets` and `/docs` (was matrx-frontend `features/workbooks/collab`): the\n * Univer <-> Yjs bridge. It owns the Yjs import, so it is loaded on demand (`import()` when a workbook\n * or document goes live). The snapshot-insert subscription hook is its own entry,\n * `@ai-matrx/office/univer/realtime`, so a static import of the hook never carries Yjs into the editor\n * chunk. A transitional subpath, like `./univer`: it goes internal when the editors move.\n */\nexport * from \"./WorkbookCollabSession\";\n","/**\n * WorkbookCollabSession — the Univer ↔ Yjs bridge.\n *\n * Subscribes to Univer's public `commandService.onMutationExecutedForCollab`\n * hook for local mutations and pushes them onto a `Y.Array<MutationOp>` shared\n * via `SupabaseYjsProvider`. Inbound Yjs updates produce `IMutationInfo` ops\n * that are applied through `commandService.syncExecuteCommand` with the\n * `fromCollab` execution option flagged so the outbound listener short-\n * circuits and we don't echo our own apply.\n *\n * See `FEATURE.md` in this directory for the architecture, the \"do NOT use\n * the Pro preset\" verdict, and the host-election rule. This class is the\n * runtime side of that plan.\n *\n * Lifecycle:\n * - constructor: wire references, no side effects\n * - start(): create Y.Doc + Awareness, instantiate provider, subscribe to\n * command stream, subscribe to Yjs Array observer\n * - stop(): detach all listeners, dispose provider\n *\n * The session is one-shot per workbook open. Reload = stop() + new instance.\n */\n\"use client\";\n\nimport * as Y from \"yjs\";\nimport { Awareness } from \"y-protocols/awareness\";\n\nimport { electCollabHost, remoteOriginOf, REMOTE_PARAM_TAG } from \"@ai-matrx/data/realtime/collab\";\n\n/**\n * Structural transport contract. `SupabaseYjsProvider` satisfies it in\n * production; the verification harness injects an in-memory loopback\n * transport through the same seam. Keeping this structural (not the concrete\n * class) is what makes the session testable without network.\n */\nexport type CollabProviderLike = {\n connect(): Promise<void>;\n disconnect(): void;\n ready(): Promise<void>;\n /**\n * True when this is the only holder of its room in this tab allowed to act as\n * host (autosave). Same-tab holders share one client id and one uid, so\n * awareness cannot tell them apart. Absent = a lone holder = leader.\n */\n isTabLeader?(): boolean;\n};\n\n// Univer types are loose at the public hook boundary; we narrow with a\n// structural interface so we don't have to import the full Univer surface.\ntype MutationParams = Record<string, unknown> | unknown[] | null;\n\nexport type CollabMutationInfo = {\n id: string;\n type: number; // CommandType.MUTATION = 2 in Univer's enum\n params: MutationParams;\n /** Who made it (the person's uid), so a peer can tell its own other tab's edit from a stranger's. */\n uid?: string;\n};\n\ntype ExecuteOptions = {\n /** Per-Univer convention: when true, the command runs locally only and is\n * NOT broadcast to other plugins. We set this on inbound-apply so the\n * subsequent onMutationExecutedForCollab does NOT re-fire on our peer. */\n onlyLocal?: boolean;\n fromCollab?: boolean;\n};\n\nexport type CommandServiceLike = {\n onMutationExecutedForCollab(\n listener: (info: CollabMutationInfo, options?: ExecuteOptions) => void,\n ): {\n dispose: () => void;\n };\n syncExecuteCommand(\n id: string,\n params: MutationParams,\n options?: ExecuteOptions,\n ): unknown;\n};\n\nexport type WorkbookCollabSessionOptions = {\n workbookId: string;\n uid: string;\n clientId: string;\n commandService: CommandServiceLike;\n /** Factory so this file does not statically depend on the provider. */\n makeProvider: (args: {\n workbookId: string;\n clientId: string;\n doc: Y.Doc;\n awareness: Awareness;\n /** Wire to the provider so a host handover re-runs the election. */\n onLeaderChange: () => void;\n }) => CollabProviderLike;\n /** Called when the awareness set changes (peers join / leave / move cursor). */\n onAwarenessChange?: (awareness: Awareness) => void;\n /**\n * A peer replaced the whole content (history restore, Google update): the\n * newest saved snapshot is the content now. Reload it in place; never\n * re-announce. See `announceReplaced`.\n */\n onReplaced?: (snapshotId: string) => void;\n /**\n * TRUE when the room is bound to a Univer DOCUMENT unit (the Docs editor): there the `doc.mutation.*`\n * stream IS the content, so it must travel. Leave unset for a workbook, where `doc.mutation.*` is the\n * in-cell editor's scratch document and never travels (see `isCellEditorScratch`).\n */\n forwardDocumentMutations?: boolean;\n};\n\n/** What `announceReplaced` writes into the room's control map. */\ntype ReplacedSignal = { snapshotId: string; by: string; nonce: string };\n\nconst MUTATIONS_ARRAY_KEY = \"mutations\";\nconst CONTROL_MAP_KEY = \"control\";\nconst REPLACED_KEY = \"replaced\";\n\nexport class WorkbookCollabSession {\n private readonly options: WorkbookCollabSessionOptions;\n private doc: Y.Doc | null = null;\n private awareness: Awareness | null = null;\n private provider: CollabProviderLike | null = null;\n private commandDisposer: { dispose: () => void } | null = null;\n private yArrayObserver:\n ((event: Y.YArrayEvent<CollabMutationInfo>) => void) | null = null;\n private awarenessObserver: (() => void) | null = null;\n private controlObserver: ((event: Y.YMapEvent<unknown>) => void) | null = null;\n /** >0 while this client swaps its own content: its mutations are not edits. */\n private muted = 0;\n private started = false;\n private disposed = false;\n\n constructor(options: WorkbookCollabSessionOptions) {\n this.options = options;\n }\n\n async start(): Promise<void> {\n if (this.started || this.disposed) return;\n this.started = true;\n\n this.doc = new Y.Doc();\n this.awareness = new Awareness(this.doc);\n\n // Seed local awareness with the user identity. Cursor (sheetId/row/col)\n // is set later via setCursor().\n this.awareness.setLocalState({\n uid: this.options.uid,\n clientId: this.options.clientId,\n name: this.options.uid.slice(0, 8),\n color: pickColor(this.options.uid),\n sheetId: null,\n row: null,\n col: null,\n ts: Date.now(),\n });\n\n this.provider = this.options.makeProvider({\n workbookId: this.options.workbookId,\n clientId: this.options.clientId,\n doc: this.doc,\n awareness: this.awareness,\n onLeaderChange: () => {\n if (this.awareness) this.options.onAwarenessChange?.(this.awareness);\n },\n });\n\n // Subscribe to inbound Yjs array changes BEFORE connecting the provider,\n // so the initial-state replay (if any) hits our observer.\n const yArray = this.doc.getArray<CollabMutationInfo>(MUTATIONS_ARRAY_KEY);\n this.yArrayObserver = (event) => {\n // Local transactions are OUR OWN pushes from handleLocalMutation —\n // Univer already executed those mutations. Re-applying them here would\n // double-apply every local edit on the originator. Only act on\n // transactions that arrived via Y.applyUpdate (origin 'remote').\n if (event.transaction.local) return;\n for (const change of event.changes.delta) {\n if (!change.insert || !Array.isArray(change.insert)) continue;\n for (const op of change.insert as CollabMutationInfo[]) {\n this.applyRemoteMutation(op);\n }\n }\n };\n yArray.observe(this.yArrayObserver as Parameters<typeof yArray.observe>[0]);\n\n // Awareness change passthrough.\n if (this.options.onAwarenessChange) {\n const cb = this.options.onAwarenessChange;\n this.awarenessObserver = () => {\n if (this.awareness) cb(this.awareness);\n };\n this.awareness.on(\"change\", this.awarenessObserver);\n }\n\n // Subscribe to Univer's collab-grain mutation stream.\n this.commandDisposer =\n this.options.commandService.onMutationExecutedForCollab((info) =>\n this.handleLocalMutation(info),\n );\n\n await this.provider.connect();\n await this.provider.ready();\n\n // Whole-content replacement from a peer. Bound AFTER the initial sync: a\n // joiner already loaded the newest snapshot, so the signal it finds in\n // the room's state is history, not news (the editor also skips a\n // snapshot it already shows).\n if (this.disposed || !this.doc) return;\n const control = this.doc.getMap<unknown>(CONTROL_MAP_KEY);\n const seen = (control.get(REPLACED_KEY) as ReplacedSignal | undefined)?.nonce ?? null;\n this.controlObserver = (event) => {\n if (event.transaction.local || !event.keysChanged.has(REPLACED_KEY)) return;\n const signal = control.get(REPLACED_KEY) as ReplacedSignal | undefined;\n if (!signal?.snapshotId || signal.nonce === seen) return;\n this.options.onReplaced?.(signal.snapshotId);\n };\n control.observe(this.controlObserver);\n }\n\n stop(): void {\n if (this.disposed) return;\n this.disposed = true;\n\n this.commandDisposer?.dispose();\n this.commandDisposer = null;\n\n if (this.doc && this.yArrayObserver) {\n const yArray = this.doc.getArray<CollabMutationInfo>(MUTATIONS_ARRAY_KEY);\n yArray.unobserve(\n this.yArrayObserver as Parameters<typeof yArray.unobserve>[0],\n );\n this.yArrayObserver = null;\n }\n\n if (this.doc && this.controlObserver) {\n this.doc.getMap<unknown>(CONTROL_MAP_KEY).unobserve(this.controlObserver);\n this.controlObserver = null;\n }\n\n if (this.awareness && this.awarenessObserver) {\n this.awareness.off(\"change\", this.awarenessObserver);\n this.awarenessObserver = null;\n }\n\n this.provider?.disconnect();\n this.provider = null;\n\n if (this.awareness) {\n this.awareness.destroy();\n this.awareness = null;\n }\n\n if (this.doc) {\n this.doc.destroy();\n this.doc = null;\n }\n }\n\n /**\n * How many mutations the shared log holds right now. Read it BEFORE\n * `workbook.save()`: that many entries are guaranteed to be inside the\n * snapshot that save produces.\n */\n logLength(): number {\n return this.doc?.getArray<CollabMutationInfo>(MUTATIONS_ARRAY_KEY).length ?? 0;\n }\n\n /**\n * Drop the first `upTo` mutations from the shared log once a snapshot that\n * contains them has been SAVED. Without this a joiner loads the saved\n * workbook (which already has every mutation baked in) and then catches up\n * the whole log on top of it, replaying edits into a workbook that already\n * holds them — Univer refuses with \"Trying to overwrite value at key\"\n * (feedback 88184e53). Only the host calls this; peers receive it as a\n * delete, which the inbound observer ignores (it acts on inserts only).\n */\n compactLog(upTo: number): void {\n if (!this.doc || upTo <= 0) return;\n const yArray = this.doc.getArray<CollabMutationInfo>(MUTATIONS_ARRAY_KEY);\n const n = Math.min(upTo, yArray.length);\n if (n > 0) this.doc.transact(() => yArray.delete(0, n));\n }\n\n /**\n * This client replaced the whole content and SAVED it as `snapshotId`.\n * One transaction empties the shared mutation log (every entry targets the\n * content that is gone — a joiner must never replay them onto the new\n * snapshot) and tells every peer to reload the newest snapshot.\n */\n announceReplaced(snapshotId: string): void {\n if (!this.doc) return;\n const yArray = this.doc.getArray<CollabMutationInfo>(MUTATIONS_ARRAY_KEY);\n const control = this.doc.getMap<unknown>(CONTROL_MAP_KEY);\n const signal: ReplacedSignal = {\n snapshotId,\n by: this.options.clientId,\n nonce: `${this.options.clientId}:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`,\n };\n this.doc.transact(() => {\n if (yArray.length > 0) yArray.delete(0, yArray.length);\n control.set(REPLACED_KEY, signal);\n });\n }\n\n /**\n * Run `swap` (this client tearing its content down and mounting the new\n * one) without broadcasting what Univer emits while it does: those are not\n * edits, and peers reload the same snapshot themselves.\n */\n muteWhile<T>(swap: () => T): T {\n this.muted += 1;\n try {\n return swap();\n } finally {\n this.muted -= 1;\n }\n }\n\n /**\n * Update the local awareness cursor. Call this from Univer's selection\n * listener (`worksheet.onSelectionChange(...)`). Throttle at the call site\n * (~50ms is plenty); the provider broadcasts on its own debounced cycle.\n */\n setCursor(input: {\n sheetId: string | null;\n row: number | null;\n col: number | null;\n }): void {\n if (!this.awareness) return;\n const current = this.awareness.getLocalState() ?? {};\n this.awareness.setLocalState({\n ...current,\n sheetId: input.sheetId,\n row: input.row,\n col: input.col,\n ts: Date.now(),\n });\n }\n\n /**\n * Returns the deterministic host: the connected peer with the lowest uid\n * (lexicographic). Host is the only client that writes canonical snapshots.\n * No election protocol; recompute whenever awareness changes.\n */\n electHost(opts: { tabTieBreak?: boolean } = {}): { isHost: boolean; hostUid: string | null } {\n if (!this.awareness) return { isHost: false, hostUid: null };\n const states = Array.from(this.awareness.getStates().values()) as Array<\n Record<string, unknown>\n >;\n // Lowest uid wins ACROSS tabs; inside this tab only the room's tab leader\n // may be host, because every same-tab holder carries the same uid.\n const leader = this.provider?.isTabLeader?.() ?? true;\n return electCollabHost(states, this.options.uid, this.options.clientId, leader, opts.tabTieBreak ?? false);\n }\n\n // ─── internals ────────────────────────────────────────────────────────\n\n private handleLocalMutation(info: CollabMutationInfo): void {\n if (this.muted > 0) return;\n if (!this.options.forwardDocumentMutations && isCellEditorScratch(info)) return;\n // Inbound-applied mutations re-fire this listener because Univer's\n // syncExecuteCommand legitimately processes them through the full\n // pipeline. Skip by sniffing the sentinel we stamped on apply.\n if (\n info.params &&\n typeof info.params === \"object\" &&\n !Array.isArray(info.params) &&\n remoteOriginOf(info.params) !== null\n ) {\n return;\n }\n if (!this.doc) return;\n\n // Normalize params through a JSON round-trip BEFORE pushing into Yjs.\n // Yjs (and the Broadcast transport) only carry JSON-encodable values;\n // Univer types its mutation params as \"serializable\" but JS objects can\n // smuggle Date / Map / class instances through that promise. The\n // round-trip makes the failure mode deterministic: a non-encodable\n // mutation is skipped with a warning instead of corrupting the shared\n // doc or desyncing peers mid-session. What we push here is byte-for-byte\n // what remote peers will apply, so local-apply == remote-apply.\n let safeParams: MutationParams;\n try {\n safeParams =\n info.params === undefined || info.params === null\n ? null\n : (JSON.parse(JSON.stringify(info.params)) as MutationParams);\n } catch (err) {\n console.warn(\n `[collab] skipping non-serializable mutation \"${info.id}\" — peers will not receive it`,\n err,\n );\n return;\n }\n\n const yArray = this.doc.getArray<CollabMutationInfo>(MUTATIONS_ARRAY_KEY);\n yArray.push([{ id: info.id, type: info.type, params: safeParams, uid: this.options.uid }]);\n }\n\n private applyRemoteMutation(op: CollabMutationInfo): void {\n // Wrap params with the sentinel so handleLocalMutation knows not to\n // echo the apply back into Yjs.\n const taggedParams: MutationParams =\n op.params && typeof op.params === \"object\" && !Array.isArray(op.params)\n ? {\n ...(op.params as Record<string, unknown>),\n [REMOTE_PARAM_TAG]: op.uid ?? true,\n }\n : op.params;\n\n try {\n this.options.commandService.syncExecuteCommand(op.id, taggedParams, {\n onlyLocal: true,\n fromCollab: true,\n });\n } catch (err) {\n // One unappliable op (already baked into the loaded snapshot, or a\n // stale target) must not throw into the realtime channel handler and\n // abort the rest of the catch-up. Said, never swallowed.\n console.warn(\n `[collab] could not apply remote mutation \"${op.id}\" — skipped`,\n err,\n );\n }\n }\n}\n\n// ─── helpers ────────────────────────────────────────────────────────────\n\n/**\n * A workbook holds no Univer DOCUMENT units of its own: the `doc.mutation.*` stream a sheet emits is\n * the in-cell editor's scratch document (typing into a cell). The finished text reaches the sheet as\n * `sheet.mutation.set-range-values`, which does travel. Sending the scratch edits made a joiner replay\n * them against an editor unit it does not have (\"Trying to overwrite value at key\").\n */\nexport function isCellEditorScratch(info: Pick<CollabMutationInfo, \"id\">): boolean {\n return typeof info.id === \"string\" && info.id.startsWith(\"doc.mutation.\");\n}\n\nconst CURSOR_PALETTE = [\n \"#3b82f6\", // blue\n \"#10b981\", // emerald\n \"#f59e0b\", // amber\n \"#ef4444\", // red\n \"#8b5cf6\", // violet\n \"#06b6d4\", // cyan\n \"#ec4899\", // pink\n \"#84cc16\", // lime\n];\n\nfunction pickColor(uid: string): string {\n // Deterministic FNV-1a-ish hash so the same user is always the same color\n // across sessions and across other users' screens.\n let h = 2166136261;\n for (let i = 0; i < uid.length; i++) {\n h ^= uid.charCodeAt(i);\n h = Math.imul(h, 16777619);\n }\n return CURSOR_PALETTE[Math.abs(h) % CURSOR_PALETTE.length] ?? \"#ec4899\";\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/univer/collab/index.ts","../src/univer/collab/WorkbookCollabSession.ts"],"sourcesContent":["/**\n * The collab session shared by `/sheets` and `/docs` (was matrx-frontend `features/workbooks/collab`): the\n * Univer <-> Yjs bridge. It owns the Yjs import, so it is loaded on demand (`import()` when a workbook\n * or document goes live). The snapshot-insert subscription hook is its own entry,\n * `@ai-matrx/office/univer/realtime`, so a static import of the hook never carries Yjs into the editor\n * chunk. A transitional subpath, like `./univer`: it goes internal when the editors move.\n */\nexport * from \"./WorkbookCollabSession\";\n","/**\n * WorkbookCollabSession — the Univer ↔ Yjs bridge.\n *\n * Subscribes to Univer's public `commandService.onMutationExecutedForCollab`\n * hook for local mutations and pushes them onto a `Y.Array<MutationOp>` shared\n * via `SupabaseYjsProvider`. Inbound Yjs updates produce `IMutationInfo` ops\n * that are applied through `commandService.syncExecuteCommand` with the\n * `fromCollab` execution option flagged so the outbound listener short-\n * circuits and we don't echo our own apply.\n *\n * See `FEATURE.md` in this directory for the architecture, the \"do NOT use\n * the Pro preset\" verdict, and the host-election rule. This class is the\n * runtime side of that plan.\n *\n * Lifecycle:\n * - constructor: wire references, no side effects\n * - start(): create Y.Doc + Awareness, instantiate provider, subscribe to\n * command stream, subscribe to Yjs Array observer\n * - stop(): detach all listeners, dispose provider\n *\n * The session is one-shot per workbook open. Reload = stop() + new instance.\n */\n\"use client\";\n\nimport * as Y from \"yjs\";\nimport { Awareness } from \"y-protocols/awareness\";\n\nimport { electCollabHost, remoteOriginOf, REMOTE_PARAM_TAG } from \"@ai-matrx/data/realtime/collab\";\n\n/**\n * Structural transport contract. `SupabaseYjsProvider` satisfies it in\n * production; the verification harness injects an in-memory loopback\n * transport through the same seam. Keeping this structural (not the concrete\n * class) is what makes the session testable without network.\n */\nexport type CollabProviderLike = {\n connect(): Promise<void>;\n disconnect(): void;\n ready(): Promise<void>;\n /**\n * True when this is the only holder of its room in this tab allowed to act as\n * host (autosave). Same-tab holders share one client id and one uid, so\n * awareness cannot tell them apart. Absent = a lone holder = leader.\n */\n isTabLeader?(): boolean;\n};\n\n// Univer types are loose at the public hook boundary; we narrow with a\n// structural interface so we don't have to import the full Univer surface.\ntype MutationParams = Record<string, unknown> | unknown[] | null;\n\nexport type CollabMutationInfo = {\n id: string;\n type: number; // CommandType.MUTATION = 2 in Univer's enum\n params: MutationParams;\n /** Who made it (the person's uid), so a peer can tell its own other tab's edit from a stranger's. */\n uid?: string;\n};\n\ntype ExecuteOptions = {\n /** Per-Univer convention: when true, the command runs locally only and is\n * NOT broadcast to other plugins. We set this on inbound-apply so the\n * subsequent onMutationExecutedForCollab does NOT re-fire on our peer. */\n onlyLocal?: boolean;\n fromCollab?: boolean;\n};\n\nexport type CommandServiceLike = {\n onMutationExecutedForCollab(\n listener: (info: CollabMutationInfo, options?: ExecuteOptions) => void,\n ): {\n dispose: () => void;\n };\n syncExecuteCommand(\n id: string,\n params: MutationParams,\n options?: ExecuteOptions,\n ): unknown;\n};\n\nexport type WorkbookCollabSessionOptions = {\n workbookId: string;\n uid: string;\n clientId: string;\n commandService: CommandServiceLike;\n /** Factory so this file does not statically depend on the provider. */\n makeProvider: (args: {\n workbookId: string;\n clientId: string;\n doc: Y.Doc;\n awareness: Awareness;\n /** Wire to the provider so a host handover re-runs the election. */\n onLeaderChange: () => void;\n }) => CollabProviderLike;\n /** Called when the awareness set changes (peers join / leave / move cursor). */\n onAwarenessChange?: (awareness: Awareness) => void;\n /**\n * A peer replaced the whole content (history restore, Google update): the\n * newest saved snapshot is the content now. Reload it in place; never\n * re-announce. See `announceReplaced`.\n */\n onReplaced?: (snapshotId: string) => void;\n /**\n * TRUE when the room is bound to a Univer DOCUMENT unit (the Docs editor): there the `doc.mutation.*`\n * stream IS the content, so it must travel. Leave unset for a workbook, where `doc.mutation.*` is the\n * in-cell editor's scratch document and never travels (see `isCellEditorScratch`).\n */\n forwardDocumentMutations?: boolean;\n /**\n * The Univer unit id this seat mounted. Each seat can mount its own unit, so a peer's mutation\n * (addressed to the SENDER's unit) is re-addressed to this one before it is applied.\n */\n unitId?: () => string | null;\n /** A peer's mutation could not be applied here: the seat says so and re-syncs; never only a console line. */\n onApplyFailed?: (mutationId: string, error: unknown) => void;\n};\n\n/** What `announceReplaced` writes into the room's control map. */\ntype ReplacedSignal = { snapshotId: string; by: string; nonce: string };\n\nconst MUTATIONS_ARRAY_KEY = \"mutations\";\nconst CONTROL_MAP_KEY = \"control\";\nconst REPLACED_KEY = \"replaced\";\n\nexport class WorkbookCollabSession {\n private readonly options: WorkbookCollabSessionOptions;\n private doc: Y.Doc | null = null;\n private awareness: Awareness | null = null;\n private provider: CollabProviderLike | null = null;\n private commandDisposer: { dispose: () => void } | null = null;\n private yArrayObserver:\n ((event: Y.YArrayEvent<CollabMutationInfo>) => void) | null = null;\n private awarenessObserver: (() => void) | null = null;\n private controlObserver: ((event: Y.YMapEvent<unknown>) => void) | null = null;\n /** >0 while this client swaps its own content: its mutations are not edits. */\n private muted = 0;\n private started = false;\n private disposed = false;\n\n constructor(options: WorkbookCollabSessionOptions) {\n this.options = options;\n }\n\n async start(): Promise<void> {\n if (this.started || this.disposed) return;\n this.started = true;\n\n this.doc = new Y.Doc();\n this.awareness = new Awareness(this.doc);\n\n // Seed local awareness with the user identity. Cursor (sheetId/row/col)\n // is set later via setCursor().\n this.awareness.setLocalState({\n uid: this.options.uid,\n clientId: this.options.clientId,\n name: this.options.uid.slice(0, 8),\n color: pickColor(this.options.uid),\n sheetId: null,\n row: null,\n col: null,\n ts: Date.now(),\n });\n\n this.provider = this.options.makeProvider({\n workbookId: this.options.workbookId,\n clientId: this.options.clientId,\n doc: this.doc,\n awareness: this.awareness,\n onLeaderChange: () => {\n if (this.awareness) this.options.onAwarenessChange?.(this.awareness);\n },\n });\n\n // Subscribe to inbound Yjs array changes BEFORE connecting the provider,\n // so the initial-state replay (if any) hits our observer.\n const yArray = this.doc.getArray<CollabMutationInfo>(MUTATIONS_ARRAY_KEY);\n this.yArrayObserver = (event) => {\n // Local transactions are OUR OWN pushes from handleLocalMutation —\n // Univer already executed those mutations. Re-applying them here would\n // double-apply every local edit on the originator. Only act on\n // transactions that arrived via Y.applyUpdate (origin 'remote').\n if (event.transaction.local) return;\n for (const change of event.changes.delta) {\n if (!change.insert || !Array.isArray(change.insert)) continue;\n for (const op of change.insert as CollabMutationInfo[]) {\n this.applyRemoteMutation(op);\n }\n }\n };\n yArray.observe(this.yArrayObserver as Parameters<typeof yArray.observe>[0]);\n\n // Awareness change passthrough.\n if (this.options.onAwarenessChange) {\n const cb = this.options.onAwarenessChange;\n this.awarenessObserver = () => {\n if (this.awareness) cb(this.awareness);\n };\n this.awareness.on(\"change\", this.awarenessObserver);\n }\n\n // Subscribe to Univer's collab-grain mutation stream.\n this.commandDisposer =\n this.options.commandService.onMutationExecutedForCollab((info) =>\n this.handleLocalMutation(info),\n );\n\n await this.provider.connect();\n await this.provider.ready();\n\n // Whole-content replacement from a peer. Bound AFTER the initial sync: a\n // joiner already loaded the newest snapshot, so the signal it finds in\n // the room's state is history, not news (the editor also skips a\n // snapshot it already shows).\n if (this.disposed || !this.doc) return;\n const control = this.doc.getMap<unknown>(CONTROL_MAP_KEY);\n const seen = (control.get(REPLACED_KEY) as ReplacedSignal | undefined)?.nonce ?? null;\n this.controlObserver = (event) => {\n if (event.transaction.local || !event.keysChanged.has(REPLACED_KEY)) return;\n const signal = control.get(REPLACED_KEY) as ReplacedSignal | undefined;\n if (!signal?.snapshotId || signal.nonce === seen) return;\n this.options.onReplaced?.(signal.snapshotId);\n };\n control.observe(this.controlObserver);\n }\n\n stop(): void {\n if (this.disposed) return;\n this.disposed = true;\n\n this.commandDisposer?.dispose();\n this.commandDisposer = null;\n\n if (this.doc && this.yArrayObserver) {\n const yArray = this.doc.getArray<CollabMutationInfo>(MUTATIONS_ARRAY_KEY);\n yArray.unobserve(\n this.yArrayObserver as Parameters<typeof yArray.unobserve>[0],\n );\n this.yArrayObserver = null;\n }\n\n if (this.doc && this.controlObserver) {\n this.doc.getMap<unknown>(CONTROL_MAP_KEY).unobserve(this.controlObserver);\n this.controlObserver = null;\n }\n\n if (this.awareness && this.awarenessObserver) {\n this.awareness.off(\"change\", this.awarenessObserver);\n this.awarenessObserver = null;\n }\n\n this.provider?.disconnect();\n this.provider = null;\n\n if (this.awareness) {\n this.awareness.destroy();\n this.awareness = null;\n }\n\n if (this.doc) {\n this.doc.destroy();\n this.doc = null;\n }\n }\n\n /**\n * How many mutations the shared log holds right now. Read it BEFORE\n * `workbook.save()`: that many entries are guaranteed to be inside the\n * snapshot that save produces.\n */\n logLength(): number {\n return this.doc?.getArray<CollabMutationInfo>(MUTATIONS_ARRAY_KEY).length ?? 0;\n }\n\n /**\n * Drop the first `upTo` mutations from the shared log once a snapshot that\n * contains them has been SAVED. Without this a joiner loads the saved\n * workbook (which already has every mutation baked in) and then catches up\n * the whole log on top of it, replaying edits into a workbook that already\n * holds them — Univer refuses with \"Trying to overwrite value at key\"\n * (feedback 88184e53). Only the host calls this; peers receive it as a\n * delete, which the inbound observer ignores (it acts on inserts only).\n */\n compactLog(upTo: number): void {\n if (!this.doc || upTo <= 0) return;\n const yArray = this.doc.getArray<CollabMutationInfo>(MUTATIONS_ARRAY_KEY);\n const n = Math.min(upTo, yArray.length);\n if (n > 0) this.doc.transact(() => yArray.delete(0, n));\n }\n\n /**\n * This client replaced the whole content and SAVED it as `snapshotId`.\n * One transaction empties the shared mutation log (every entry targets the\n * content that is gone — a joiner must never replay them onto the new\n * snapshot) and tells every peer to reload the newest snapshot.\n */\n announceReplaced(snapshotId: string): void {\n if (!this.doc) return;\n const yArray = this.doc.getArray<CollabMutationInfo>(MUTATIONS_ARRAY_KEY);\n const control = this.doc.getMap<unknown>(CONTROL_MAP_KEY);\n const signal: ReplacedSignal = {\n snapshotId,\n by: this.options.clientId,\n nonce: `${this.options.clientId}:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`,\n };\n this.doc.transact(() => {\n if (yArray.length > 0) yArray.delete(0, yArray.length);\n control.set(REPLACED_KEY, signal);\n });\n }\n\n /**\n * Run `swap` (this client tearing its content down and mounting the new\n * one) without broadcasting what Univer emits while it does: those are not\n * edits, and peers reload the same snapshot themselves.\n */\n muteWhile<T>(swap: () => T): T {\n this.muted += 1;\n try {\n return swap();\n } finally {\n this.muted -= 1;\n }\n }\n\n /**\n * Update the local awareness cursor. Call this from Univer's selection\n * listener (`worksheet.onSelectionChange(...)`). Throttle at the call site\n * (~50ms is plenty); the provider broadcasts on its own debounced cycle.\n */\n setCursor(input: {\n sheetId: string | null;\n row: number | null;\n col: number | null;\n }): void {\n if (!this.awareness) return;\n const current = this.awareness.getLocalState() ?? {};\n this.awareness.setLocalState({\n ...current,\n sheetId: input.sheetId,\n row: input.row,\n col: input.col,\n ts: Date.now(),\n });\n }\n\n /**\n * Returns the deterministic host: the connected peer with the lowest uid\n * (lexicographic). Host is the only client that writes canonical snapshots.\n * No election protocol; recompute whenever awareness changes.\n */\n electHost(opts: { tabTieBreak?: boolean } = {}): { isHost: boolean; hostUid: string | null } {\n if (!this.awareness) return { isHost: false, hostUid: null };\n const states = Array.from(this.awareness.getStates().values()) as Array<\n Record<string, unknown>\n >;\n // Lowest uid wins ACROSS tabs; inside this tab only the room's tab leader\n // may be host, because every same-tab holder carries the same uid.\n const leader = this.provider?.isTabLeader?.() ?? true;\n return electCollabHost(states, this.options.uid, this.options.clientId, leader, opts.tabTieBreak ?? false);\n }\n\n // ─── internals ────────────────────────────────────────────────────────\n\n private handleLocalMutation(info: CollabMutationInfo): void {\n if (this.muted > 0) return;\n if (!this.options.forwardDocumentMutations && isCellEditorScratch(info)) return;\n // Inbound-applied mutations re-fire this listener because Univer's\n // syncExecuteCommand legitimately processes them through the full\n // pipeline. Skip by sniffing the sentinel we stamped on apply.\n if (\n info.params &&\n typeof info.params === \"object\" &&\n !Array.isArray(info.params) &&\n remoteOriginOf(info.params) !== null\n ) {\n return;\n }\n if (!this.doc) return;\n\n // Normalize params through a JSON round-trip BEFORE pushing into Yjs.\n // Yjs (and the Broadcast transport) only carry JSON-encodable values;\n // Univer types its mutation params as \"serializable\" but JS objects can\n // smuggle Date / Map / class instances through that promise. The\n // round-trip makes the failure mode deterministic: a non-encodable\n // mutation is skipped with a warning instead of corrupting the shared\n // doc or desyncing peers mid-session. What we push here is byte-for-byte\n // what remote peers will apply, so local-apply == remote-apply.\n let safeParams: MutationParams;\n try {\n safeParams =\n info.params === undefined || info.params === null\n ? null\n : (JSON.parse(JSON.stringify(info.params)) as MutationParams);\n } catch (err) {\n console.warn(\n `[collab] skipping non-serializable mutation \"${info.id}\" — peers will not receive it`,\n err,\n );\n return;\n }\n\n const yArray = this.doc.getArray<CollabMutationInfo>(MUTATIONS_ARRAY_KEY);\n yArray.push([{ id: info.id, type: info.type, params: safeParams, uid: this.options.uid }]);\n }\n\n private applyRemoteMutation(op: CollabMutationInfo): void {\n // Wrap params with the sentinel so handleLocalMutation knows not to\n // echo the apply back into Yjs.\n const ownUnit = this.options.unitId?.() ?? null;\n const taggedParams: MutationParams =\n op.params && typeof op.params === \"object\" && !Array.isArray(op.params)\n ? {\n ...(op.params as Record<string, unknown>),\n ...(ownUnit && typeof (op.params as { unitId?: unknown }).unitId === \"string\" ? { unitId: ownUnit } : {}),\n [REMOTE_PARAM_TAG]: op.uid ?? true,\n }\n : op.params;\n\n try {\n this.options.commandService.syncExecuteCommand(op.id, taggedParams, {\n onlyLocal: true,\n fromCollab: true,\n });\n } catch (err) {\n // One unappliable op (already baked into the loaded snapshot, or a\n // stale target) must not throw into the realtime channel handler and\n // abort the rest of the catch-up. Said, never swallowed.\n console.warn(\n `[collab] could not apply remote mutation \"${op.id}\" — skipped`,\n err,\n );\n this.options.onApplyFailed?.(op.id, err);\n }\n }\n}\n\n// ─── helpers ────────────────────────────────────────────────────────────\n\n/**\n * A workbook holds no Univer DOCUMENT units of its own: the `doc.mutation.*` stream a sheet emits is\n * the in-cell editor's scratch document (typing into a cell). The finished text reaches the sheet as\n * `sheet.mutation.set-range-values`, which does travel. Sending the scratch edits made a joiner replay\n * them against an editor unit it does not have (\"Trying to overwrite value at key\").\n */\nexport function isCellEditorScratch(info: Pick<CollabMutationInfo, \"id\">): boolean {\n return typeof info.id === \"string\" && info.id.startsWith(\"doc.mutation.\");\n}\n\nconst CURSOR_PALETTE = [\n \"#3b82f6\", // blue\n \"#10b981\", // emerald\n \"#f59e0b\", // amber\n \"#ef4444\", // red\n \"#8b5cf6\", // violet\n \"#06b6d4\", // cyan\n \"#ec4899\", // pink\n \"#84cc16\", // lime\n];\n\nfunction pickColor(uid: string): string {\n // Deterministic FNV-1a-ish hash so the same user is always the same color\n // across sessions and across other users' screens.\n let h = 2166136261;\n for (let i = 0; i < uid.length; i++) {\n h ^= uid.charCodeAt(i);\n h = Math.imul(h, 16777619);\n }\n return CURSOR_PALETTE[Math.abs(h) % CURSOR_PALETTE.length] ?? \"#ec4899\";\n}\n"],"mappings":"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","names":[]}
@@ -67,6 +67,13 @@ type WorkbookCollabSessionOptions = {
67
67
  * in-cell editor's scratch document and never travels (see `isCellEditorScratch`).
68
68
  */
69
69
  forwardDocumentMutations?: boolean;
70
+ /**
71
+ * The Univer unit id this seat mounted. Each seat can mount its own unit, so a peer's mutation
72
+ * (addressed to the SENDER's unit) is re-addressed to this one before it is applied.
73
+ */
74
+ unitId?: () => string | null;
75
+ /** A peer's mutation could not be applied here: the seat says so and re-syncs; never only a console line. */
76
+ onApplyFailed?: (mutationId: string, error: unknown) => void;
70
77
  };
71
78
  declare class WorkbookCollabSession {
72
79
  private readonly options;
@@ -67,6 +67,13 @@ type WorkbookCollabSessionOptions = {
67
67
  * in-cell editor's scratch document and never travels (see `isCellEditorScratch`).
68
68
  */
69
69
  forwardDocumentMutations?: boolean;
70
+ /**
71
+ * The Univer unit id this seat mounted. Each seat can mount its own unit, so a peer's mutation
72
+ * (addressed to the SENDER's unit) is re-addressed to this one before it is applied.
73
+ */
74
+ unitId?: () => string | null;
75
+ /** A peer's mutation could not be applied here: the seat says so and re-syncs; never only a console line. */
76
+ onApplyFailed?: (mutationId: string, error: unknown) => void;
70
77
  };
71
78
  declare class WorkbookCollabSession {
72
79
  private readonly options;
@@ -217,8 +217,10 @@ var WorkbookCollabSession = class {
217
217
  yArray.push([{ id: info.id, type: info.type, params: safeParams, uid: this.options.uid }]);
218
218
  }
219
219
  applyRemoteMutation(op) {
220
+ const ownUnit = this.options.unitId?.() ?? null;
220
221
  const taggedParams = op.params && typeof op.params === "object" && !Array.isArray(op.params) ? {
221
222
  ...op.params,
223
+ ...ownUnit && typeof op.params.unitId === "string" ? { unitId: ownUnit } : {},
222
224
  [REMOTE_PARAM_TAG]: op.uid ?? true
223
225
  } : op.params;
224
226
  try {
@@ -231,6 +233,7 @@ var WorkbookCollabSession = class {
231
233
  `[collab] could not apply remote mutation "${op.id}" \u2014 skipped`,
232
234
  err
233
235
  );
236
+ this.options.onApplyFailed?.(op.id, err);
234
237
  }
235
238
  }
236
239
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/univer/collab/WorkbookCollabSession.ts"],"sourcesContent":["/**\n * WorkbookCollabSession — the Univer ↔ Yjs bridge.\n *\n * Subscribes to Univer's public `commandService.onMutationExecutedForCollab`\n * hook for local mutations and pushes them onto a `Y.Array<MutationOp>` shared\n * via `SupabaseYjsProvider`. Inbound Yjs updates produce `IMutationInfo` ops\n * that are applied through `commandService.syncExecuteCommand` with the\n * `fromCollab` execution option flagged so the outbound listener short-\n * circuits and we don't echo our own apply.\n *\n * See `FEATURE.md` in this directory for the architecture, the \"do NOT use\n * the Pro preset\" verdict, and the host-election rule. This class is the\n * runtime side of that plan.\n *\n * Lifecycle:\n * - constructor: wire references, no side effects\n * - start(): create Y.Doc + Awareness, instantiate provider, subscribe to\n * command stream, subscribe to Yjs Array observer\n * - stop(): detach all listeners, dispose provider\n *\n * The session is one-shot per workbook open. Reload = stop() + new instance.\n */\n\"use client\";\n\nimport * as Y from \"yjs\";\nimport { Awareness } from \"y-protocols/awareness\";\n\nimport { electCollabHost, remoteOriginOf, REMOTE_PARAM_TAG } from \"@ai-matrx/data/realtime/collab\";\n\n/**\n * Structural transport contract. `SupabaseYjsProvider` satisfies it in\n * production; the verification harness injects an in-memory loopback\n * transport through the same seam. Keeping this structural (not the concrete\n * class) is what makes the session testable without network.\n */\nexport type CollabProviderLike = {\n connect(): Promise<void>;\n disconnect(): void;\n ready(): Promise<void>;\n /**\n * True when this is the only holder of its room in this tab allowed to act as\n * host (autosave). Same-tab holders share one client id and one uid, so\n * awareness cannot tell them apart. Absent = a lone holder = leader.\n */\n isTabLeader?(): boolean;\n};\n\n// Univer types are loose at the public hook boundary; we narrow with a\n// structural interface so we don't have to import the full Univer surface.\ntype MutationParams = Record<string, unknown> | unknown[] | null;\n\nexport type CollabMutationInfo = {\n id: string;\n type: number; // CommandType.MUTATION = 2 in Univer's enum\n params: MutationParams;\n /** Who made it (the person's uid), so a peer can tell its own other tab's edit from a stranger's. */\n uid?: string;\n};\n\ntype ExecuteOptions = {\n /** Per-Univer convention: when true, the command runs locally only and is\n * NOT broadcast to other plugins. We set this on inbound-apply so the\n * subsequent onMutationExecutedForCollab does NOT re-fire on our peer. */\n onlyLocal?: boolean;\n fromCollab?: boolean;\n};\n\nexport type CommandServiceLike = {\n onMutationExecutedForCollab(\n listener: (info: CollabMutationInfo, options?: ExecuteOptions) => void,\n ): {\n dispose: () => void;\n };\n syncExecuteCommand(\n id: string,\n params: MutationParams,\n options?: ExecuteOptions,\n ): unknown;\n};\n\nexport type WorkbookCollabSessionOptions = {\n workbookId: string;\n uid: string;\n clientId: string;\n commandService: CommandServiceLike;\n /** Factory so this file does not statically depend on the provider. */\n makeProvider: (args: {\n workbookId: string;\n clientId: string;\n doc: Y.Doc;\n awareness: Awareness;\n /** Wire to the provider so a host handover re-runs the election. */\n onLeaderChange: () => void;\n }) => CollabProviderLike;\n /** Called when the awareness set changes (peers join / leave / move cursor). */\n onAwarenessChange?: (awareness: Awareness) => void;\n /**\n * A peer replaced the whole content (history restore, Google update): the\n * newest saved snapshot is the content now. Reload it in place; never\n * re-announce. See `announceReplaced`.\n */\n onReplaced?: (snapshotId: string) => void;\n /**\n * TRUE when the room is bound to a Univer DOCUMENT unit (the Docs editor): there the `doc.mutation.*`\n * stream IS the content, so it must travel. Leave unset for a workbook, where `doc.mutation.*` is the\n * in-cell editor's scratch document and never travels (see `isCellEditorScratch`).\n */\n forwardDocumentMutations?: boolean;\n};\n\n/** What `announceReplaced` writes into the room's control map. */\ntype ReplacedSignal = { snapshotId: string; by: string; nonce: string };\n\nconst MUTATIONS_ARRAY_KEY = \"mutations\";\nconst CONTROL_MAP_KEY = \"control\";\nconst REPLACED_KEY = \"replaced\";\n\nexport class WorkbookCollabSession {\n private readonly options: WorkbookCollabSessionOptions;\n private doc: Y.Doc | null = null;\n private awareness: Awareness | null = null;\n private provider: CollabProviderLike | null = null;\n private commandDisposer: { dispose: () => void } | null = null;\n private yArrayObserver:\n ((event: Y.YArrayEvent<CollabMutationInfo>) => void) | null = null;\n private awarenessObserver: (() => void) | null = null;\n private controlObserver: ((event: Y.YMapEvent<unknown>) => void) | null = null;\n /** >0 while this client swaps its own content: its mutations are not edits. */\n private muted = 0;\n private started = false;\n private disposed = false;\n\n constructor(options: WorkbookCollabSessionOptions) {\n this.options = options;\n }\n\n async start(): Promise<void> {\n if (this.started || this.disposed) return;\n this.started = true;\n\n this.doc = new Y.Doc();\n this.awareness = new Awareness(this.doc);\n\n // Seed local awareness with the user identity. Cursor (sheetId/row/col)\n // is set later via setCursor().\n this.awareness.setLocalState({\n uid: this.options.uid,\n clientId: this.options.clientId,\n name: this.options.uid.slice(0, 8),\n color: pickColor(this.options.uid),\n sheetId: null,\n row: null,\n col: null,\n ts: Date.now(),\n });\n\n this.provider = this.options.makeProvider({\n workbookId: this.options.workbookId,\n clientId: this.options.clientId,\n doc: this.doc,\n awareness: this.awareness,\n onLeaderChange: () => {\n if (this.awareness) this.options.onAwarenessChange?.(this.awareness);\n },\n });\n\n // Subscribe to inbound Yjs array changes BEFORE connecting the provider,\n // so the initial-state replay (if any) hits our observer.\n const yArray = this.doc.getArray<CollabMutationInfo>(MUTATIONS_ARRAY_KEY);\n this.yArrayObserver = (event) => {\n // Local transactions are OUR OWN pushes from handleLocalMutation —\n // Univer already executed those mutations. Re-applying them here would\n // double-apply every local edit on the originator. Only act on\n // transactions that arrived via Y.applyUpdate (origin 'remote').\n if (event.transaction.local) return;\n for (const change of event.changes.delta) {\n if (!change.insert || !Array.isArray(change.insert)) continue;\n for (const op of change.insert as CollabMutationInfo[]) {\n this.applyRemoteMutation(op);\n }\n }\n };\n yArray.observe(this.yArrayObserver as Parameters<typeof yArray.observe>[0]);\n\n // Awareness change passthrough.\n if (this.options.onAwarenessChange) {\n const cb = this.options.onAwarenessChange;\n this.awarenessObserver = () => {\n if (this.awareness) cb(this.awareness);\n };\n this.awareness.on(\"change\", this.awarenessObserver);\n }\n\n // Subscribe to Univer's collab-grain mutation stream.\n this.commandDisposer =\n this.options.commandService.onMutationExecutedForCollab((info) =>\n this.handleLocalMutation(info),\n );\n\n await this.provider.connect();\n await this.provider.ready();\n\n // Whole-content replacement from a peer. Bound AFTER the initial sync: a\n // joiner already loaded the newest snapshot, so the signal it finds in\n // the room's state is history, not news (the editor also skips a\n // snapshot it already shows).\n if (this.disposed || !this.doc) return;\n const control = this.doc.getMap<unknown>(CONTROL_MAP_KEY);\n const seen = (control.get(REPLACED_KEY) as ReplacedSignal | undefined)?.nonce ?? null;\n this.controlObserver = (event) => {\n if (event.transaction.local || !event.keysChanged.has(REPLACED_KEY)) return;\n const signal = control.get(REPLACED_KEY) as ReplacedSignal | undefined;\n if (!signal?.snapshotId || signal.nonce === seen) return;\n this.options.onReplaced?.(signal.snapshotId);\n };\n control.observe(this.controlObserver);\n }\n\n stop(): void {\n if (this.disposed) return;\n this.disposed = true;\n\n this.commandDisposer?.dispose();\n this.commandDisposer = null;\n\n if (this.doc && this.yArrayObserver) {\n const yArray = this.doc.getArray<CollabMutationInfo>(MUTATIONS_ARRAY_KEY);\n yArray.unobserve(\n this.yArrayObserver as Parameters<typeof yArray.unobserve>[0],\n );\n this.yArrayObserver = null;\n }\n\n if (this.doc && this.controlObserver) {\n this.doc.getMap<unknown>(CONTROL_MAP_KEY).unobserve(this.controlObserver);\n this.controlObserver = null;\n }\n\n if (this.awareness && this.awarenessObserver) {\n this.awareness.off(\"change\", this.awarenessObserver);\n this.awarenessObserver = null;\n }\n\n this.provider?.disconnect();\n this.provider = null;\n\n if (this.awareness) {\n this.awareness.destroy();\n this.awareness = null;\n }\n\n if (this.doc) {\n this.doc.destroy();\n this.doc = null;\n }\n }\n\n /**\n * How many mutations the shared log holds right now. Read it BEFORE\n * `workbook.save()`: that many entries are guaranteed to be inside the\n * snapshot that save produces.\n */\n logLength(): number {\n return this.doc?.getArray<CollabMutationInfo>(MUTATIONS_ARRAY_KEY).length ?? 0;\n }\n\n /**\n * Drop the first `upTo` mutations from the shared log once a snapshot that\n * contains them has been SAVED. Without this a joiner loads the saved\n * workbook (which already has every mutation baked in) and then catches up\n * the whole log on top of it, replaying edits into a workbook that already\n * holds them — Univer refuses with \"Trying to overwrite value at key\"\n * (feedback 88184e53). Only the host calls this; peers receive it as a\n * delete, which the inbound observer ignores (it acts on inserts only).\n */\n compactLog(upTo: number): void {\n if (!this.doc || upTo <= 0) return;\n const yArray = this.doc.getArray<CollabMutationInfo>(MUTATIONS_ARRAY_KEY);\n const n = Math.min(upTo, yArray.length);\n if (n > 0) this.doc.transact(() => yArray.delete(0, n));\n }\n\n /**\n * This client replaced the whole content and SAVED it as `snapshotId`.\n * One transaction empties the shared mutation log (every entry targets the\n * content that is gone — a joiner must never replay them onto the new\n * snapshot) and tells every peer to reload the newest snapshot.\n */\n announceReplaced(snapshotId: string): void {\n if (!this.doc) return;\n const yArray = this.doc.getArray<CollabMutationInfo>(MUTATIONS_ARRAY_KEY);\n const control = this.doc.getMap<unknown>(CONTROL_MAP_KEY);\n const signal: ReplacedSignal = {\n snapshotId,\n by: this.options.clientId,\n nonce: `${this.options.clientId}:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`,\n };\n this.doc.transact(() => {\n if (yArray.length > 0) yArray.delete(0, yArray.length);\n control.set(REPLACED_KEY, signal);\n });\n }\n\n /**\n * Run `swap` (this client tearing its content down and mounting the new\n * one) without broadcasting what Univer emits while it does: those are not\n * edits, and peers reload the same snapshot themselves.\n */\n muteWhile<T>(swap: () => T): T {\n this.muted += 1;\n try {\n return swap();\n } finally {\n this.muted -= 1;\n }\n }\n\n /**\n * Update the local awareness cursor. Call this from Univer's selection\n * listener (`worksheet.onSelectionChange(...)`). Throttle at the call site\n * (~50ms is plenty); the provider broadcasts on its own debounced cycle.\n */\n setCursor(input: {\n sheetId: string | null;\n row: number | null;\n col: number | null;\n }): void {\n if (!this.awareness) return;\n const current = this.awareness.getLocalState() ?? {};\n this.awareness.setLocalState({\n ...current,\n sheetId: input.sheetId,\n row: input.row,\n col: input.col,\n ts: Date.now(),\n });\n }\n\n /**\n * Returns the deterministic host: the connected peer with the lowest uid\n * (lexicographic). Host is the only client that writes canonical snapshots.\n * No election protocol; recompute whenever awareness changes.\n */\n electHost(opts: { tabTieBreak?: boolean } = {}): { isHost: boolean; hostUid: string | null } {\n if (!this.awareness) return { isHost: false, hostUid: null };\n const states = Array.from(this.awareness.getStates().values()) as Array<\n Record<string, unknown>\n >;\n // Lowest uid wins ACROSS tabs; inside this tab only the room's tab leader\n // may be host, because every same-tab holder carries the same uid.\n const leader = this.provider?.isTabLeader?.() ?? true;\n return electCollabHost(states, this.options.uid, this.options.clientId, leader, opts.tabTieBreak ?? false);\n }\n\n // ─── internals ────────────────────────────────────────────────────────\n\n private handleLocalMutation(info: CollabMutationInfo): void {\n if (this.muted > 0) return;\n if (!this.options.forwardDocumentMutations && isCellEditorScratch(info)) return;\n // Inbound-applied mutations re-fire this listener because Univer's\n // syncExecuteCommand legitimately processes them through the full\n // pipeline. Skip by sniffing the sentinel we stamped on apply.\n if (\n info.params &&\n typeof info.params === \"object\" &&\n !Array.isArray(info.params) &&\n remoteOriginOf(info.params) !== null\n ) {\n return;\n }\n if (!this.doc) return;\n\n // Normalize params through a JSON round-trip BEFORE pushing into Yjs.\n // Yjs (and the Broadcast transport) only carry JSON-encodable values;\n // Univer types its mutation params as \"serializable\" but JS objects can\n // smuggle Date / Map / class instances through that promise. The\n // round-trip makes the failure mode deterministic: a non-encodable\n // mutation is skipped with a warning instead of corrupting the shared\n // doc or desyncing peers mid-session. What we push here is byte-for-byte\n // what remote peers will apply, so local-apply == remote-apply.\n let safeParams: MutationParams;\n try {\n safeParams =\n info.params === undefined || info.params === null\n ? null\n : (JSON.parse(JSON.stringify(info.params)) as MutationParams);\n } catch (err) {\n console.warn(\n `[collab] skipping non-serializable mutation \"${info.id}\" — peers will not receive it`,\n err,\n );\n return;\n }\n\n const yArray = this.doc.getArray<CollabMutationInfo>(MUTATIONS_ARRAY_KEY);\n yArray.push([{ id: info.id, type: info.type, params: safeParams, uid: this.options.uid }]);\n }\n\n private applyRemoteMutation(op: CollabMutationInfo): void {\n // Wrap params with the sentinel so handleLocalMutation knows not to\n // echo the apply back into Yjs.\n const taggedParams: MutationParams =\n op.params && typeof op.params === \"object\" && !Array.isArray(op.params)\n ? {\n ...(op.params as Record<string, unknown>),\n [REMOTE_PARAM_TAG]: op.uid ?? true,\n }\n : op.params;\n\n try {\n this.options.commandService.syncExecuteCommand(op.id, taggedParams, {\n onlyLocal: true,\n fromCollab: true,\n });\n } catch (err) {\n // One unappliable op (already baked into the loaded snapshot, or a\n // stale target) must not throw into the realtime channel handler and\n // abort the rest of the catch-up. Said, never swallowed.\n console.warn(\n `[collab] could not apply remote mutation \"${op.id}\" — skipped`,\n err,\n );\n }\n }\n}\n\n// ─── helpers ────────────────────────────────────────────────────────────\n\n/**\n * A workbook holds no Univer DOCUMENT units of its own: the `doc.mutation.*` stream a sheet emits is\n * the in-cell editor's scratch document (typing into a cell). The finished text reaches the sheet as\n * `sheet.mutation.set-range-values`, which does travel. Sending the scratch edits made a joiner replay\n * them against an editor unit it does not have (\"Trying to overwrite value at key\").\n */\nexport function isCellEditorScratch(info: Pick<CollabMutationInfo, \"id\">): boolean {\n return typeof info.id === \"string\" && info.id.startsWith(\"doc.mutation.\");\n}\n\nconst CURSOR_PALETTE = [\n \"#3b82f6\", // blue\n \"#10b981\", // emerald\n \"#f59e0b\", // amber\n \"#ef4444\", // red\n \"#8b5cf6\", // violet\n \"#06b6d4\", // cyan\n \"#ec4899\", // pink\n \"#84cc16\", // lime\n];\n\nfunction pickColor(uid: string): string {\n // Deterministic FNV-1a-ish hash so the same user is always the same color\n // across sessions and across other users' screens.\n let h = 2166136261;\n for (let i = 0; i < uid.length; i++) {\n h ^= uid.charCodeAt(i);\n h = Math.imul(h, 16777619);\n }\n return CURSOR_PALETTE[Math.abs(h) % CURSOR_PALETTE.length] ?? \"#ec4899\";\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/univer/collab/WorkbookCollabSession.ts"],"sourcesContent":["/**\n * WorkbookCollabSession — the Univer ↔ Yjs bridge.\n *\n * Subscribes to Univer's public `commandService.onMutationExecutedForCollab`\n * hook for local mutations and pushes them onto a `Y.Array<MutationOp>` shared\n * via `SupabaseYjsProvider`. Inbound Yjs updates produce `IMutationInfo` ops\n * that are applied through `commandService.syncExecuteCommand` with the\n * `fromCollab` execution option flagged so the outbound listener short-\n * circuits and we don't echo our own apply.\n *\n * See `FEATURE.md` in this directory for the architecture, the \"do NOT use\n * the Pro preset\" verdict, and the host-election rule. This class is the\n * runtime side of that plan.\n *\n * Lifecycle:\n * - constructor: wire references, no side effects\n * - start(): create Y.Doc + Awareness, instantiate provider, subscribe to\n * command stream, subscribe to Yjs Array observer\n * - stop(): detach all listeners, dispose provider\n *\n * The session is one-shot per workbook open. Reload = stop() + new instance.\n */\n\"use client\";\n\nimport * as Y from \"yjs\";\nimport { Awareness } from \"y-protocols/awareness\";\n\nimport { electCollabHost, remoteOriginOf, REMOTE_PARAM_TAG } from \"@ai-matrx/data/realtime/collab\";\n\n/**\n * Structural transport contract. `SupabaseYjsProvider` satisfies it in\n * production; the verification harness injects an in-memory loopback\n * transport through the same seam. Keeping this structural (not the concrete\n * class) is what makes the session testable without network.\n */\nexport type CollabProviderLike = {\n connect(): Promise<void>;\n disconnect(): void;\n ready(): Promise<void>;\n /**\n * True when this is the only holder of its room in this tab allowed to act as\n * host (autosave). Same-tab holders share one client id and one uid, so\n * awareness cannot tell them apart. Absent = a lone holder = leader.\n */\n isTabLeader?(): boolean;\n};\n\n// Univer types are loose at the public hook boundary; we narrow with a\n// structural interface so we don't have to import the full Univer surface.\ntype MutationParams = Record<string, unknown> | unknown[] | null;\n\nexport type CollabMutationInfo = {\n id: string;\n type: number; // CommandType.MUTATION = 2 in Univer's enum\n params: MutationParams;\n /** Who made it (the person's uid), so a peer can tell its own other tab's edit from a stranger's. */\n uid?: string;\n};\n\ntype ExecuteOptions = {\n /** Per-Univer convention: when true, the command runs locally only and is\n * NOT broadcast to other plugins. We set this on inbound-apply so the\n * subsequent onMutationExecutedForCollab does NOT re-fire on our peer. */\n onlyLocal?: boolean;\n fromCollab?: boolean;\n};\n\nexport type CommandServiceLike = {\n onMutationExecutedForCollab(\n listener: (info: CollabMutationInfo, options?: ExecuteOptions) => void,\n ): {\n dispose: () => void;\n };\n syncExecuteCommand(\n id: string,\n params: MutationParams,\n options?: ExecuteOptions,\n ): unknown;\n};\n\nexport type WorkbookCollabSessionOptions = {\n workbookId: string;\n uid: string;\n clientId: string;\n commandService: CommandServiceLike;\n /** Factory so this file does not statically depend on the provider. */\n makeProvider: (args: {\n workbookId: string;\n clientId: string;\n doc: Y.Doc;\n awareness: Awareness;\n /** Wire to the provider so a host handover re-runs the election. */\n onLeaderChange: () => void;\n }) => CollabProviderLike;\n /** Called when the awareness set changes (peers join / leave / move cursor). */\n onAwarenessChange?: (awareness: Awareness) => void;\n /**\n * A peer replaced the whole content (history restore, Google update): the\n * newest saved snapshot is the content now. Reload it in place; never\n * re-announce. See `announceReplaced`.\n */\n onReplaced?: (snapshotId: string) => void;\n /**\n * TRUE when the room is bound to a Univer DOCUMENT unit (the Docs editor): there the `doc.mutation.*`\n * stream IS the content, so it must travel. Leave unset for a workbook, where `doc.mutation.*` is the\n * in-cell editor's scratch document and never travels (see `isCellEditorScratch`).\n */\n forwardDocumentMutations?: boolean;\n /**\n * The Univer unit id this seat mounted. Each seat can mount its own unit, so a peer's mutation\n * (addressed to the SENDER's unit) is re-addressed to this one before it is applied.\n */\n unitId?: () => string | null;\n /** A peer's mutation could not be applied here: the seat says so and re-syncs; never only a console line. */\n onApplyFailed?: (mutationId: string, error: unknown) => void;\n};\n\n/** What `announceReplaced` writes into the room's control map. */\ntype ReplacedSignal = { snapshotId: string; by: string; nonce: string };\n\nconst MUTATIONS_ARRAY_KEY = \"mutations\";\nconst CONTROL_MAP_KEY = \"control\";\nconst REPLACED_KEY = \"replaced\";\n\nexport class WorkbookCollabSession {\n private readonly options: WorkbookCollabSessionOptions;\n private doc: Y.Doc | null = null;\n private awareness: Awareness | null = null;\n private provider: CollabProviderLike | null = null;\n private commandDisposer: { dispose: () => void } | null = null;\n private yArrayObserver:\n ((event: Y.YArrayEvent<CollabMutationInfo>) => void) | null = null;\n private awarenessObserver: (() => void) | null = null;\n private controlObserver: ((event: Y.YMapEvent<unknown>) => void) | null = null;\n /** >0 while this client swaps its own content: its mutations are not edits. */\n private muted = 0;\n private started = false;\n private disposed = false;\n\n constructor(options: WorkbookCollabSessionOptions) {\n this.options = options;\n }\n\n async start(): Promise<void> {\n if (this.started || this.disposed) return;\n this.started = true;\n\n this.doc = new Y.Doc();\n this.awareness = new Awareness(this.doc);\n\n // Seed local awareness with the user identity. Cursor (sheetId/row/col)\n // is set later via setCursor().\n this.awareness.setLocalState({\n uid: this.options.uid,\n clientId: this.options.clientId,\n name: this.options.uid.slice(0, 8),\n color: pickColor(this.options.uid),\n sheetId: null,\n row: null,\n col: null,\n ts: Date.now(),\n });\n\n this.provider = this.options.makeProvider({\n workbookId: this.options.workbookId,\n clientId: this.options.clientId,\n doc: this.doc,\n awareness: this.awareness,\n onLeaderChange: () => {\n if (this.awareness) this.options.onAwarenessChange?.(this.awareness);\n },\n });\n\n // Subscribe to inbound Yjs array changes BEFORE connecting the provider,\n // so the initial-state replay (if any) hits our observer.\n const yArray = this.doc.getArray<CollabMutationInfo>(MUTATIONS_ARRAY_KEY);\n this.yArrayObserver = (event) => {\n // Local transactions are OUR OWN pushes from handleLocalMutation —\n // Univer already executed those mutations. Re-applying them here would\n // double-apply every local edit on the originator. Only act on\n // transactions that arrived via Y.applyUpdate (origin 'remote').\n if (event.transaction.local) return;\n for (const change of event.changes.delta) {\n if (!change.insert || !Array.isArray(change.insert)) continue;\n for (const op of change.insert as CollabMutationInfo[]) {\n this.applyRemoteMutation(op);\n }\n }\n };\n yArray.observe(this.yArrayObserver as Parameters<typeof yArray.observe>[0]);\n\n // Awareness change passthrough.\n if (this.options.onAwarenessChange) {\n const cb = this.options.onAwarenessChange;\n this.awarenessObserver = () => {\n if (this.awareness) cb(this.awareness);\n };\n this.awareness.on(\"change\", this.awarenessObserver);\n }\n\n // Subscribe to Univer's collab-grain mutation stream.\n this.commandDisposer =\n this.options.commandService.onMutationExecutedForCollab((info) =>\n this.handleLocalMutation(info),\n );\n\n await this.provider.connect();\n await this.provider.ready();\n\n // Whole-content replacement from a peer. Bound AFTER the initial sync: a\n // joiner already loaded the newest snapshot, so the signal it finds in\n // the room's state is history, not news (the editor also skips a\n // snapshot it already shows).\n if (this.disposed || !this.doc) return;\n const control = this.doc.getMap<unknown>(CONTROL_MAP_KEY);\n const seen = (control.get(REPLACED_KEY) as ReplacedSignal | undefined)?.nonce ?? null;\n this.controlObserver = (event) => {\n if (event.transaction.local || !event.keysChanged.has(REPLACED_KEY)) return;\n const signal = control.get(REPLACED_KEY) as ReplacedSignal | undefined;\n if (!signal?.snapshotId || signal.nonce === seen) return;\n this.options.onReplaced?.(signal.snapshotId);\n };\n control.observe(this.controlObserver);\n }\n\n stop(): void {\n if (this.disposed) return;\n this.disposed = true;\n\n this.commandDisposer?.dispose();\n this.commandDisposer = null;\n\n if (this.doc && this.yArrayObserver) {\n const yArray = this.doc.getArray<CollabMutationInfo>(MUTATIONS_ARRAY_KEY);\n yArray.unobserve(\n this.yArrayObserver as Parameters<typeof yArray.unobserve>[0],\n );\n this.yArrayObserver = null;\n }\n\n if (this.doc && this.controlObserver) {\n this.doc.getMap<unknown>(CONTROL_MAP_KEY).unobserve(this.controlObserver);\n this.controlObserver = null;\n }\n\n if (this.awareness && this.awarenessObserver) {\n this.awareness.off(\"change\", this.awarenessObserver);\n this.awarenessObserver = null;\n }\n\n this.provider?.disconnect();\n this.provider = null;\n\n if (this.awareness) {\n this.awareness.destroy();\n this.awareness = null;\n }\n\n if (this.doc) {\n this.doc.destroy();\n this.doc = null;\n }\n }\n\n /**\n * How many mutations the shared log holds right now. Read it BEFORE\n * `workbook.save()`: that many entries are guaranteed to be inside the\n * snapshot that save produces.\n */\n logLength(): number {\n return this.doc?.getArray<CollabMutationInfo>(MUTATIONS_ARRAY_KEY).length ?? 0;\n }\n\n /**\n * Drop the first `upTo` mutations from the shared log once a snapshot that\n * contains them has been SAVED. Without this a joiner loads the saved\n * workbook (which already has every mutation baked in) and then catches up\n * the whole log on top of it, replaying edits into a workbook that already\n * holds them — Univer refuses with \"Trying to overwrite value at key\"\n * (feedback 88184e53). Only the host calls this; peers receive it as a\n * delete, which the inbound observer ignores (it acts on inserts only).\n */\n compactLog(upTo: number): void {\n if (!this.doc || upTo <= 0) return;\n const yArray = this.doc.getArray<CollabMutationInfo>(MUTATIONS_ARRAY_KEY);\n const n = Math.min(upTo, yArray.length);\n if (n > 0) this.doc.transact(() => yArray.delete(0, n));\n }\n\n /**\n * This client replaced the whole content and SAVED it as `snapshotId`.\n * One transaction empties the shared mutation log (every entry targets the\n * content that is gone — a joiner must never replay them onto the new\n * snapshot) and tells every peer to reload the newest snapshot.\n */\n announceReplaced(snapshotId: string): void {\n if (!this.doc) return;\n const yArray = this.doc.getArray<CollabMutationInfo>(MUTATIONS_ARRAY_KEY);\n const control = this.doc.getMap<unknown>(CONTROL_MAP_KEY);\n const signal: ReplacedSignal = {\n snapshotId,\n by: this.options.clientId,\n nonce: `${this.options.clientId}:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`,\n };\n this.doc.transact(() => {\n if (yArray.length > 0) yArray.delete(0, yArray.length);\n control.set(REPLACED_KEY, signal);\n });\n }\n\n /**\n * Run `swap` (this client tearing its content down and mounting the new\n * one) without broadcasting what Univer emits while it does: those are not\n * edits, and peers reload the same snapshot themselves.\n */\n muteWhile<T>(swap: () => T): T {\n this.muted += 1;\n try {\n return swap();\n } finally {\n this.muted -= 1;\n }\n }\n\n /**\n * Update the local awareness cursor. Call this from Univer's selection\n * listener (`worksheet.onSelectionChange(...)`). Throttle at the call site\n * (~50ms is plenty); the provider broadcasts on its own debounced cycle.\n */\n setCursor(input: {\n sheetId: string | null;\n row: number | null;\n col: number | null;\n }): void {\n if (!this.awareness) return;\n const current = this.awareness.getLocalState() ?? {};\n this.awareness.setLocalState({\n ...current,\n sheetId: input.sheetId,\n row: input.row,\n col: input.col,\n ts: Date.now(),\n });\n }\n\n /**\n * Returns the deterministic host: the connected peer with the lowest uid\n * (lexicographic). Host is the only client that writes canonical snapshots.\n * No election protocol; recompute whenever awareness changes.\n */\n electHost(opts: { tabTieBreak?: boolean } = {}): { isHost: boolean; hostUid: string | null } {\n if (!this.awareness) return { isHost: false, hostUid: null };\n const states = Array.from(this.awareness.getStates().values()) as Array<\n Record<string, unknown>\n >;\n // Lowest uid wins ACROSS tabs; inside this tab only the room's tab leader\n // may be host, because every same-tab holder carries the same uid.\n const leader = this.provider?.isTabLeader?.() ?? true;\n return electCollabHost(states, this.options.uid, this.options.clientId, leader, opts.tabTieBreak ?? false);\n }\n\n // ─── internals ────────────────────────────────────────────────────────\n\n private handleLocalMutation(info: CollabMutationInfo): void {\n if (this.muted > 0) return;\n if (!this.options.forwardDocumentMutations && isCellEditorScratch(info)) return;\n // Inbound-applied mutations re-fire this listener because Univer's\n // syncExecuteCommand legitimately processes them through the full\n // pipeline. Skip by sniffing the sentinel we stamped on apply.\n if (\n info.params &&\n typeof info.params === \"object\" &&\n !Array.isArray(info.params) &&\n remoteOriginOf(info.params) !== null\n ) {\n return;\n }\n if (!this.doc) return;\n\n // Normalize params through a JSON round-trip BEFORE pushing into Yjs.\n // Yjs (and the Broadcast transport) only carry JSON-encodable values;\n // Univer types its mutation params as \"serializable\" but JS objects can\n // smuggle Date / Map / class instances through that promise. The\n // round-trip makes the failure mode deterministic: a non-encodable\n // mutation is skipped with a warning instead of corrupting the shared\n // doc or desyncing peers mid-session. What we push here is byte-for-byte\n // what remote peers will apply, so local-apply == remote-apply.\n let safeParams: MutationParams;\n try {\n safeParams =\n info.params === undefined || info.params === null\n ? null\n : (JSON.parse(JSON.stringify(info.params)) as MutationParams);\n } catch (err) {\n console.warn(\n `[collab] skipping non-serializable mutation \"${info.id}\" — peers will not receive it`,\n err,\n );\n return;\n }\n\n const yArray = this.doc.getArray<CollabMutationInfo>(MUTATIONS_ARRAY_KEY);\n yArray.push([{ id: info.id, type: info.type, params: safeParams, uid: this.options.uid }]);\n }\n\n private applyRemoteMutation(op: CollabMutationInfo): void {\n // Wrap params with the sentinel so handleLocalMutation knows not to\n // echo the apply back into Yjs.\n const ownUnit = this.options.unitId?.() ?? null;\n const taggedParams: MutationParams =\n op.params && typeof op.params === \"object\" && !Array.isArray(op.params)\n ? {\n ...(op.params as Record<string, unknown>),\n ...(ownUnit && typeof (op.params as { unitId?: unknown }).unitId === \"string\" ? { unitId: ownUnit } : {}),\n [REMOTE_PARAM_TAG]: op.uid ?? true,\n }\n : op.params;\n\n try {\n this.options.commandService.syncExecuteCommand(op.id, taggedParams, {\n onlyLocal: true,\n fromCollab: true,\n });\n } catch (err) {\n // One unappliable op (already baked into the loaded snapshot, or a\n // stale target) must not throw into the realtime channel handler and\n // abort the rest of the catch-up. Said, never swallowed.\n console.warn(\n `[collab] could not apply remote mutation \"${op.id}\" — skipped`,\n err,\n );\n this.options.onApplyFailed?.(op.id, err);\n }\n }\n}\n\n// ─── helpers ────────────────────────────────────────────────────────────\n\n/**\n * A workbook holds no Univer DOCUMENT units of its own: the `doc.mutation.*` stream a sheet emits is\n * the in-cell editor's scratch document (typing into a cell). The finished text reaches the sheet as\n * `sheet.mutation.set-range-values`, which does travel. Sending the scratch edits made a joiner replay\n * them against an editor unit it does not have (\"Trying to overwrite value at key\").\n */\nexport function isCellEditorScratch(info: Pick<CollabMutationInfo, \"id\">): boolean {\n return typeof info.id === \"string\" && info.id.startsWith(\"doc.mutation.\");\n}\n\nconst CURSOR_PALETTE = [\n \"#3b82f6\", // blue\n \"#10b981\", // emerald\n \"#f59e0b\", // amber\n \"#ef4444\", // red\n \"#8b5cf6\", // violet\n \"#06b6d4\", // cyan\n \"#ec4899\", // pink\n \"#84cc16\", // lime\n];\n\nfunction pickColor(uid: string): string {\n // Deterministic FNV-1a-ish hash so the same user is always the same color\n // across sessions and across other users' screens.\n let h = 2166136261;\n for (let i = 0; i < uid.length; i++) {\n h ^= uid.charCodeAt(i);\n h = Math.imul(h, 16777619);\n }\n return CURSOR_PALETTE[Math.abs(h) % CURSOR_PALETTE.length] ?? \"#ec4899\";\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-matrx/office",
3
- "version": "0.4.0",
3
+ "version": "0.5.2",
4
4
  "description": "The AI Matrx Office layer: the editors' shared command and menu model, the Google and Microsoft ribbon model, page setup, the Office record types and the configured product hosts. The Univer glue shared by Sheets and Docs. Sheets, Docs and the other products arrive as sections of this one package.",
5
5
  "type": "module",
6
6
  "main": "./dist/core.cjs",
@@ -111,22 +111,55 @@
111
111
  "types": "./dist/univer-realtime.d.cts",
112
112
  "default": "./dist/univer-realtime.cjs"
113
113
  }
114
+ },
115
+ "./docs": {
116
+ "import": {
117
+ "types": "./dist/docs.d.ts",
118
+ "default": "./dist/docs.js"
119
+ },
120
+ "require": {
121
+ "types": "./dist/docs.d.cts",
122
+ "default": "./dist/docs.cjs"
123
+ }
124
+ },
125
+ "./docs/io": {
126
+ "import": {
127
+ "types": "./dist/docs-io.d.ts",
128
+ "default": "./dist/docs-io.js"
129
+ },
130
+ "require": {
131
+ "types": "./dist/docs-io.d.cts",
132
+ "default": "./dist/docs-io.cjs"
133
+ }
134
+ },
135
+ "./docs/tools": {
136
+ "import": {
137
+ "types": "./dist/docs-tools.d.ts",
138
+ "default": "./dist/docs-tools.js"
139
+ },
140
+ "require": {
141
+ "types": "./dist/docs-tools.d.cts",
142
+ "default": "./dist/docs-tools.cjs"
143
+ }
114
144
  }
115
145
  },
116
146
  "dependencies": {
117
147
  "@univerjs/core": "1.0.4",
148
+ "@univerjs/docs": "1.0.4",
118
149
  "@univerjs/icons": "1.43.1",
119
150
  "@univerjs/presets": "1.0.4",
120
151
  "@univerjs/ui": "1.0.4",
121
152
  "y-protocols": "^1.0.7",
122
153
  "yjs": "^13.6.33",
123
- "@ai-matrx/alchemy": "latest",
154
+ "@ai-matrx/api": "latest",
124
155
  "@ai-matrx/auth": "latest",
156
+ "@ai-matrx/content-ir": "latest",
125
157
  "@ai-matrx/data": "latest",
126
- "@ai-matrx/print": "latest",
127
158
  "@ai-matrx/kit": "latest",
128
- "@ai-matrx/api": "latest",
129
- "@ai-matrx/records": "latest"
159
+ "@ai-matrx/alchemy": "latest",
160
+ "@ai-matrx/print": "latest",
161
+ "@ai-matrx/records": "latest",
162
+ "@ai-matrx/rich-content": "latest"
130
163
  },
131
164
  "peerDependencies": {
132
165
  "react": ">=18.0.0",
@@ -137,6 +170,7 @@
137
170
  "@supabase/supabase-js": "^2.117.0",
138
171
  "@types/node": "^26.6.2",
139
172
  "@types/react": "19.3.0",
173
+ "jsdom": "^30.1.1",
140
174
  "publint": "^0.3.24",
141
175
  "react": "^19.3.0",
142
176
  "tsup": "^8.5.1",
@@ -177,5 +211,5 @@
177
211
  "test:watch": "vitest",
178
212
  "verify:tarball": "node ./scripts/verify-tarball.mjs"
179
213
  },
180
- "gitHead": "90ba9478e3daeb47287e61e3acdf09f48b723cba"
214
+ "gitHead": "90fd9532558a152b93ec59d54332de66a67153a9"
181
215
  }