@doxi/collab 0.11.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mahbub Hasan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # @doxi/collab
2
+
3
+ Real-time collaboration substrate for Doxiva. Step-based concurrency control with per-peer Lamport ordering, pluggable transports, and presence cursors.
4
+
5
+ > **Status:** v0.10.x preview. v0.6.0 ships sequential + disjoint-position convergence. Same-position concurrent inserts are a known limitation — see below.
6
+
7
+ ## What it does
8
+
9
+ - **`CollabEngine`** — wraps a local `EditorView` so every dispatched `Transaction` is mirrored to remote peers, and inbound updates are rebased and applied locally. Per-peer Lamport clock + monotonic local version. Concurrency uses Mapping-based rebase of pending local updates against inbound steps (the ProseMirror-collab model).
10
+ - **`Transport` interface** — pluggable channel for `CollabUpdate` + `RemoteCursor` messages. Two implementations ship:
11
+ - **`InMemoryBroker`** (`@doxi/collab/transports/in-memory`) — in-process fan-out, primarily for tests.
12
+ - **`BroadcastChannelTransport`** (`@doxi/collab/transports/broadcast`) — same-origin tab sync via the browser `BroadcastChannel` API. No server, no auth, no persistence.
13
+ - **`RemoteCursorTracker` + `installRemoteCursors(view, engine)`** — presence cursors painted as a direct-DOM overlay anchored to the view. Caret + peer label, color per peer.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ pnpm add @doxi/collab @doxi/core
19
+ ```
20
+
21
+ ## Quickstart
22
+
23
+ ```ts
24
+ import { CollabEngine, installRemoteCursors } from '@doxi/collab'
25
+ import { BroadcastChannelTransport } from '@doxi/collab/transports/broadcast'
26
+
27
+ const transport = new BroadcastChannelTransport('doxiva-demo')
28
+ const engine = new CollabEngine({
29
+ view,
30
+ transport,
31
+ peer: { id: crypto.randomUUID(), label: 'Alice', color: '#d93025' },
32
+ })
33
+ const uninstall = installRemoteCursors(view, engine)
34
+
35
+ // Later: uninstall(); engine.dispose(); transport.dispose()
36
+ ```
37
+
38
+ ## Known limitations
39
+
40
+ - **Same-position concurrent inserts may diverge in ordering.** v0.6 is step-based, not character-CRDT. A Logoot/RGA fractional-index variant is on the roadmap.
41
+ - **No authoritative server / replay log.** Peers joining late don't see prior history. v0.6 is peer-to-peer only via `BroadcastChannel` (same-origin tabs).
42
+ - **No WebSocket transport in-tree.** The transport interface is open; consumers wire their own. WebSocket adapter is planned.
43
+ - **No RBAC, comments, suggestions, track-changes** — these need their own model layer.
44
+
45
+ See [spec §7.2](../../docs/internal/specs/2026-05-23-doxiva-editor-design.md) for the full design and convergence model.
46
+
47
+ ## Tests
48
+
49
+ 400 fast-check cases per scenario family across `convergence.prop.test.ts` (sequential + disjoint-position + cursor-only). Arbitrary async schedules are a v0.6.x follow-up reserved by an `it.todo`.
50
+
51
+ ## License
52
+
53
+ [MIT](LICENSE)
@@ -0,0 +1,22 @@
1
+ import type { ClientId, RemoteCursor } from './types.js';
2
+ /**
3
+ * Tracks remote cursors per peer. CollabEngine's onCursor handler updates
4
+ * this; the view-side renderer (installRemoteCursors) reads via the public
5
+ * `all()` and re-renders on `subscribe()` notifications.
6
+ *
7
+ * Stale cursors (older than `maxAgeMs` since `lastSeen`) are pruned on
8
+ * demand via pruneStale(); the engine does NOT prune automatically in
9
+ * v0.6.0 — callers decide the policy.
10
+ */
11
+ export declare class RemoteCursorTracker {
12
+ private cursors;
13
+ private listeners;
14
+ set(cursor: RemoteCursor): void;
15
+ remove(clientId: ClientId): void;
16
+ pruneStale(now: number, maxAgeMs: number): number;
17
+ all(): ReadonlyArray<RemoteCursor>;
18
+ size(): number;
19
+ subscribe(listener: () => void): () => void;
20
+ private notify;
21
+ }
22
+ //# sourceMappingURL=cursors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cursors.d.ts","sourceRoot":"","sources":["../src/cursors.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAExD;;;;;;;;GAQG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,OAAO,CAAoC;IACnD,OAAO,CAAC,SAAS,CAAwB;IAEzC,GAAG,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI;IAK/B,MAAM,CAAC,QAAQ,EAAE,QAAQ,GAAG,IAAI;IAIhC,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM;IAYjD,GAAG,IAAI,aAAa,CAAC,YAAY,CAAC;IAIlC,IAAI,IAAI,MAAM;IAId,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAO3C,OAAO,CAAC,MAAM;CAGf"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Tracks remote cursors per peer. CollabEngine's onCursor handler updates
3
+ * this; the view-side renderer (installRemoteCursors) reads via the public
4
+ * `all()` and re-renders on `subscribe()` notifications.
5
+ *
6
+ * Stale cursors (older than `maxAgeMs` since `lastSeen`) are pruned on
7
+ * demand via pruneStale(); the engine does NOT prune automatically in
8
+ * v0.6.0 — callers decide the policy.
9
+ */
10
+ export class RemoteCursorTracker {
11
+ cursors = new Map();
12
+ listeners = new Set();
13
+ set(cursor) {
14
+ this.cursors.set(cursor.clientId, cursor);
15
+ this.notify();
16
+ }
17
+ remove(clientId) {
18
+ if (this.cursors.delete(clientId))
19
+ this.notify();
20
+ }
21
+ pruneStale(now, maxAgeMs) {
22
+ let removed = 0;
23
+ for (const [id, c] of this.cursors) {
24
+ if (now - c.lastSeen > maxAgeMs) {
25
+ this.cursors.delete(id);
26
+ removed++;
27
+ }
28
+ }
29
+ if (removed > 0)
30
+ this.notify();
31
+ return removed;
32
+ }
33
+ all() {
34
+ return Array.from(this.cursors.values());
35
+ }
36
+ size() {
37
+ return this.cursors.size;
38
+ }
39
+ subscribe(listener) {
40
+ this.listeners.add(listener);
41
+ return () => {
42
+ this.listeners.delete(listener);
43
+ };
44
+ }
45
+ notify() {
46
+ for (const l of this.listeners)
47
+ l();
48
+ }
49
+ }
50
+ //# sourceMappingURL=cursors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cursors.js","sourceRoot":"","sources":["../src/cursors.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,MAAM,OAAO,mBAAmB;IACtB,OAAO,GAAG,IAAI,GAAG,EAA0B,CAAA;IAC3C,SAAS,GAAG,IAAI,GAAG,EAAc,CAAA;IAEzC,GAAG,CAAC,MAAoB;QACtB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAA;QACzC,IAAI,CAAC,MAAM,EAAE,CAAA;IACf,CAAC;IAED,MAAM,CAAC,QAAkB;QACvB,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC;YAAE,IAAI,CAAC,MAAM,EAAE,CAAA;IAClD,CAAC;IAED,UAAU,CAAC,GAAW,EAAE,QAAgB;QACtC,IAAI,OAAO,GAAG,CAAC,CAAA;QACf,KAAK,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACnC,IAAI,GAAG,GAAG,CAAC,CAAC,QAAQ,GAAG,QAAQ,EAAE,CAAC;gBAChC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;gBACvB,OAAO,EAAE,CAAA;YACX,CAAC;QACH,CAAC;QACD,IAAI,OAAO,GAAG,CAAC;YAAE,IAAI,CAAC,MAAM,EAAE,CAAA;QAC9B,OAAO,OAAO,CAAA;IAChB,CAAC;IAED,GAAG;QACD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;IAC1C,CAAC;IAED,IAAI;QACF,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAA;IAC1B,CAAC;IAED,SAAS,CAAC,QAAoB;QAC5B,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;QAC5B,OAAO,GAAG,EAAE;YACV,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;QACjC,CAAC,CAAA;IACH,CAAC;IAEO,MAAM;QACZ,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,SAAS;YAAE,CAAC,EAAE,CAAA;IACrC,CAAC;CACF"}
@@ -0,0 +1,87 @@
1
+ import { type EditorView } from '@doxi/core';
2
+ import { RemoteCursorTracker } from './cursors.js';
3
+ import type { Transport } from './transport.js';
4
+ import type { RemoteCursor, RemoteCursorSelection } from './types.js';
5
+ export interface CollabEngineOptions {
6
+ readonly clientId: string;
7
+ readonly transport: Transport;
8
+ readonly cursorLabel?: string;
9
+ readonly cursorColor?: string;
10
+ }
11
+ export interface CollabEngineSnapshot {
12
+ readonly clientId: string;
13
+ readonly clock: number;
14
+ readonly localUpdatesCount: number;
15
+ readonly remoteCursors: ReadonlyArray<RemoteCursor>;
16
+ }
17
+ /**
18
+ * CollabEngine wires a local EditorView into a Transport so every dispatched
19
+ * Transaction is mirrored to remote peers, and inbound CollabUpdates are
20
+ * rebased and applied locally.
21
+ *
22
+ * Concurrency model (v0.6.0):
23
+ * - Each peer maintains a Lamport clock and a per-peer monotonic
24
+ * `localVersion` (count of confirmed local updates).
25
+ * - On local dispatch the engine increments localVersion, broadcasts
26
+ * the steps tagged with the pre-dispatch baseVersion, and records
27
+ * the mapRanges of the dispatched steps so future inbound updates
28
+ * can be rebased through them.
29
+ * - On inbound update the engine builds a Mapping from every local
30
+ * update applied AFTER the inbound's baseVersion (i.e. the local
31
+ * work the remote peer hadn't seen) and maps the inbound steps
32
+ * through it. The mapped steps are applied as a single Transaction.
33
+ *
34
+ * Presence (v0.6.0):
35
+ * - Every committed local Transaction (whether it carries steps or is
36
+ * selection-only) publishes a RemoteCursor with the post-transaction
37
+ * selection. The view-side tracker (cursorTracker) stores the latest
38
+ * cursor per peer and notifies listeners; installRemoteCursors paints
39
+ * the overlay.
40
+ *
41
+ * This is NOT a fully convergent CRDT — see README for the schedules where
42
+ * peers can diverge.
43
+ */
44
+ export declare class CollabEngine {
45
+ readonly options: CollabEngineOptions;
46
+ private clock;
47
+ private localVersion;
48
+ /** Per-local-update list of mapRanges (used for rebasing inbound updates). */
49
+ private localUpdateMappings;
50
+ private readonly tracker;
51
+ private applyingRemote;
52
+ private view;
53
+ private originalDispatch;
54
+ private offUpdate;
55
+ private offCursor;
56
+ private disposed;
57
+ private lastPublishedSelectionKey;
58
+ constructor(options: CollabEngineOptions);
59
+ /** Attach to a view. Returns a cleanup fn. */
60
+ attach(view: EditorView): () => void;
61
+ /** Read-only snapshot for tests / debug. */
62
+ state(): CollabEngineSnapshot;
63
+ /**
64
+ * The tracker that holds the latest RemoteCursor per peer. Use
65
+ * `tracker.subscribe(...)` to repaint a presence overlay when peers
66
+ * move; `tracker.all()` returns the current set.
67
+ */
68
+ get cursorTracker(): RemoteCursorTracker;
69
+ /** Broadcast the local cursor selection to remote peers. */
70
+ publishLocalCursor(selection: RemoteCursorSelection): void;
71
+ dispose(): void;
72
+ private captureLocal;
73
+ /**
74
+ * Publish the current view selection if it has changed since the last
75
+ * publish. De-duped on a `type:anchor:head` key so a content-only edit
76
+ * that doesn't move the caret still publishes once (because positions
77
+ * may have remapped) — see the key reset in captureLocal-driven paths.
78
+ *
79
+ * Selection-only transactions (no steps) reach here via the dispatch
80
+ * wrapper and update the published cursor without producing a
81
+ * CollabUpdate.
82
+ */
83
+ private maybePublishLocalCursor;
84
+ private onRemoteUpdate;
85
+ private onRemoteCursor;
86
+ }
87
+ //# sourceMappingURL=engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,UAAU,EAIhB,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AAClD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC/C,OAAO,KAAK,EAAyB,YAAY,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAA;AAE5F,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAA;IAC7B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAC9B;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAA;IAClC,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC,YAAY,CAAC,CAAA;CACpD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,qBAAa,YAAY;IAcX,QAAQ,CAAC,OAAO,EAAE,mBAAmB;IAbjD,OAAO,CAAC,KAAK,CAAI;IACjB,OAAO,CAAC,YAAY,CAAI;IACxB,8EAA8E;IAC9E,OAAO,CAAC,mBAAmB,CAAiE;IAC5F,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4B;IACpD,OAAO,CAAC,cAAc,CAAQ;IAC9B,OAAO,CAAC,IAAI,CAA0B;IACtC,OAAO,CAAC,gBAAgB,CAA2C;IACnE,OAAO,CAAC,SAAS,CAA4B;IAC7C,OAAO,CAAC,SAAS,CAA4B;IAC7C,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,yBAAyB,CAAsB;gBAElC,OAAO,EAAE,mBAAmB;IAEjD,8CAA8C;IAC9C,MAAM,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,IAAI;IA8BpC,4CAA4C;IAC5C,KAAK,IAAI,oBAAoB;IAS7B;;;;OAIG;IACH,IAAI,aAAa,IAAI,mBAAmB,CAEvC;IAED,4DAA4D;IAC5D,kBAAkB,CAAC,SAAS,EAAE,qBAAqB,GAAG,IAAI;IAY1D,OAAO,IAAI,IAAI;IAgBf,OAAO,CAAC,YAAY;IAkBpB;;;;;;;;;OASG;IACH,OAAO,CAAC,uBAAuB;IAY/B,OAAO,CAAC,cAAc;IA0DtB,OAAO,CAAC,cAAc;CAKvB"}
package/dist/engine.js ADDED
@@ -0,0 +1,230 @@
1
+ import { Mapping, stepFromJSON, Transaction, } from '@doxi/core';
2
+ import { RemoteCursorTracker } from './cursors.js';
3
+ /**
4
+ * CollabEngine wires a local EditorView into a Transport so every dispatched
5
+ * Transaction is mirrored to remote peers, and inbound CollabUpdates are
6
+ * rebased and applied locally.
7
+ *
8
+ * Concurrency model (v0.6.0):
9
+ * - Each peer maintains a Lamport clock and a per-peer monotonic
10
+ * `localVersion` (count of confirmed local updates).
11
+ * - On local dispatch the engine increments localVersion, broadcasts
12
+ * the steps tagged with the pre-dispatch baseVersion, and records
13
+ * the mapRanges of the dispatched steps so future inbound updates
14
+ * can be rebased through them.
15
+ * - On inbound update the engine builds a Mapping from every local
16
+ * update applied AFTER the inbound's baseVersion (i.e. the local
17
+ * work the remote peer hadn't seen) and maps the inbound steps
18
+ * through it. The mapped steps are applied as a single Transaction.
19
+ *
20
+ * Presence (v0.6.0):
21
+ * - Every committed local Transaction (whether it carries steps or is
22
+ * selection-only) publishes a RemoteCursor with the post-transaction
23
+ * selection. The view-side tracker (cursorTracker) stores the latest
24
+ * cursor per peer and notifies listeners; installRemoteCursors paints
25
+ * the overlay.
26
+ *
27
+ * This is NOT a fully convergent CRDT — see README for the schedules where
28
+ * peers can diverge.
29
+ */
30
+ export class CollabEngine {
31
+ options;
32
+ clock = 0;
33
+ localVersion = 0;
34
+ /** Per-local-update list of mapRanges (used for rebasing inbound updates). */
35
+ localUpdateMappings = [];
36
+ tracker = new RemoteCursorTracker();
37
+ applyingRemote = false;
38
+ view = null;
39
+ originalDispatch = null;
40
+ offUpdate = null;
41
+ offCursor = null;
42
+ disposed = false;
43
+ lastPublishedSelectionKey = null;
44
+ constructor(options) {
45
+ this.options = options;
46
+ }
47
+ /** Attach to a view. Returns a cleanup fn. */
48
+ attach(view) {
49
+ if (this.view)
50
+ throw new Error('CollabEngine.attach: already attached');
51
+ if (this.disposed)
52
+ throw new Error('CollabEngine.attach: engine disposed');
53
+ this.view = view;
54
+ // Wrap view.dispatch so we observe every local Transaction AFTER it
55
+ // has been committed by the host.
56
+ const original = view.dispatch.bind(view);
57
+ this.originalDispatch = original;
58
+ view.dispatch = (tr) => {
59
+ original(tr);
60
+ if (this.applyingRemote)
61
+ return;
62
+ this.captureLocal(tr);
63
+ // Force a cursor publish whenever a transaction carried steps — even
64
+ // if anchor/head stayed numerically constant (e.g., a cursor at the
65
+ // start of the doc while the user types further along). Remote peers
66
+ // re-render against the new doc and need a fresh ping. Selection-only
67
+ // transactions go through the deduped path.
68
+ this.maybePublishLocalCursor(tr.steps.length > 0);
69
+ };
70
+ this.offUpdate = this.options.transport.onUpdate((u) => this.onRemoteUpdate(u));
71
+ this.offCursor = this.options.transport.onCursor((c) => this.onRemoteCursor(c));
72
+ // Publish initial cursor so remote peers see this client on first attach.
73
+ this.maybePublishLocalCursor();
74
+ return () => this.dispose();
75
+ }
76
+ /** Read-only snapshot for tests / debug. */
77
+ state() {
78
+ return {
79
+ clientId: this.options.clientId,
80
+ clock: this.clock,
81
+ localUpdatesCount: this.localVersion,
82
+ remoteCursors: this.tracker.all(),
83
+ };
84
+ }
85
+ /**
86
+ * The tracker that holds the latest RemoteCursor per peer. Use
87
+ * `tracker.subscribe(...)` to repaint a presence overlay when peers
88
+ * move; `tracker.all()` returns the current set.
89
+ */
90
+ get cursorTracker() {
91
+ return this.tracker;
92
+ }
93
+ /** Broadcast the local cursor selection to remote peers. */
94
+ publishLocalCursor(selection) {
95
+ if (this.disposed)
96
+ return;
97
+ const cursor = {
98
+ clientId: this.options.clientId,
99
+ selection,
100
+ lastSeen: Date.now(),
101
+ ...(this.options.cursorLabel !== undefined ? { label: this.options.cursorLabel } : {}),
102
+ ...(this.options.cursorColor !== undefined ? { color: this.options.cursorColor } : {}),
103
+ };
104
+ this.options.transport.publishCursor(cursor);
105
+ }
106
+ dispose() {
107
+ if (this.disposed)
108
+ return;
109
+ this.disposed = true;
110
+ if (this.view && this.originalDispatch) {
111
+ this.view.dispatch = this.originalDispatch;
112
+ }
113
+ this.view = null;
114
+ this.originalDispatch = null;
115
+ this.offUpdate?.();
116
+ this.offCursor?.();
117
+ this.offUpdate = null;
118
+ this.offCursor = null;
119
+ }
120
+ // ---- internals ----------------------------------------------------------
121
+ captureLocal(tr) {
122
+ if (tr.steps.length === 0)
123
+ return; // selection-only updates don't propagate
124
+ const baseVersion = this.localVersion;
125
+ const nextVersion = baseVersion + 1;
126
+ const lamport = { clock: this.clock, clientId: this.options.clientId };
127
+ const stepJSONs = tr.steps.map((s) => s.toJSON());
128
+ const update = {
129
+ clientId: this.options.clientId,
130
+ baseVersion,
131
+ lamport,
132
+ steps: stepJSONs,
133
+ };
134
+ this.localVersion = nextVersion;
135
+ this.clock = this.clock + 1;
136
+ this.localUpdateMappings.push({ baseVersion, ranges: tr.mapping.ranges });
137
+ this.options.transport.publish(update);
138
+ }
139
+ /**
140
+ * Publish the current view selection if it has changed since the last
141
+ * publish. De-duped on a `type:anchor:head` key so a content-only edit
142
+ * that doesn't move the caret still publishes once (because positions
143
+ * may have remapped) — see the key reset in captureLocal-driven paths.
144
+ *
145
+ * Selection-only transactions (no steps) reach here via the dispatch
146
+ * wrapper and update the published cursor without producing a
147
+ * CollabUpdate.
148
+ */
149
+ maybePublishLocalCursor(force = false) {
150
+ if (this.disposed)
151
+ return;
152
+ const view = this.view;
153
+ if (!view)
154
+ return;
155
+ const sel = view.state.selection;
156
+ const json = sel.toJSON();
157
+ const key = `${json.type}:${json.anchor}:${json.head}`;
158
+ if (!force && key === this.lastPublishedSelectionKey)
159
+ return;
160
+ this.lastPublishedSelectionKey = key;
161
+ this.publishLocalCursor(json);
162
+ }
163
+ onRemoteUpdate(update) {
164
+ if (this.disposed)
165
+ return;
166
+ if (update.clientId === this.options.clientId)
167
+ return; // ignore own echoes
168
+ const view = this.view;
169
+ if (!view)
170
+ return;
171
+ // Build the rebase mapping: every local update applied AFTER the
172
+ // remote's baseVersion. The remote peer based its steps on a doc
173
+ // state where our local updates after baseVersion had NOT happened,
174
+ // so we map its step positions forward through them.
175
+ const rebaseRanges = [];
176
+ for (const entry of this.localUpdateMappings) {
177
+ if (entry.baseVersion >= update.baseVersion) {
178
+ rebaseRanges.push(...entry.ranges);
179
+ }
180
+ }
181
+ const rebase = rebaseRanges.length === 0 ? Mapping.empty : new Mapping(rebaseRanges);
182
+ const state = view.state;
183
+ const schema = state.schema;
184
+ const rehydrated = [];
185
+ for (const sjson of update.steps) {
186
+ const step = stepFromJSON(schema, sjson);
187
+ const mapped = rebase.ranges.length === 0 ? step : step.map(rebase);
188
+ if (mapped)
189
+ rehydrated.push(mapped);
190
+ }
191
+ if (rehydrated.length === 0) {
192
+ this.clock = Math.max(this.clock, update.lamport.clock) + 1;
193
+ return;
194
+ }
195
+ let tr = state.tr;
196
+ let ok = true;
197
+ for (const step of rehydrated) {
198
+ try {
199
+ tr = tr.step(step);
200
+ }
201
+ catch {
202
+ // A step failed to apply (e.g. position out of bounds after a
203
+ // concurrent delete swallowed its anchor). Drop the offending step
204
+ // and continue. v0.6.0 known limitation.
205
+ ok = false;
206
+ }
207
+ }
208
+ if (tr.steps.length === 0) {
209
+ this.clock = Math.max(this.clock, update.lamport.clock) + 1;
210
+ return;
211
+ }
212
+ void ok;
213
+ this.applyingRemote = true;
214
+ try {
215
+ view.dispatch(tr);
216
+ }
217
+ finally {
218
+ this.applyingRemote = false;
219
+ }
220
+ this.clock = Math.max(this.clock, update.lamport.clock) + 1;
221
+ }
222
+ onRemoteCursor(cursor) {
223
+ if (this.disposed)
224
+ return;
225
+ if (cursor.clientId === this.options.clientId)
226
+ return;
227
+ this.tracker.set(cursor);
228
+ }
229
+ }
230
+ //# sourceMappingURL=engine.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"engine.js","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ export { CollabEngine } from './engine.js';
2
+ export type { CollabEngineOptions, CollabEngineSnapshot } from './engine.js';
3
+ export { RemoteCursorTracker } from './cursors.js';
4
+ export { installRemoteCursors } from './install-remote-cursors.js';
5
+ export type { Transport } from './transport.js';
6
+ export type { ClientId, Lamport, CollabUpdate, RemoteCursor, RemoteCursorSelection, } from './types.js';
7
+ export { lamportLessThan } from './types.js';
8
+ export { InMemoryBroker } from './transports/in-memory.js';
9
+ export { BroadcastChannelTransport } from './transports/broadcast.js';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,YAAY,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAC5E,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AAClD,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAA;AAClE,YAAY,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC/C,YAAY,EACV,QAAQ,EACR,OAAO,EACP,YAAY,EACZ,YAAY,EACZ,qBAAqB,GACtB,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAA;AAC1D,OAAO,EAAE,yBAAyB,EAAE,MAAM,2BAA2B,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { CollabEngine } from './engine.js';
2
+ export { RemoteCursorTracker } from './cursors.js';
3
+ export { installRemoteCursors } from './install-remote-cursors.js';
4
+ export { lamportLessThan } from './types.js';
5
+ export { InMemoryBroker } from './transports/in-memory.js';
6
+ export { BroadcastChannelTransport } from './transports/broadcast.js';
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAE1C,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AAClD,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAA;AASlE,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAC5C,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAA;AAC1D,OAAO,EAAE,yBAAyB,EAAE,MAAM,2BAA2B,CAAA"}
@@ -0,0 +1,24 @@
1
+ import { type EditorView } from '@doxi/core';
2
+ import type { CollabEngine } from './engine.js';
3
+ /**
4
+ * Mount a remote-cursors overlay inside `view.dom`. The overlay paints a
5
+ * thin caret + label for every remote peer's cursor in the engine's
6
+ * tracker.
7
+ *
8
+ * v0.6.0 "direct-DOM" approach: positions are resolved from model →
9
+ * DOM via the core's `locateDomFromModelPos` helper, then pixel-positioned
10
+ * with a transient Range + getBoundingClientRect. The overlay is an
11
+ * absolutely positioned `<div class="dx-remote-cursors-layer">` inside
12
+ * `view.dom` (whose position is forced to `relative` if necessary).
13
+ *
14
+ * Repaint policy: while at least one remote cursor exists, repaint every
15
+ * animation frame (~60Hz). This is intentionally simple — it covers
16
+ * local edits that shift remote-cursor positions and reflows from window
17
+ * resizes without wiring into the view's internal lifecycle. Idle when
18
+ * the tracker is empty.
19
+ *
20
+ * Future v0.7+ can migrate to a decorations-based renderer; the on-wire
21
+ * RemoteCursor shape stays the same.
22
+ */
23
+ export declare function installRemoteCursors(view: EditorView, engine: CollabEngine): () => void;
24
+ //# sourceMappingURL=install-remote-cursors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install-remote-cursors.d.ts","sourceRoot":"","sources":["../src/install-remote-cursors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAyB,KAAK,UAAU,EAAE,MAAM,YAAY,CAAA;AACnE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAK/C;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,YAAY,GAAG,MAAM,IAAI,CA8EvF"}
@@ -0,0 +1,153 @@
1
+ import { locateDomFromModelPos } from '@doxi/core';
2
+ const DEFAULT_COLOR = '#1a73e8';
3
+ /**
4
+ * Mount a remote-cursors overlay inside `view.dom`. The overlay paints a
5
+ * thin caret + label for every remote peer's cursor in the engine's
6
+ * tracker.
7
+ *
8
+ * v0.6.0 "direct-DOM" approach: positions are resolved from model →
9
+ * DOM via the core's `locateDomFromModelPos` helper, then pixel-positioned
10
+ * with a transient Range + getBoundingClientRect. The overlay is an
11
+ * absolutely positioned `<div class="dx-remote-cursors-layer">` inside
12
+ * `view.dom` (whose position is forced to `relative` if necessary).
13
+ *
14
+ * Repaint policy: while at least one remote cursor exists, repaint every
15
+ * animation frame (~60Hz). This is intentionally simple — it covers
16
+ * local edits that shift remote-cursor positions and reflows from window
17
+ * resizes without wiring into the view's internal lifecycle. Idle when
18
+ * the tracker is empty.
19
+ *
20
+ * Future v0.7+ can migrate to a decorations-based renderer; the on-wire
21
+ * RemoteCursor shape stays the same.
22
+ */
23
+ export function installRemoteCursors(view, engine) {
24
+ const doc = view.dom.ownerDocument;
25
+ if (!doc)
26
+ return () => { };
27
+ const win = doc.defaultView;
28
+ // Ensure the view.dom is a positioning context. Only override if it is
29
+ // currently `static` so we don't fight a host that already set
30
+ // position:relative/absolute on the editor element.
31
+ const computed = win?.getComputedStyle(view.dom).position;
32
+ if (!computed || computed === 'static') {
33
+ view.dom.style.position = 'relative';
34
+ }
35
+ const layer = doc.createElement('div');
36
+ layer.className = 'dx-remote-cursors-layer';
37
+ // Inline styles as a defensive fallback: if the host page hasn't loaded
38
+ // the stylesheet, the overlay still positions correctly.
39
+ layer.style.position = 'absolute';
40
+ layer.style.top = '0';
41
+ layer.style.left = '0';
42
+ layer.style.right = '0';
43
+ layer.style.bottom = '0';
44
+ layer.style.pointerEvents = 'none';
45
+ view.dom.appendChild(layer);
46
+ let rafHandle = null;
47
+ let dirty = false;
48
+ let disposed = false;
49
+ const schedule = () => {
50
+ if (disposed)
51
+ return;
52
+ if (!win)
53
+ return;
54
+ dirty = true;
55
+ if (rafHandle !== null)
56
+ return;
57
+ rafHandle = win.requestAnimationFrame(tick);
58
+ };
59
+ const tick = () => {
60
+ rafHandle = null;
61
+ if (disposed)
62
+ return;
63
+ if (!dirty)
64
+ return;
65
+ dirty = false;
66
+ repaint();
67
+ // Keep ticking while cursors are present so positions follow local
68
+ // edits and layout changes. Idle when empty.
69
+ if (engine.cursorTracker.size() > 0 && win) {
70
+ rafHandle = win.requestAnimationFrame(tick);
71
+ dirty = true;
72
+ }
73
+ };
74
+ const repaint = () => {
75
+ // Clear the layer.
76
+ while (layer.firstChild)
77
+ layer.removeChild(layer.firstChild);
78
+ const cursors = engine.cursorTracker.all();
79
+ if (cursors.length === 0)
80
+ return;
81
+ const viewRect = view.dom.getBoundingClientRect();
82
+ for (const cursor of cursors) {
83
+ paintCursor(doc, layer, view, cursor, viewRect);
84
+ }
85
+ };
86
+ const unsubscribe = engine.cursorTracker.subscribe(schedule);
87
+ // Initial paint, in case cursors are already present at install time.
88
+ schedule();
89
+ return () => {
90
+ if (disposed)
91
+ return;
92
+ disposed = true;
93
+ unsubscribe();
94
+ if (rafHandle !== null && win) {
95
+ win.cancelAnimationFrame(rafHandle);
96
+ rafHandle = null;
97
+ }
98
+ if (layer.parentNode === view.dom) {
99
+ view.dom.removeChild(layer);
100
+ }
101
+ };
102
+ }
103
+ function paintCursor(doc, layer, view, cursor, viewRect) {
104
+ const headPos = cursor.selection.head;
105
+ const loc = locateDomFromModelPos(view.dom, headPos, view.state.doc);
106
+ if (!loc)
107
+ return;
108
+ const range = doc.createRange();
109
+ try {
110
+ range.setStart(loc.node, loc.offset);
111
+ range.setEnd(loc.node, loc.offset);
112
+ }
113
+ catch {
114
+ return;
115
+ }
116
+ const rect = range.getBoundingClientRect();
117
+ // happy-dom and some browsers return a zero-rect for collapsed ranges
118
+ // on the start of an empty text node. Fall back to the bounding rect
119
+ // of the containing element.
120
+ let left = rect.left;
121
+ let top = rect.top;
122
+ let height = rect.height;
123
+ if (rect.width === 0 && rect.height === 0) {
124
+ const parent = loc.node.nodeType === 3 ? loc.node.parentElement : loc.node;
125
+ if (parent) {
126
+ const pr = parent.getBoundingClientRect();
127
+ left = pr.left;
128
+ top = pr.top;
129
+ height = pr.height;
130
+ }
131
+ }
132
+ if (!Number.isFinite(left) || !Number.isFinite(top))
133
+ return;
134
+ const x = left - viewRect.left;
135
+ const y = top - viewRect.top;
136
+ const caret = doc.createElement('span');
137
+ caret.className = 'dx-remote-cursor';
138
+ caret.dataset.clientId = cursor.clientId;
139
+ const color = cursor.color ?? DEFAULT_COLOR;
140
+ caret.style.setProperty('--dx-remote-color', color);
141
+ caret.style.position = 'absolute';
142
+ caret.style.left = `${x}px`;
143
+ caret.style.top = `${y}px`;
144
+ if (height > 0)
145
+ caret.style.height = `${height}px`;
146
+ const label = doc.createElement('span');
147
+ label.className = 'dx-remote-cursor-label';
148
+ label.textContent = cursor.label ?? cursor.clientId.slice(0, 6);
149
+ label.style.setProperty('--dx-remote-color', color);
150
+ caret.appendChild(label);
151
+ layer.appendChild(caret);
152
+ }
153
+ //# sourceMappingURL=install-remote-cursors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"install-remote-cursors.js","sourceRoot":"","sources":["../src/install-remote-cursors.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import type { CollabUpdate, RemoteCursor } from './types.js';
2
+ export interface Transport {
3
+ publish(update: CollabUpdate): void;
4
+ publishCursor(cursor: RemoteCursor): void;
5
+ onUpdate(handler: (update: CollabUpdate) => void): () => void;
6
+ onCursor(handler: (cursor: RemoteCursor) => void): () => void;
7
+ close(): void;
8
+ }
9
+ //# sourceMappingURL=transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAE5D,MAAM,WAAW,SAAS;IACxB,OAAO,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CAAA;IACnC,aAAa,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CAAA;IACzC,QAAQ,CAAC,OAAO,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,GAAG,MAAM,IAAI,CAAA;IAC7D,QAAQ,CAAC,OAAO,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,GAAG,MAAM,IAAI,CAAA;IAC7D,KAAK,IAAI,IAAI,CAAA;CACd"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":""}
@@ -0,0 +1,24 @@
1
+ import type { Transport } from '../transport.js';
2
+ import type { CollabUpdate, RemoteCursor } from '../types.js';
3
+ /**
4
+ * Transport over the same-origin BroadcastChannel API. Lets multiple browser
5
+ * tabs of the same app act as collaborating peers without a server.
6
+ *
7
+ * Note: BroadcastChannel is undefined in some test environments (e.g. older
8
+ * happy-dom builds). Consumers running in such environments must polyfill or
9
+ * skip BroadcastChannel-backed tests; coverage of this transport is provided
10
+ * by the v0.6 e2e two-tab demo.
11
+ */
12
+ export declare class BroadcastChannelTransport implements Transport {
13
+ readonly channelName: string;
14
+ private channel;
15
+ private updateHandlers;
16
+ private cursorHandlers;
17
+ constructor(channelName: string);
18
+ publish(update: CollabUpdate): void;
19
+ publishCursor(cursor: RemoteCursor): void;
20
+ onUpdate(h: (u: CollabUpdate) => void): () => void;
21
+ onCursor(h: (c: RemoteCursor) => void): () => void;
22
+ close(): void;
23
+ }
24
+ //# sourceMappingURL=broadcast.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"broadcast.d.ts","sourceRoot":"","sources":["../../src/transports/broadcast.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAChD,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAO7D;;;;;;;;GAQG;AACH,qBAAa,yBAA0B,YAAW,SAAS;IAK7C,QAAQ,CAAC,WAAW,EAAE,MAAM;IAJxC,OAAO,CAAC,OAAO,CAAkB;IACjC,OAAO,CAAC,cAAc,CAAuC;IAC7D,OAAO,CAAC,cAAc,CAAuC;gBAExC,WAAW,EAAE,MAAM;IAaxC,OAAO,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI;IAInC,aAAa,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI;IAIzC,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,YAAY,KAAK,IAAI,GAAG,MAAM,IAAI;IAOlD,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,YAAY,KAAK,IAAI,GAAG,MAAM,IAAI;IAOlD,KAAK,IAAI,IAAI;CAKd"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Transport over the same-origin BroadcastChannel API. Lets multiple browser
3
+ * tabs of the same app act as collaborating peers without a server.
4
+ *
5
+ * Note: BroadcastChannel is undefined in some test environments (e.g. older
6
+ * happy-dom builds). Consumers running in such environments must polyfill or
7
+ * skip BroadcastChannel-backed tests; coverage of this transport is provided
8
+ * by the v0.6 e2e two-tab demo.
9
+ */
10
+ export class BroadcastChannelTransport {
11
+ channelName;
12
+ channel;
13
+ updateHandlers = new Set();
14
+ cursorHandlers = new Set();
15
+ constructor(channelName) {
16
+ this.channelName = channelName;
17
+ this.channel = new BroadcastChannel(channelName);
18
+ this.channel.onmessage = (event) => {
19
+ const msg = event.data;
20
+ if (!msg || typeof msg !== 'object')
21
+ return;
22
+ if (msg.kind === 'update') {
23
+ for (const h of this.updateHandlers)
24
+ h(msg.payload);
25
+ }
26
+ else if (msg.kind === 'cursor') {
27
+ for (const h of this.cursorHandlers)
28
+ h(msg.payload);
29
+ }
30
+ };
31
+ }
32
+ publish(update) {
33
+ this.channel.postMessage({ kind: 'update', payload: update });
34
+ }
35
+ publishCursor(cursor) {
36
+ this.channel.postMessage({ kind: 'cursor', payload: cursor });
37
+ }
38
+ onUpdate(h) {
39
+ this.updateHandlers.add(h);
40
+ return () => {
41
+ this.updateHandlers.delete(h);
42
+ };
43
+ }
44
+ onCursor(h) {
45
+ this.cursorHandlers.add(h);
46
+ return () => {
47
+ this.cursorHandlers.delete(h);
48
+ };
49
+ }
50
+ close() {
51
+ this.channel.close();
52
+ this.updateHandlers.clear();
53
+ this.cursorHandlers.clear();
54
+ }
55
+ }
56
+ //# sourceMappingURL=broadcast.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"broadcast.js","sourceRoot":"","sources":["../../src/transports/broadcast.ts"],"names":[],"mappings":"AAQA;;;;;;;;GAQG;AACH,MAAM,OAAO,yBAAyB;IAKf;IAJb,OAAO,CAAkB;IACzB,cAAc,GAAG,IAAI,GAAG,EAA6B,CAAA;IACrD,cAAc,GAAG,IAAI,GAAG,EAA6B,CAAA;IAE7D,YAAqB,WAAmB;QAAnB,gBAAW,GAAX,WAAW,CAAQ;QACtC,IAAI,CAAC,OAAO,GAAG,IAAI,gBAAgB,CAAC,WAAW,CAAC,CAAA;QAChD,IAAI,CAAC,OAAO,CAAC,SAAS,GAAG,CAAC,KAAmB,EAAE,EAAE;YAC/C,MAAM,GAAG,GAAG,KAAK,CAAC,IAAyC,CAAA;YAC3D,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;gBAAE,OAAM;YAC3C,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBAC1B,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,cAAc;oBAAE,CAAC,CAAC,GAAG,CAAC,OAAuB,CAAC,CAAA;YACrE,CAAC;iBAAM,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACjC,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,cAAc;oBAAE,CAAC,CAAC,GAAG,CAAC,OAAuB,CAAC,CAAA;YACrE,CAAC;QACH,CAAC,CAAA;IACH,CAAC;IAED,OAAO,CAAC,MAAoB;QAC1B,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAA2B,CAAC,CAAA;IACxF,CAAC;IAED,aAAa,CAAC,MAAoB;QAChC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAA2B,CAAC,CAAA;IACxF,CAAC;IAED,QAAQ,CAAC,CAA4B;QACnC,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;QAC1B,OAAO,GAAG,EAAE;YACV,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;QAC/B,CAAC,CAAA;IACH,CAAC;IAED,QAAQ,CAAC,CAA4B;QACnC,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;QAC1B,OAAO,GAAG,EAAE;YACV,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;QAC/B,CAAC,CAAA;IACH,CAAC;IAED,KAAK;QACH,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAA;QACpB,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,CAAA;QAC3B,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,CAAA;IAC7B,CAAC;CACF"}
@@ -0,0 +1,14 @@
1
+ import type { Transport } from '../transport.js';
2
+ /**
3
+ * In-process broker that fans CollabUpdates and RemoteCursors out to every
4
+ * connected peer except the sender. Used for unit tests and same-process
5
+ * peer simulation.
6
+ */
7
+ export declare class InMemoryBroker {
8
+ private peers;
9
+ /** Create a Transport for a new peer connected to this broker. */
10
+ connect(): Transport;
11
+ /** Test helper: number of currently connected peers. */
12
+ get peerCount(): number;
13
+ }
14
+ //# sourceMappingURL=in-memory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"in-memory.d.ts","sourceRoot":"","sources":["../../src/transports/in-memory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAQhD;;;;GAIG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,KAAK,CAAwB;IAErC,kEAAkE;IAClE,OAAO,IAAI,SAAS;IA0CpB,wDAAwD;IACxD,IAAI,SAAS,IAAI,MAAM,CAEtB;CACF"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * In-process broker that fans CollabUpdates and RemoteCursors out to every
3
+ * connected peer except the sender. Used for unit tests and same-process
4
+ * peer simulation.
5
+ */
6
+ export class InMemoryBroker {
7
+ peers = new Set();
8
+ /** Create a Transport for a new peer connected to this broker. */
9
+ connect() {
10
+ const peer = {
11
+ onUpdateHandlers: new Set(),
12
+ onCursorHandlers: new Set(),
13
+ };
14
+ this.peers.add(peer);
15
+ let closed = false;
16
+ return {
17
+ publish: (update) => {
18
+ if (closed)
19
+ return;
20
+ for (const other of this.peers) {
21
+ if (other === peer)
22
+ continue;
23
+ for (const h of other.onUpdateHandlers)
24
+ h(update);
25
+ }
26
+ },
27
+ publishCursor: (cursor) => {
28
+ if (closed)
29
+ return;
30
+ for (const other of this.peers) {
31
+ if (other === peer)
32
+ continue;
33
+ for (const h of other.onCursorHandlers)
34
+ h(cursor);
35
+ }
36
+ },
37
+ onUpdate: (h) => {
38
+ peer.onUpdateHandlers.add(h);
39
+ return () => {
40
+ peer.onUpdateHandlers.delete(h);
41
+ };
42
+ },
43
+ onCursor: (h) => {
44
+ peer.onCursorHandlers.add(h);
45
+ return () => {
46
+ peer.onCursorHandlers.delete(h);
47
+ };
48
+ },
49
+ close: () => {
50
+ if (closed)
51
+ return;
52
+ closed = true;
53
+ this.peers.delete(peer);
54
+ },
55
+ };
56
+ }
57
+ /** Test helper: number of currently connected peers. */
58
+ get peerCount() {
59
+ return this.peers.size;
60
+ }
61
+ }
62
+ //# sourceMappingURL=in-memory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"in-memory.js","sourceRoot":"","sources":["../../src/transports/in-memory.ts"],"names":[],"mappings":"AAQA;;;;GAIG;AACH,MAAM,OAAO,cAAc;IACjB,KAAK,GAAG,IAAI,GAAG,EAAc,CAAA;IAErC,kEAAkE;IAClE,OAAO;QACL,MAAM,IAAI,GAAe;YACvB,gBAAgB,EAAE,IAAI,GAAG,EAAE;YAC3B,gBAAgB,EAAE,IAAI,GAAG,EAAE;SAC5B,CAAA;QACD,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QACpB,IAAI,MAAM,GAAG,KAAK,CAAA;QAClB,OAAO;YACL,OAAO,EAAE,CAAC,MAAM,EAAE,EAAE;gBAClB,IAAI,MAAM;oBAAE,OAAM;gBAClB,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;oBAC/B,IAAI,KAAK,KAAK,IAAI;wBAAE,SAAQ;oBAC5B,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,gBAAgB;wBAAE,CAAC,CAAC,MAAM,CAAC,CAAA;gBACnD,CAAC;YACH,CAAC;YACD,aAAa,EAAE,CAAC,MAAM,EAAE,EAAE;gBACxB,IAAI,MAAM;oBAAE,OAAM;gBAClB,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;oBAC/B,IAAI,KAAK,KAAK,IAAI;wBAAE,SAAQ;oBAC5B,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,gBAAgB;wBAAE,CAAC,CAAC,MAAM,CAAC,CAAA;gBACnD,CAAC;YACH,CAAC;YACD,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE;gBACd,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;gBAC5B,OAAO,GAAG,EAAE;oBACV,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;gBACjC,CAAC,CAAA;YACH,CAAC;YACD,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE;gBACd,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;gBAC5B,OAAO,GAAG,EAAE;oBACV,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAA;gBACjC,CAAC,CAAA;YACH,CAAC;YACD,KAAK,EAAE,GAAG,EAAE;gBACV,IAAI,MAAM;oBAAE,OAAM;gBAClB,MAAM,GAAG,IAAI,CAAA;gBACb,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;YACzB,CAAC;SACF,CAAA;IACH,CAAC;IAED,wDAAwD;IACxD,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAA;IACxB,CAAC;CACF"}
@@ -0,0 +1,26 @@
1
+ export type ClientId = string;
2
+ export interface Lamport {
3
+ readonly clock: number;
4
+ readonly clientId: ClientId;
5
+ }
6
+ export interface CollabUpdate {
7
+ readonly clientId: ClientId;
8
+ readonly baseVersion: number;
9
+ readonly lamport: Lamport;
10
+ readonly steps: ReadonlyArray<unknown>;
11
+ readonly source?: string;
12
+ }
13
+ export interface RemoteCursorSelection {
14
+ readonly type: string;
15
+ readonly anchor: number;
16
+ readonly head: number;
17
+ }
18
+ export interface RemoteCursor {
19
+ readonly clientId: ClientId;
20
+ readonly label?: string;
21
+ readonly color?: string;
22
+ readonly selection: RemoteCursorSelection;
23
+ readonly lastSeen: number;
24
+ }
25
+ export declare function lamportLessThan(a: Lamport, b: Lamport): boolean;
26
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAA;AAE7B,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAA;CAC5B;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAA;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,CAAA;IACtC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CACzB;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAA;IAC3B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,SAAS,EAAE,qBAAqB,CAAA;IACzC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAC1B;AAED,wBAAgB,eAAe,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,CAG/D"}
package/dist/types.js ADDED
@@ -0,0 +1,6 @@
1
+ export function lamportLessThan(a, b) {
2
+ if (a.clock !== b.clock)
3
+ return a.clock < b.clock;
4
+ return a.clientId < b.clientId;
5
+ }
6
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AA6BA,MAAM,UAAU,eAAe,CAAC,CAAU,EAAE,CAAU;IACpD,IAAI,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,KAAK;QAAE,OAAO,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAA;IACjD,OAAO,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,QAAQ,CAAA;AAChC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@doxi/collab",
3
+ "version": "0.11.0",
4
+ "description": "Doxiva collaboration engine — step-based CRDT and transports.",
5
+ "license": "MIT",
6
+ "author": "Mahbub Hasan",
7
+ "homepage": "https://github.com/mahbub-hasaan/doxiva#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/mahbub-hasaan/doxiva.git",
11
+ "directory": "packages/collab"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/mahbub-hasaan/doxiva/issues"
15
+ },
16
+ "keywords": ["collaboration", "crdt", "real-time", "doxiva"],
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "type": "module",
21
+ "sideEffects": false,
22
+ "files": ["dist", "README.md", "LICENSE"],
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/index.d.ts",
26
+ "import": "./dist/index.js"
27
+ },
28
+ "./transports/in-memory": {
29
+ "types": "./dist/transports/in-memory.d.ts",
30
+ "import": "./dist/transports/in-memory.js"
31
+ },
32
+ "./transports/broadcast": {
33
+ "types": "./dist/transports/broadcast.d.ts",
34
+ "import": "./dist/transports/broadcast.js"
35
+ }
36
+ },
37
+ "scripts": {
38
+ "build": "tsc -p tsconfig.json",
39
+ "typecheck": "tsc -p tsconfig.json --noEmit",
40
+ "test": "vitest run",
41
+ "test:watch": "vitest",
42
+ "clean": "rimraf dist"
43
+ },
44
+ "peerDependencies": {
45
+ "@doxi/core": "workspace:*"
46
+ },
47
+ "devDependencies": {
48
+ "rimraf": "6.0.1"
49
+ }
50
+ }