@beyondwork/docx-react-component 1.0.47 → 1.0.48

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.
Files changed (53) hide show
  1. package/package.json +1 -1
  2. package/src/api/public-types.ts +115 -1
  3. package/src/compare/diff-engine.ts +4 -0
  4. package/src/core/commands/add-scope.ts +257 -0
  5. package/src/core/commands/formatting-commands.ts +2 -0
  6. package/src/core/schema/text-schema.ts +95 -1
  7. package/src/core/state/text-transaction.ts +17 -5
  8. package/src/io/chart-preview-resolver.ts +27 -0
  9. package/src/io/docx-session.ts +226 -38
  10. package/src/io/export/serialize-main-document.ts +37 -0
  11. package/src/io/export/serialize-settings.ts +421 -0
  12. package/src/io/export/serialize-styles.ts +10 -0
  13. package/src/io/normalize/normalize-text.ts +1 -0
  14. package/src/io/ooxml/chart/parse-axis.ts +277 -0
  15. package/src/io/ooxml/chart/parse-chart-space.ts +813 -0
  16. package/src/io/ooxml/chart/parse-series.ts +570 -0
  17. package/src/io/ooxml/chart/resolve-color.ts +251 -0
  18. package/src/io/ooxml/chart/types.ts +420 -0
  19. package/src/io/ooxml/parse-block-structure.ts +99 -0
  20. package/src/io/ooxml/parse-complex-content.ts +87 -2
  21. package/src/io/ooxml/parse-main-document.ts +115 -1
  22. package/src/io/ooxml/parse-scope-markers.ts +184 -0
  23. package/src/io/ooxml/parse-settings-blueprint.ts +349 -0
  24. package/src/io/ooxml/parse-settings.ts +97 -1
  25. package/src/io/ooxml/parse-styles.ts +65 -0
  26. package/src/io/ooxml/parse-theme.ts +2 -127
  27. package/src/io/ooxml/xml-attr-helpers.ts +59 -1
  28. package/src/io/ooxml/xml-parser.ts +142 -0
  29. package/src/model/canonical-document.ts +94 -0
  30. package/src/model/scope-markers.ts +144 -0
  31. package/src/runtime/collab/base-doc-fingerprint.ts +99 -0
  32. package/src/runtime/collab/checkpoint-election.ts +75 -0
  33. package/src/runtime/collab/checkpoint-scheduler.ts +204 -0
  34. package/src/runtime/collab/checkpoint-store.ts +115 -0
  35. package/src/runtime/collab/event-types.ts +27 -0
  36. package/src/runtime/collab/index.ts +22 -0
  37. package/src/runtime/collab/remote-cursor-awareness.ts +167 -0
  38. package/src/runtime/collab/runtime-collab-sync.ts +279 -0
  39. package/src/runtime/document-runtime.ts +214 -16
  40. package/src/runtime/editor-surface/capabilities.ts +63 -50
  41. package/src/runtime/layout/layout-engine-version.ts +8 -1
  42. package/src/runtime/prerender/cache-envelope.ts +19 -7
  43. package/src/runtime/prerender/cache-key.ts +25 -14
  44. package/src/runtime/prerender/canonical-document-hash.ts +63 -0
  45. package/src/runtime/prerender/customxml-cache.ts +211 -0
  46. package/src/runtime/prerender/customxml-probe.ts +78 -0
  47. package/src/runtime/prerender/prerender-document.ts +74 -7
  48. package/src/runtime/scope-resolver.ts +148 -0
  49. package/src/runtime/scope-tag-registry.ts +10 -0
  50. package/src/runtime/surface-projection.ts +8 -1
  51. package/src/ui/WordReviewEditor.tsx +30 -0
  52. package/src/ui/editor-runtime-boundary.ts +6 -1
  53. package/src/ui/runtime-shortcut-dispatch.ts +12 -7
@@ -0,0 +1,99 @@
1
+ import type { CanonicalDocumentEnvelope } from "../../core/state/editor-state.ts";
2
+ import { sha256Hex } from "../../io/source-package-provenance.ts";
3
+
4
+ /**
5
+ * Computes a deterministic fingerprint of the base-doc portion of a
6
+ * {@link CanonicalDocumentEnvelope}. The fingerprint is used by
7
+ * `createRuntimeCollabSync` to verify that every peer attaching to a
8
+ * shared `Y.Doc` started from the same canonical content.
9
+ *
10
+ * The fingerprint intentionally **excludes** session-local identity
11
+ * fields so two peers loading the same source `.docx` under different
12
+ * local session IDs still converge on the same hash:
13
+ *
14
+ * | Field | Included | Rationale |
15
+ * |---|---|---|
16
+ * | `schemaVersion` | yes | CDS contract version; mismatch is a different base |
17
+ * | `metadata` | yes | customProperties are part of the document |
18
+ * | `styles` | yes | style cascade is base content |
19
+ * | `numbering` | yes | numbering definitions are base content |
20
+ * | `media` | yes | embedded media are base content |
21
+ * | `content` | yes | the editable body tree |
22
+ * | `preservation` | yes | opaque fragments round-trip identically from the source |
23
+ * | `docId` | **no** | session-local |
24
+ * | `createdAt` / `updatedAt` | **no** | session-local timestamps |
25
+ * | `review` | **no** | tracked changes are session state |
26
+ * | `diagnostics` | **no** | validator output is session state |
27
+ * | `subParts` / `fieldRegistry` | **no** | may be partially session-computed |
28
+ *
29
+ * Hash pipeline: the subset is serialized via {@link stableStringify}
30
+ * (sorts object keys recursively, preserves array order, follows
31
+ * `JSON.stringify` semantics for `undefined`), encoded UTF-8, and fed
32
+ * to `sha256Hex` from `src/io/source-package-provenance.ts`. Returns a
33
+ * 64-char lowercase hex string.
34
+ *
35
+ * This function is pure and synchronous — safe to call at attach time
36
+ * without breaking the `createRuntimeCollabSync` disposer contract.
37
+ */
38
+ export function computeBaseDocFingerprint(envelope: CanonicalDocumentEnvelope): string {
39
+ const subset = {
40
+ schemaVersion: envelope.schemaVersion,
41
+ metadata: envelope.metadata,
42
+ styles: envelope.styles,
43
+ numbering: envelope.numbering,
44
+ media: envelope.media,
45
+ content: envelope.content,
46
+ preservation: envelope.preservation,
47
+ };
48
+ const serialized = stableStringify(subset);
49
+ const bytes = new TextEncoder().encode(serialized);
50
+ return sha256Hex(bytes);
51
+ }
52
+
53
+ /**
54
+ * Deterministic JSON-like serializer. Object keys are sorted
55
+ * alphabetically at every depth; arrays preserve order; `undefined`
56
+ * values in objects are skipped, `undefined` values in arrays become
57
+ * `null` — matching `JSON.stringify` semantics. Primitives emit
58
+ * identically to `JSON.stringify`.
59
+ *
60
+ * Exported for unit testing and for callers that want to hash a
61
+ * narrower slice of the envelope.
62
+ */
63
+ export function stableStringify(value: unknown): string {
64
+ if (value === null) return "null";
65
+ if (value === undefined) return "null";
66
+
67
+ const t = typeof value;
68
+ if (t === "string" || t === "number" || t === "boolean") {
69
+ return JSON.stringify(value);
70
+ }
71
+
72
+ if (Array.isArray(value)) {
73
+ const parts: string[] = [];
74
+ for (const item of value) {
75
+ parts.push(item === undefined ? "null" : stableStringify(item));
76
+ }
77
+ return "[" + parts.join(",") + "]";
78
+ }
79
+
80
+ if (t === "object") {
81
+ const obj = value as Record<string, unknown>;
82
+ const keys: string[] = [];
83
+ for (const key of Object.keys(obj)) {
84
+ if (obj[key] !== undefined) {
85
+ keys.push(key);
86
+ }
87
+ }
88
+ keys.sort();
89
+ const parts: string[] = [];
90
+ for (const key of keys) {
91
+ parts.push(JSON.stringify(key) + ":" + stableStringify(obj[key]));
92
+ }
93
+ return "{" + parts.join(",") + "}";
94
+ }
95
+
96
+ // Functions, symbols, bigints — should not appear in a canonical
97
+ // envelope. Fall back to `null` to keep the output valid JSON-ish.
98
+ return "null";
99
+ }
@@ -0,0 +1,75 @@
1
+ import type { Awareness } from "y-protocols/awareness";
2
+
3
+ /**
4
+ * Election policy for "which peer should write the next checkpoint":
5
+ * the peer with the lowest `clientID` currently visible in
6
+ * `awareness.getStates()` wins. Deterministic, zero-coordination, and
7
+ * stable across the network (every peer independently computes the
8
+ * same winner).
9
+ *
10
+ * Properties:
11
+ * - `clientID` is stable per Y.Doc lifetime, so a peer does not drift
12
+ * between elected / not-elected while connected.
13
+ * - If the elected peer disconnects, `awareness` emits `change` with
14
+ * `removed`, and the next-lowest peer observes the change + becomes
15
+ * elected. No consensus handshake needed.
16
+ * - If awareness is empty (no peers have published state yet), no peer
17
+ * is elected — wait for `setLocalStateField` / `setLocalState` calls
18
+ * to land.
19
+ *
20
+ * NOT a leader-election primitive with fencing / lease semantics. Two
21
+ * peers racing at exactly the same split-second could both believe
22
+ * they're elected and both write a checkpoint. That is benign — both
23
+ * writes flow into the same `Y.Array<Checkpoint>`; joiners simply see
24
+ * two consecutive checkpoints and apply the last one. Bandwidth cost
25
+ * is one extra snapshot per race, no correctness impact.
26
+ */
27
+ export function isElectedCheckpointAuthor(
28
+ awareness: Awareness,
29
+ localClientId: number,
30
+ ): boolean {
31
+ const states = awareness.getStates();
32
+ if (states.size === 0) return false;
33
+ let minClientId: number | null = null;
34
+ for (const [clientId] of states) {
35
+ if (minClientId === null || clientId < minClientId) {
36
+ minClientId = clientId;
37
+ }
38
+ }
39
+ return minClientId === localClientId;
40
+ }
41
+
42
+ /**
43
+ * Subscribes to election-status changes for the local client. Fires
44
+ * immediately on subscribe with the current status, then on every
45
+ * awareness `change` where the elected status flips. Never fires
46
+ * consecutively with the same value.
47
+ *
48
+ * Returns an unsubscribe function.
49
+ */
50
+ export function subscribeElectedCheckpointAuthor(
51
+ awareness: Awareness,
52
+ localClientId: number,
53
+ listener: (isElected: boolean) => void,
54
+ ): () => void {
55
+ let lastValue: boolean | null = null;
56
+
57
+ function check(): void {
58
+ const elected = isElectedCheckpointAuthor(awareness, localClientId);
59
+ if (elected !== lastValue) {
60
+ lastValue = elected;
61
+ try {
62
+ listener(elected);
63
+ } catch {
64
+ // Listener errors are isolated.
65
+ }
66
+ }
67
+ }
68
+
69
+ awareness.on("change", check);
70
+ check();
71
+
72
+ return () => {
73
+ awareness.off("change", check);
74
+ };
75
+ }
@@ -0,0 +1,204 @@
1
+ import * as Y from "yjs";
2
+
3
+ import type { CommandEvent } from "./event-types.ts";
4
+
5
+ /**
6
+ * Context passed to `onTrigger` when the scheduler decides a checkpoint
7
+ * should be produced.
8
+ *
9
+ * - `reason: "event-count"` — the count of broadcast events since the
10
+ * last `markCheckpointed()` crossed `eventCountThreshold`.
11
+ * - `reason: "inactivity"` — no new broadcast events arrived for
12
+ * `inactivityMs` after the most recent one, so the document has
13
+ * stabilized and it is a good moment to snapshot.
14
+ *
15
+ * `afterEventId` is the `eventId` of the most recent broadcast event
16
+ * included in the trigger. The caller (the elected author) should
17
+ * pass this verbatim into the `Checkpoint.afterEventId` field so
18
+ * joiners know which tail events still need replay.
19
+ */
20
+ export interface CheckpointTriggerContext {
21
+ reason: "event-count" | "inactivity";
22
+ afterEventId: string;
23
+ }
24
+
25
+ export interface CreateCheckpointSchedulerOptions {
26
+ ydoc: Y.Doc;
27
+ /**
28
+ * Fire a trigger once the count of broadcast events since the last
29
+ * `markCheckpointed()` reaches this number. Omit (or set to
30
+ * `undefined`) to disable count-based triggers.
31
+ */
32
+ eventCountThreshold?: number;
33
+ /**
34
+ * Fire a trigger once no new broadcast events have arrived for this
35
+ * many milliseconds (measured from the most recent event). Omit (or
36
+ * set to `undefined`) to disable inactivity triggers.
37
+ */
38
+ inactivityMs?: number;
39
+ /**
40
+ * Called when a threshold or inactivity window fires. The scheduler
41
+ * suppresses further triggers until the caller acknowledges by
42
+ * calling `markCheckpointed()` — preventing duplicate checkpoints
43
+ * while the author is still finishing its write.
44
+ */
45
+ onTrigger: (ctx: CheckpointTriggerContext) => void;
46
+ /**
47
+ * Override `Date.now` — used by tests to avoid real clocks. Defaults
48
+ * to `Date.now`.
49
+ */
50
+ now?: () => number;
51
+ /**
52
+ * Override `setTimeout` — used by tests to inject a fake timer.
53
+ * Defaults to the global `setTimeout`.
54
+ */
55
+ setTimeout?: typeof setTimeout;
56
+ /** Override `clearTimeout` — matches `setTimeout`. */
57
+ clearTimeout?: typeof clearTimeout;
58
+ }
59
+
60
+ export interface CheckpointSchedulerHandle {
61
+ /**
62
+ * Resets the counter + idle state after the caller has written the
63
+ * corresponding checkpoint. Clears the internal "fired" flag so
64
+ * future thresholds can trigger again.
65
+ */
66
+ markCheckpointed(): void;
67
+ destroy(): void;
68
+ }
69
+
70
+ /**
71
+ * Watches a shared `Y.Array<CommandEvent>("commandEvents")` and fires
72
+ * `onTrigger` when a count or inactivity threshold suggests a
73
+ * checkpoint should be written. Stateless with respect to the actual
74
+ * checkpoint store — callers compose the two explicitly (election
75
+ * decides who writes; the writer calls `markCheckpointed()` after
76
+ * appending).
77
+ */
78
+ export function createCheckpointScheduler(
79
+ options: CreateCheckpointSchedulerOptions,
80
+ ): CheckpointSchedulerHandle {
81
+ const {
82
+ ydoc,
83
+ eventCountThreshold,
84
+ inactivityMs,
85
+ onTrigger,
86
+ now = Date.now,
87
+ setTimeout: setTimeoutFn = setTimeout,
88
+ clearTimeout: clearTimeoutFn = clearTimeout,
89
+ } = options;
90
+
91
+ const yEvents = ydoc.getArray<CommandEvent>("commandEvents");
92
+ const disabled = eventCountThreshold === undefined && inactivityMs === undefined;
93
+
94
+ let eventsSinceLastCheckpoint = 0;
95
+ let lastEventId: string | null = null;
96
+ let fired = false;
97
+ let idleTimerHandle: ReturnType<typeof setTimeout> | null = null;
98
+ let destroyed = false;
99
+
100
+ function clearIdleTimer(): void {
101
+ if (idleTimerHandle !== null) {
102
+ clearTimeoutFn(idleTimerHandle);
103
+ idleTimerHandle = null;
104
+ }
105
+ }
106
+
107
+ function armIdleTimer(): void {
108
+ if (destroyed) return;
109
+ if (inactivityMs === undefined) return;
110
+ clearIdleTimer();
111
+ idleTimerHandle = setTimeoutFn(() => {
112
+ idleTimerHandle = null;
113
+ if (destroyed || fired || lastEventId === null) return;
114
+ fired = true;
115
+ try {
116
+ onTrigger({ reason: "inactivity", afterEventId: lastEventId });
117
+ } catch {
118
+ // Caller errors are isolated; the scheduler keeps running.
119
+ }
120
+ }, inactivityMs);
121
+ }
122
+
123
+ // Avoid pretending there was no history: on init, check what's
124
+ // already in the array so scheduler state reflects the current doc.
125
+ // We don't fire for pre-existing events — we only count new ones
126
+ // post-subscribe. `lastEventId` seeds from the tail so an
127
+ // `inactivityMs` trigger has something to report if the caller
128
+ // wires `now` to a later time.
129
+ const initial = yEvents.toArray();
130
+ if (initial.length > 0) {
131
+ const tail = initial[initial.length - 1];
132
+ if (tail && typeof tail.eventId === "string") {
133
+ lastEventId = tail.eventId;
134
+ }
135
+ }
136
+
137
+ function onYEventsChange(event: Y.YArrayEvent<CommandEvent>): void {
138
+ if (destroyed) return;
139
+ if (disabled) return;
140
+ let inserted = 0;
141
+ let tailEventId: string | null = null;
142
+ for (const delta of event.changes.delta) {
143
+ if (!Array.isArray(delta.insert)) continue;
144
+ for (const value of delta.insert) {
145
+ const evt = value as CommandEvent | undefined;
146
+ if (evt && typeof evt.eventId === "string") {
147
+ inserted += 1;
148
+ tailEventId = evt.eventId;
149
+ }
150
+ }
151
+ }
152
+ if (inserted === 0) return;
153
+
154
+ eventsSinceLastCheckpoint += inserted;
155
+ if (tailEventId !== null) lastEventId = tailEventId;
156
+
157
+ if (fired) {
158
+ // Suppress until caller acknowledges with markCheckpointed().
159
+ // Keep counter growing for accuracy on next cycle though.
160
+ return;
161
+ }
162
+
163
+ if (
164
+ eventCountThreshold !== undefined &&
165
+ eventsSinceLastCheckpoint >= eventCountThreshold &&
166
+ lastEventId !== null
167
+ ) {
168
+ fired = true;
169
+ clearIdleTimer();
170
+ try {
171
+ onTrigger({ reason: "event-count", afterEventId: lastEventId });
172
+ } catch {
173
+ // Caller errors isolated.
174
+ }
175
+ return;
176
+ }
177
+
178
+ // New activity: reset the idle window.
179
+ armIdleTimer();
180
+ }
181
+
182
+ if (!disabled) {
183
+ yEvents.observe(onYEventsChange);
184
+ }
185
+
186
+ // `now` is accepted for test determinism; the scheduler doesn't read
187
+ // it today but keeping the option lets future slices (e.g., per-peer
188
+ // backoff) use a shared clock without a signature change.
189
+ void now;
190
+
191
+ return {
192
+ markCheckpointed() {
193
+ eventsSinceLastCheckpoint = 0;
194
+ fired = false;
195
+ clearIdleTimer();
196
+ },
197
+ destroy() {
198
+ if (destroyed) return;
199
+ destroyed = true;
200
+ if (!disabled) yEvents.unobserve(onYEventsChange);
201
+ clearIdleTimer();
202
+ },
203
+ };
204
+ }
@@ -0,0 +1,115 @@
1
+ import * as Y from "yjs";
2
+
3
+ import type { CanonicalDocumentEnvelope } from "../../core/state/editor-state.ts";
4
+
5
+ /**
6
+ * A single snapshot of the canonical document at a specific point in the
7
+ * broadcast event log. Stored in `Y.Array<Checkpoint>("checkpoints")` so
8
+ * late joiners can apply the latest checkpoint instead of replaying the
9
+ * full command history.
10
+ *
11
+ * Contract:
12
+ * - `afterEventId` is the `eventId` of the last `CommandEvent` included
13
+ * in the snapshot. Joiners replay broadcast events whose position in
14
+ * the shared `Y.Array<CommandEvent>` comes AFTER the event with this
15
+ * id. If the id is not found (e.g. out-of-order Yjs merge), joiners
16
+ * fall back to replaying from the start — the runtime's existing
17
+ * `appliedEventIds` dedup makes that safe but wasteful.
18
+ * - `documentAtCheckpoint` is a full `CanonicalDocumentEnvelope`.
19
+ * Passing it to `createDocumentRuntime({ initialCanonicalDocument })`
20
+ * produces a runtime that matches the author's state at that event.
21
+ * - `authorClientId` identifies which peer produced the checkpoint
22
+ * (single author is elected deterministically — see
23
+ * `checkpoint-scheduler.ts` in a later slice).
24
+ */
25
+ export interface Checkpoint {
26
+ checkpointId: string;
27
+ createdAt: string;
28
+ afterEventId: string;
29
+ documentAtCheckpoint: CanonicalDocumentEnvelope;
30
+ authorClientId: number;
31
+ }
32
+
33
+ const CHECKPOINTS_KEY = "checkpoints";
34
+
35
+ export interface CreateCheckpointStoreOptions {
36
+ ydoc: Y.Doc;
37
+ }
38
+
39
+ export interface CheckpointStoreHandle {
40
+ /** Append a new checkpoint to the shared log. */
41
+ append(checkpoint: Checkpoint): void;
42
+ /** Most recent checkpoint, or `undefined` when the log is empty. */
43
+ latest(): Checkpoint | undefined;
44
+ /** All checkpoints in insertion order. */
45
+ all(): Checkpoint[];
46
+ /**
47
+ * Subscribe to post-insert notifications. The listener receives the
48
+ * current full list (cheap copy) on every change. Returns an
49
+ * unsubscribe function.
50
+ */
51
+ subscribe(listener: (checkpoints: Checkpoint[]) => void): () => void;
52
+ /** Detach the underlying observer. Safe to call multiple times. */
53
+ destroy(): void;
54
+ }
55
+
56
+ /**
57
+ * Creates a thin adapter over `ydoc.getArray<Checkpoint>("checkpoints")`.
58
+ * The array is the canonical list — multiple stores on the same Y.Doc
59
+ * share state via Yjs, and peers on the network see every append.
60
+ *
61
+ * Writers should elect a single author (see
62
+ * `createCheckpointScheduler` + election slice) to avoid duplicate
63
+ * snapshots. Readers can always open a store and call `latest()`; the
64
+ * adapter is cheap (no internal cache beyond the Y.Array).
65
+ */
66
+ export function createCheckpointStore(
67
+ options: CreateCheckpointStoreOptions,
68
+ ): CheckpointStoreHandle {
69
+ const { ydoc } = options;
70
+ const yCheckpoints = ydoc.getArray<Checkpoint>(CHECKPOINTS_KEY);
71
+
72
+ const listeners = new Set<(checkpoints: Checkpoint[]) => void>();
73
+ let destroyed = false;
74
+
75
+ function onChange(): void {
76
+ if (destroyed) return;
77
+ if (listeners.size === 0) return;
78
+ const snapshot = yCheckpoints.toArray() as Checkpoint[];
79
+ for (const listener of [...listeners]) {
80
+ try {
81
+ listener(snapshot);
82
+ } catch {
83
+ // Listener exceptions are isolated; the store continues.
84
+ }
85
+ }
86
+ }
87
+ yCheckpoints.observe(onChange);
88
+
89
+ return {
90
+ append(checkpoint) {
91
+ if (destroyed) return;
92
+ yCheckpoints.push([checkpoint]);
93
+ },
94
+ latest() {
95
+ const length = yCheckpoints.length;
96
+ if (length === 0) return undefined;
97
+ return yCheckpoints.get(length - 1) as Checkpoint;
98
+ },
99
+ all() {
100
+ return yCheckpoints.toArray() as Checkpoint[];
101
+ },
102
+ subscribe(listener) {
103
+ listeners.add(listener);
104
+ return () => {
105
+ listeners.delete(listener);
106
+ };
107
+ },
108
+ destroy() {
109
+ if (destroyed) return;
110
+ destroyed = true;
111
+ yCheckpoints.unobserve(onChange);
112
+ listeners.clear();
113
+ },
114
+ };
115
+ }
@@ -28,9 +28,35 @@ import type { EditorStoryTarget } from "../../api/public-types.ts";
28
28
  * current canonical state — see `applyDocumentPatch` in
29
29
  * `core/commands/index.ts`.
30
30
  */
31
+ /**
32
+ * Wire version of a {@link CommandEvent}. Bump to `2` on any breaking
33
+ * change to the envelope shape (renames, reorderings, or removed fields).
34
+ *
35
+ * **Versioning policy** — strict equality, not semver. Peers that see an
36
+ * event with a `schemaVersion !== COMMAND_EVENT_SCHEMA_VERSION` skip the
37
+ * event and emit `collab_event_schema_mismatch` on the collab-sync handle
38
+ * so the host can decide whether to prompt a reload. Additive changes
39
+ * (new optional `context` fields, new broadcast command types) do NOT
40
+ * bump this number; peers preserve unknown fields on decode and ignore
41
+ * unknown command types via the existing `BROADCAST_COMMAND_TYPES`
42
+ * classifier.
43
+ *
44
+ * Legacy events written before this field existed decode as `1` via the
45
+ * decoder fallback in `runtime-collab-sync.ts:asCommandEvent` — they
46
+ * continue to replay cleanly on upgraded clients.
47
+ */
48
+ export const COMMAND_EVENT_SCHEMA_VERSION = 1 as const;
49
+
31
50
  export interface CommandEvent {
32
51
  /** UUID — dedup key for defensive idempotency. */
33
52
  eventId: string;
53
+ /**
54
+ * Wire version of this envelope. See {@link COMMAND_EVENT_SCHEMA_VERSION}
55
+ * for the policy. Every event minted by `createCommandEvent` carries
56
+ * the current version; legacy events without this field are defaulted
57
+ * to `1` at decode time.
58
+ */
59
+ schemaVersion: 1;
34
60
  /** `Y.Doc.clientID` of the originating client. */
35
61
  originClientId: number;
36
62
  /** User identity for attribution (e.g. `currentUser.userId`). */
@@ -169,6 +195,7 @@ export interface CreateCommandEventInput {
169
195
  export function createCommandEvent(input: CreateCommandEventInput): CommandEvent {
170
196
  return {
171
197
  eventId: generateEventId(),
198
+ schemaVersion: COMMAND_EVENT_SCHEMA_VERSION,
172
199
  originClientId: input.originClientId,
173
200
  authorId: input.authorId,
174
201
  timestamp: input.timestamp,
@@ -1,12 +1,30 @@
1
1
  export {
2
2
  createRuntimeCollabSync,
3
3
  createRuntimeCommandAppliedBridge,
4
+ type RuntimeCollabSyncEvent,
4
5
  type RuntimeCollabSyncHandle,
5
6
  type RuntimeCollabSyncOptions,
6
7
  type RuntimeCommandAppliedBridge,
7
8
  type RuntimeCommandAppliedListener,
8
9
  } from "./runtime-collab-sync.ts";
9
10
  export {
11
+ createCheckpointStore,
12
+ type Checkpoint,
13
+ type CheckpointStoreHandle,
14
+ type CreateCheckpointStoreOptions,
15
+ } from "./checkpoint-store.ts";
16
+ export {
17
+ createCheckpointScheduler,
18
+ type CheckpointSchedulerHandle,
19
+ type CheckpointTriggerContext,
20
+ type CreateCheckpointSchedulerOptions,
21
+ } from "./checkpoint-scheduler.ts";
22
+ export {
23
+ isElectedCheckpointAuthor,
24
+ subscribeElectedCheckpointAuthor,
25
+ } from "./checkpoint-election.ts";
26
+ export {
27
+ COMMAND_EVENT_SCHEMA_VERSION,
10
28
  createCommandEvent,
11
29
  isBroadcastCommand,
12
30
  isLocalOnlyCommand,
@@ -15,8 +33,12 @@ export {
15
33
  } from "./event-types.ts";
16
34
  export {
17
35
  clearLocalCursorState,
36
+ createRemoteCursorTracker,
18
37
  getCursorColorForUser,
19
38
  getRemoteCursorStates,
39
+ mapRemoteCursorThroughMapping,
20
40
  setLocalCursorState,
21
41
  type RemoteCursorState,
42
+ type RemoteCursorTrackerHandle,
43
+ type RemoteCursorTrackerOptions,
22
44
  } from "./remote-cursor-awareness.ts";