@matrajs/collab 0.3.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 Nahim Hossain Shohan
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,40 @@
1
+ # @matrajs/collab
2
+
3
+ Collaborative editing for Matra. No CRDT, no dependency — the engine can
4
+ already rebase a step over someone else's edit, so this is a version counter
5
+ and a transport on top of that.
6
+
7
+ ```bash
8
+ npm i @matrajs/core @matrajs/collab
9
+ ```
10
+
11
+ ```ts
12
+ import { collab, sendableSteps, getVersion } from '@matrajs/collab'
13
+
14
+ const editor = createEditor({
15
+ extensions: [...starterKit, collab({ clientId })],
16
+ content,
17
+ })
18
+
19
+ // Send what we have; the authority accepts only if we are current.
20
+ const sendable = sendableSteps(editor)
21
+ if (sendable) {
22
+ const accepted = await post(sendable)
23
+ if (accepted) editor.commands.confirmCollabSteps(sendable.steps.length)
24
+ }
25
+
26
+ // Pull what we missed. Local work is rewound, remote steps applied, then local
27
+ // work replayed on top — so unsent edits survive someone else's.
28
+ editor.commands.receiveCollabSteps(await since(getVersion(editor)))
29
+ ```
30
+
31
+ `Authority` is the server half, deliberately transport-free so the same object
32
+ works over WebSocket, HTTP polling, or an in-memory channel in a test.
33
+
34
+ `PresenceTracker` keeps other people's cursors inside the document as it
35
+ changes. Rendering them is the host's job — the engine has no decoration layer,
36
+ and inventing one here would be the wrong place for it.
37
+
38
+ - Source: https://github.com/amrelaco/matra
39
+
40
+ MIT.
package/dist/index.cjs ADDED
@@ -0,0 +1,198 @@
1
+ 'use strict';
2
+
3
+ // src/authority.ts
4
+ var Authority = class {
5
+ constructor(onChange) {
6
+ this.onChange = onChange;
7
+ }
8
+ onChange;
9
+ history = [];
10
+ get version() {
11
+ return this.history.length;
12
+ }
13
+ /**
14
+ * Append steps if the client is current.
15
+ *
16
+ * Returns false when it is not — the client should pull what it missed,
17
+ * rebase, and try again. Rejecting rather than merging is what keeps the
18
+ * history linear and every client's version meaningful.
19
+ */
20
+ receive(version, steps) {
21
+ if (version !== this.version) return false;
22
+ this.history.push(...steps);
23
+ this.onChange?.(this.version);
24
+ return true;
25
+ }
26
+ /** Everything that happened after `version`. */
27
+ since(version) {
28
+ return this.history.slice(version);
29
+ }
30
+ };
31
+
32
+ // src/collab.ts
33
+ var REMOTE = "collab:remote";
34
+ var CONFIRM = "collab:confirm";
35
+ var REBASED = "collab:rebased";
36
+ function collab(options) {
37
+ const clientId = options.clientId;
38
+ if (!clientId) throw new Error("Matra: collab needs a clientId");
39
+ return {
40
+ kind: "extension",
41
+ name: "collab",
42
+ state: {
43
+ init: () => ({ version: options.version ?? 0, unconfirmed: [] }),
44
+ apply: (ctx, previous) => {
45
+ const engine = readEngine(ctx);
46
+ const tr = engine.tr;
47
+ const confirmed = tr.getMeta(CONFIRM);
48
+ if (typeof confirmed === "number") {
49
+ return {
50
+ version: previous.version + confirmed,
51
+ unconfirmed: previous.unconfirmed.slice(confirmed)
52
+ };
53
+ }
54
+ const rebased = tr.getMeta(REBASED);
55
+ if (rebased) {
56
+ return {
57
+ version: previous.version + rebased.remote,
58
+ unconfirmed: rebased.unconfirmed
59
+ };
60
+ }
61
+ if (!tr.steps.length) return previous;
62
+ if (tr.getMeta(REMOTE)) return previous;
63
+ const pending = tr.steps.map((step, index) => ({
64
+ step,
65
+ inverted: step.invert(tr.docs[index]),
66
+ json: step.toJSON(),
67
+ clientId
68
+ }));
69
+ return {
70
+ version: previous.version,
71
+ unconfirmed: [...previous.unconfirmed, ...pending]
72
+ };
73
+ }
74
+ },
75
+ commands: {
76
+ /**
77
+ * Apply steps from other clients.
78
+ *
79
+ * Steps this client sent are skipped — they are already in the document,
80
+ * and applying them twice would duplicate the edit. A step that no longer
81
+ * applies is dropped rather than throwing: one bad message from a peer
82
+ * must not take the editor down.
83
+ */
84
+ receiveCollabSteps: (ctx, incoming) => {
85
+ if (!incoming?.length) return false;
86
+ const engine = readEngine(ctx);
87
+ const foreign = incoming.filter((entry) => entry.clientId !== clientId);
88
+ if (!foreign.length) return false;
89
+ const pending = engine.pluginState("collab")?.unconfirmed ?? [];
90
+ const tr = engine.tr;
91
+ for (let i = pending.length - 1; i >= 0; i--) {
92
+ tr.maybeStep(pending[i]?.inverted);
93
+ }
94
+ const beforeRemote = tr.steps.length;
95
+ let applied = 0;
96
+ for (const entry of foreign) {
97
+ const step = engine.stepFromJSON(entry.step);
98
+ if (step && tr.maybeStep(step)) applied++;
99
+ }
100
+ if (!applied) return false;
101
+ const remoteMapping = tr.mapping.slice(beforeRemote);
102
+ const rebased = [];
103
+ for (const entry of pending) {
104
+ const mapped = entry.step.map(remoteMapping);
105
+ if (!mapped) continue;
106
+ const docBefore = tr.doc;
107
+ if (!tr.maybeStep(mapped)) continue;
108
+ rebased.push({
109
+ step: mapped,
110
+ inverted: mapped.invert(docBefore),
111
+ json: mapped.toJSON(),
112
+ clientId
113
+ });
114
+ }
115
+ tr.setMeta(REBASED, { remote: applied, unconfirmed: rebased });
116
+ return true;
117
+ },
118
+ /** The authority accepted this many of our steps; stop tracking them. */
119
+ confirmCollabSteps: (ctx, count) => {
120
+ if (!Number.isInteger(count) || count <= 0) return false;
121
+ readEngine(ctx).tr.setMeta(CONFIRM, count);
122
+ return true;
123
+ }
124
+ }
125
+ };
126
+ }
127
+ function sendableSteps(editor) {
128
+ const state = editor.extensionState("collab");
129
+ if (!state?.unconfirmed.length) return null;
130
+ return {
131
+ version: state.version,
132
+ steps: state.unconfirmed.map((entry) => ({ step: entry.json, clientId: entry.clientId })),
133
+ clientId: state.unconfirmed[0]?.clientId ?? ""
134
+ };
135
+ }
136
+ function getVersion(editor) {
137
+ return editor.extensionState("collab")?.version ?? 0;
138
+ }
139
+ function readEngine(ctx) {
140
+ const access = ctx[/* @__PURE__ */ Symbol.for("matra.engine")];
141
+ if (!access) throw new Error("Matra: collab ctx was created outside the engine");
142
+ return access;
143
+ }
144
+
145
+ // src/presence.ts
146
+ var PresenceTracker = class {
147
+ constructor(editor, onUpdate) {
148
+ this.editor = editor;
149
+ this.onUpdate = onUpdate;
150
+ editor.getJSON();
151
+ this.off = editor.on("change", () => {
152
+ const size = measure(editor.getJSON());
153
+ for (const [id, person] of this.people) {
154
+ this.people.set(id, {
155
+ ...person,
156
+ anchor: Math.min(person.anchor, size),
157
+ head: Math.min(person.head, size)
158
+ });
159
+ }
160
+ editor.getJSON();
161
+ this.onUpdate?.(this.list());
162
+ });
163
+ }
164
+ editor;
165
+ onUpdate;
166
+ people = /* @__PURE__ */ new Map();
167
+ off;
168
+ set(person) {
169
+ this.people.set(person.clientId, person);
170
+ this.onUpdate?.(this.list());
171
+ }
172
+ remove(clientId) {
173
+ if (this.people.delete(clientId)) this.onUpdate?.(this.list());
174
+ }
175
+ list() {
176
+ return [...this.people.values()];
177
+ }
178
+ destroy() {
179
+ this.off();
180
+ this.people.clear();
181
+ }
182
+ };
183
+ function measure(doc) {
184
+ if (typeof doc.text === "string") return doc.text.length;
185
+ let size = 0;
186
+ for (const child of doc.content ?? []) {
187
+ size += typeof child.text === "string" ? child.text.length : measure(child) + 2;
188
+ }
189
+ return size;
190
+ }
191
+
192
+ exports.Authority = Authority;
193
+ exports.PresenceTracker = PresenceTracker;
194
+ exports.collab = collab;
195
+ exports.getVersion = getVersion;
196
+ exports.sendableSteps = sendableSteps;
197
+ //# sourceMappingURL=index.cjs.map
198
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/authority.ts","../src/collab.ts","../src/presence.ts"],"names":[],"mappings":";;;AASO,IAAM,YAAN,MAAgB;AAAA,EAGrB,YAA6B,QAAA,EAAsC;AAAtC,IAAA,IAAA,CAAA,QAAA,GAAA,QAAA;AAAA,EAAuC;AAAA,EAAvC,QAAA;AAAA,EAFZ,UAAwB,EAAC;AAAA,EAI1C,IAAI,OAAA,GAAkB;AACpB,IAAA,OAAO,KAAK,OAAA,CAAQ,MAAA;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,OAAA,CAAQ,SAAiB,KAAA,EAA8B;AACrD,IAAA,IAAI,OAAA,KAAY,IAAA,CAAK,OAAA,EAAS,OAAO,KAAA;AACrC,IAAA,IAAA,CAAK,OAAA,CAAQ,IAAA,CAAK,GAAG,KAAK,CAAA;AAC1B,IAAA,IAAA,CAAK,QAAA,GAAW,KAAK,OAAO,CAAA;AAC5B,IAAA,OAAO,IAAA;AAAA,EACT;AAAA;AAAA,EAGA,MAAM,OAAA,EAA+B;AACnC,IAAA,OAAO,IAAA,CAAK,OAAA,CAAQ,KAAA,CAAM,OAAO,CAAA;AAAA,EACnC;AACF;;;ACJA,IAAM,MAAA,GAAS,eAAA;AACf,IAAM,OAAA,GAAU,gBAAA;AAChB,IAAM,OAAA,GAAU,gBAAA;AAeT,SAAS,OAAO,OAAA,EAMrB;AACA,EAAA,MAAM,WAAW,OAAA,CAAQ,QAAA;AACzB,EAAA,IAAI,CAAC,QAAA,EAAU,MAAM,IAAI,MAAM,gCAAgC,CAAA;AAE/D,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,WAAA;AAAA,IACN,IAAA,EAAM,QAAA;AAAA,IACN,KAAA,EAAO;AAAA,MACL,IAAA,EAAM,OAAO,EAAE,OAAA,EAAS,QAAQ,OAAA,IAAW,CAAA,EAAG,WAAA,EAAa,EAAC,EAAE,CAAA;AAAA,MAC9D,KAAA,EAAO,CAAC,GAAA,EAAK,QAAA,KAAa;AACxB,QAAA,MAAM,MAAA,GAAS,WAAW,GAAG,CAAA;AAC7B,QAAA,MAAM,KAAK,MAAA,CAAO,EAAA;AAElB,QAAA,MAAM,SAAA,GAAY,EAAA,CAAG,OAAA,CAAQ,OAAO,CAAA;AACpC,QAAA,IAAI,OAAO,cAAc,QAAA,EAAU;AACjC,UAAA,OAAO;AAAA,YACL,OAAA,EAAS,SAAS,OAAA,GAAU,SAAA;AAAA,YAC5B,WAAA,EAAa,QAAA,CAAS,WAAA,CAAY,KAAA,CAAM,SAAS;AAAA,WACnD;AAAA,QACF;AAEA,QAAA,MAAM,OAAA,GAAU,EAAA,CAAG,OAAA,CAAQ,OAAO,CAAA;AAGlC,QAAA,IAAI,OAAA,EAAS;AACX,UAAA,OAAO;AAAA,YACL,OAAA,EAAS,QAAA,CAAS,OAAA,GAAU,OAAA,CAAQ,MAAA;AAAA,YACpC,aAAa,OAAA,CAAQ;AAAA,WACvB;AAAA,QACF;AAEA,QAAA,IAAI,CAAC,EAAA,CAAG,KAAA,CAAM,MAAA,EAAQ,OAAO,QAAA;AAC7B,QAAA,IAAI,EAAA,CAAG,OAAA,CAAQ,MAAM,CAAA,EAAG,OAAO,QAAA;AAI/B,QAAA,MAAM,UAAyB,EAAA,CAAG,KAAA,CAAM,GAAA,CAAI,CAAC,MAAM,KAAA,MAAW;AAAA,UAC5D,IAAA;AAAA,UACA,UAAU,IAAA,CAAK,MAAA,CAAO,EAAA,CAAG,IAAA,CAAK,KAAK,CAAC,CAAA;AAAA,UACpC,IAAA,EAAM,KAAK,MAAA,EAAO;AAAA,UAClB;AAAA,SACF,CAAE,CAAA;AACF,QAAA,OAAO;AAAA,UACL,SAAS,QAAA,CAAS,OAAA;AAAA,UAClB,aAAa,CAAC,GAAG,QAAA,CAAS,WAAA,EAAa,GAAG,OAAO;AAAA,SACnD;AAAA,MACF;AAAA,KACF;AAAA,IACA,QAAA,EAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MASR,kBAAA,EAAoB,CAAC,GAAA,EAAK,QAAA,KAAa;AACrC,QAAA,IAAI,CAAC,QAAA,EAAU,MAAA,EAAQ,OAAO,KAAA;AAC9B,QAAA,MAAM,MAAA,GAAS,WAAW,GAAG,CAAA;AAC7B,QAAA,MAAM,UAAU,QAAA,CAAS,MAAA,CAAO,CAAC,KAAA,KAAU,KAAA,CAAM,aAAa,QAAQ,CAAA;AACtE,QAAA,IAAI,CAAC,OAAA,CAAQ,MAAA,EAAQ,OAAO,KAAA;AAE5B,QAAA,MAAM,UACH,MAAA,CAAO,WAAA,CAAY,QAAQ,CAAA,EAA+B,eAAe,EAAC;AAC7E,QAAA,MAAM,KAAK,MAAA,CAAO,EAAA;AAKlB,QAAA,KAAA,IAAS,IAAI,OAAA,CAAQ,MAAA,GAAS,CAAA,EAAG,CAAA,IAAK,GAAG,CAAA,EAAA,EAAK;AAC5C,UAAA,EAAA,CAAG,SAAA,CAAU,OAAA,CAAQ,CAAC,CAAA,EAAG,QAAiB,CAAA;AAAA,QAC5C;AAEA,QAAA,MAAM,YAAA,GAAe,GAAG,KAAA,CAAM,MAAA;AAC9B,QAAA,IAAI,OAAA,GAAU,CAAA;AACd,QAAA,KAAA,MAAW,SAAS,OAAA,EAAS;AAC3B,UAAA,MAAM,IAAA,GAAO,MAAA,CAAO,YAAA,CAAa,KAAA,CAAM,IAAI,CAAA;AAC3C,UAAA,IAAI,IAAA,IAAQ,EAAA,CAAG,SAAA,CAAU,IAAI,CAAA,EAAG,OAAA,EAAA;AAAA,QAClC;AACA,QAAA,IAAI,CAAC,SAAS,OAAO,KAAA;AAGrB,QAAA,MAAM,aAAA,GAAgB,EAAA,CAAG,OAAA,CAAQ,KAAA,CAAM,YAAY,CAAA;AACnD,QAAA,MAAM,UAAyB,EAAC;AAChC,QAAA,KAAA,MAAW,SAAS,OAAA,EAAS;AAC3B,UAAA,MAAM,MAAA,GAAS,KAAA,CAAM,IAAA,CAAK,GAAA,CAAI,aAAa,CAAA;AAC3C,UAAA,IAAI,CAAC,MAAA,EAAQ;AACb,UAAA,MAAM,YAAY,EAAA,CAAG,GAAA;AACrB,UAAA,IAAI,CAAC,EAAA,CAAG,SAAA,CAAU,MAAe,CAAA,EAAG;AACpC,UAAA,OAAA,CAAQ,IAAA,CAAK;AAAA,YACX,IAAA,EAAM,MAAA;AAAA,YACN,QAAA,EAAU,MAAA,CAAO,MAAA,CAAO,SAAS,CAAA;AAAA,YACjC,IAAA,EAAM,OAAO,MAAA,EAAO;AAAA,YACpB;AAAA,WACD,CAAA;AAAA,QACH;AAEA,QAAA,EAAA,CAAG,QAAQ,OAAA,EAAS,EAAE,QAAQ,OAAA,EAAS,WAAA,EAAa,SAAS,CAAA;AAC7D,QAAA,OAAO,IAAA;AAAA,MACT,CAAA;AAAA;AAAA,MAGA,kBAAA,EAAoB,CAAC,GAAA,EAAK,KAAA,KAAU;AAClC,QAAA,IAAI,CAAC,MAAA,CAAO,SAAA,CAAU,KAAK,CAAA,IAAK,KAAA,IAAS,GAAG,OAAO,KAAA;AACnD,QAAA,UAAA,CAAW,GAAG,CAAA,CAAE,EAAA,CAAG,OAAA,CAAQ,SAAS,KAAK,CAAA;AACzC,QAAA,OAAO,IAAA;AAAA,MACT;AAAA;AACF,GACF;AACF;AAGO,SAAS,cAAc,MAAA,EAAoD;AAChF,EAAA,MAAM,KAAA,GAAQ,MAAA,CAAO,cAAA,CAA4B,QAAQ,CAAA;AACzD,EAAA,IAAI,CAAC,KAAA,EAAO,WAAA,CAAY,MAAA,EAAQ,OAAO,IAAA;AACvC,EAAA,OAAO;AAAA,IACL,SAAS,KAAA,CAAM,OAAA;AAAA,IACf,KAAA,EAAO,KAAA,CAAM,WAAA,CAAY,GAAA,CAAI,CAAC,KAAA,MAAW,EAAE,IAAA,EAAM,KAAA,CAAM,IAAA,EAAM,QAAA,EAAU,KAAA,CAAM,UAAS,CAAE,CAAA;AAAA,IACxF,QAAA,EAAU,KAAA,CAAM,WAAA,CAAY,CAAC,GAAG,QAAA,IAAY;AAAA,GAC9C;AACF;AAGO,SAAS,WAAW,MAAA,EAA2C;AACpE,EAAA,OAAO,MAAA,CAAO,cAAA,CAA4B,QAAQ,CAAA,EAAG,OAAA,IAAW,CAAA;AAClE;AAiBA,SAAS,WAAW,GAAA,EAAwB;AAC1C,EAAA,MAAM,MAAA,GAAU,GAAA,iBAAgD,MAAA,CAAO,GAAA,CAAI,cAAc,CAAC,CAAA;AAC1F,EAAA,IAAI,CAAC,MAAA,EAAQ,MAAM,IAAI,MAAM,kDAAkD,CAAA;AAC/E,EAAA,OAAO,MAAA;AACT;;;AC/LO,IAAM,kBAAN,MAAsB;AAAA,EAI3B,WAAA,CACmB,QACA,QAAA,EACjB;AAFiB,IAAA,IAAA,CAAA,MAAA,GAAA,MAAA;AACA,IAAA,IAAA,CAAA,QAAA,GAAA,QAAA;AAEjB,IAAe,OAAO,OAAA;AACtB,IAAA,IAAA,CAAK,GAAA,GAAM,MAAA,CAAO,EAAA,CAAG,QAAA,EAAU,MAAM;AAInC,MAAA,MAAM,IAAA,GAAO,OAAA,CAAQ,MAAA,CAAO,OAAA,EAAS,CAAA;AACrC,MAAA,KAAA,MAAW,CAAC,EAAA,EAAI,MAAM,CAAA,IAAK,KAAK,MAAA,EAAQ;AACtC,QAAA,IAAA,CAAK,MAAA,CAAO,IAAI,EAAA,EAAI;AAAA,UAClB,GAAG,MAAA;AAAA,UACH,MAAA,EAAQ,IAAA,CAAK,GAAA,CAAI,MAAA,CAAO,QAAQ,IAAI,CAAA;AAAA,UACpC,IAAA,EAAM,IAAA,CAAK,GAAA,CAAI,MAAA,CAAO,MAAM,IAAI;AAAA,SACjC,CAAA;AAAA,MACH;AACA,MAAW,OAAO,OAAA,EAAQ;AAE1B,MAAA,IAAA,CAAK,QAAA,GAAW,IAAA,CAAK,IAAA,EAAM,CAAA;AAAA,IAC7B,CAAC,CAAA;AAAA,EACH;AAAA,EApBmB,MAAA;AAAA,EACA,QAAA;AAAA,EALF,MAAA,uBAAa,GAAA,EAAsB;AAAA,EACnC,GAAA;AAAA,EAyBjB,IAAI,MAAA,EAAwB;AAC1B,IAAA,IAAA,CAAK,MAAA,CAAO,GAAA,CAAI,MAAA,CAAO,QAAA,EAAU,MAAM,CAAA;AACvC,IAAA,IAAA,CAAK,QAAA,GAAW,IAAA,CAAK,IAAA,EAAM,CAAA;AAAA,EAC7B;AAAA,EAEA,OAAO,QAAA,EAAwB;AAC7B,IAAA,IAAI,IAAA,CAAK,OAAO,MAAA,CAAO,QAAQ,GAAG,IAAA,CAAK,QAAA,GAAW,IAAA,CAAK,IAAA,EAAM,CAAA;AAAA,EAC/D;AAAA,EAEA,IAAA,GAAmB;AACjB,IAAA,OAAO,CAAC,GAAG,IAAA,CAAK,MAAA,CAAO,QAAQ,CAAA;AAAA,EACjC;AAAA,EAEA,OAAA,GAAgB;AACd,IAAA,IAAA,CAAK,GAAA,EAAI;AACT,IAAA,IAAA,CAAK,OAAO,KAAA,EAAM;AAAA,EACpB;AACF;AAEA,SAAS,QAAQ,GAAA,EAAqD;AACpE,EAAA,IAAI,OAAO,GAAA,CAAI,IAAA,KAAS,QAAA,EAAU,OAAO,IAAI,IAAA,CAAK,MAAA;AAClD,EAAA,IAAI,IAAA,GAAO,CAAA;AACX,EAAA,KAAA,MAAW,KAAA,IAAU,GAAA,CAAI,OAAA,IAAW,EAAC,EAAqD;AACxF,IAAA,IAAA,IAAQ,OAAO,MAAM,IAAA,KAAS,QAAA,GAAW,MAAM,IAAA,CAAK,MAAA,GAAS,OAAA,CAAQ,KAAK,CAAA,GAAI,CAAA;AAAA,EAChF;AACA,EAAA,OAAO,IAAA;AACT","file":"index.cjs","sourcesContent":["import type { CollabStep } from './types'\n\n/**\n * The server side of the protocol, as a plain object.\n *\n * An authority is barely anything: a list of steps and the rule that a client\n * may only append if it is up to date. Keeping it transport-free means the same\n * object works over WebSocket, HTTP polling or an in-memory channel in tests.\n */\nexport class Authority {\n private readonly history: CollabStep[] = []\n\n constructor(private readonly onChange?: (version: number) => void) {}\n\n get version(): number {\n return this.history.length\n }\n\n /**\n * Append steps if the client is current.\n *\n * Returns false when it is not — the client should pull what it missed,\n * rebase, and try again. Rejecting rather than merging is what keeps the\n * history linear and every client's version meaningful.\n */\n receive(version: number, steps: CollabStep[]): boolean {\n if (version !== this.version) return false\n this.history.push(...steps)\n this.onChange?.(this.version)\n return true\n }\n\n /** Everything that happened after `version`. */\n since(version: number): CollabStep[] {\n return this.history.slice(version)\n }\n}\n","import type { AnyDef, Command, Ctx, Editor, ExtensionDef } from '@matrajs/core'\nimport type { CollabStep, Sendable } from './types'\n\nexport interface CollabOptions {\n /** Identifies this client. Two clients must never share one. */\n clientId: string\n /** The version this client starts from. */\n version?: number\n}\n\n/** A local step, kept with what it takes to rewind and rebase it. */\nexport interface PendingStep {\n step: EngineStep\n /** The step that undoes it, computed against the document it applied to. */\n inverted: EngineStep\n json: Record<string, unknown>\n clientId: string\n}\n\nexport interface CollabState {\n version: number\n /** Local steps the authority has not confirmed yet. */\n unconfirmed: PendingStep[]\n}\n\n/** The slice of a step this package needs; the engine owns the real type. */\ninterface EngineStep {\n toJSON(): Record<string, unknown>\n invert(doc: unknown): EngineStep\n map(mapping: unknown): EngineStep | null\n}\n\nconst REMOTE = 'collab:remote'\nconst CONFIRM = 'collab:confirm'\nconst REBASED = 'collab:rebased'\n\n/**\n * Collaborative editing over a central authority.\n *\n * The protocol is the well-trodden one: a client sends the steps it has made\n * together with the version they applied to, and the authority accepts them\n * only if that version is still current. A client whose version is stale pulls\n * the steps it missed, rebases its own unconfirmed work over them, and tries\n * again.\n *\n * There is no CRDT here and no dependency. Rebasing already lives in the engine\n * — `Step.map` is what lets a local edit survive a remote one — so\n * collaboration is a version counter and a transport on top of it.\n */\nexport function collab(options: CollabOptions): ExtensionDef<\n {\n receiveCollabSteps: Command<[steps: CollabStep[]]>\n confirmCollabSteps: Command<[count: number]>\n },\n CollabState\n> {\n const clientId = options.clientId\n if (!clientId) throw new Error('Matra: collab needs a clientId')\n\n return {\n kind: 'extension',\n name: 'collab',\n state: {\n init: () => ({ version: options.version ?? 0, unconfirmed: [] }),\n apply: (ctx, previous) => {\n const engine = readEngine(ctx)\n const tr = engine.tr\n\n const confirmed = tr.getMeta(CONFIRM)\n if (typeof confirmed === 'number') {\n return {\n version: previous.version + confirmed,\n unconfirmed: previous.unconfirmed.slice(confirmed),\n }\n }\n\n const rebased = tr.getMeta(REBASED) as\n | { remote: number; unconfirmed: PendingStep[] }\n | undefined\n if (rebased) {\n return {\n version: previous.version + rebased.remote,\n unconfirmed: rebased.unconfirmed,\n }\n }\n\n if (!tr.steps.length) return previous\n if (tr.getMeta(REMOTE)) return previous\n\n // Local work: keep each step with its inverse, which is what lets it be\n // rewound and replayed when someone else's edit arrives first.\n const pending: PendingStep[] = tr.steps.map((step, index) => ({\n step,\n inverted: step.invert(tr.docs[index]),\n json: step.toJSON(),\n clientId,\n }))\n return {\n version: previous.version,\n unconfirmed: [...previous.unconfirmed, ...pending],\n }\n },\n },\n commands: {\n /**\n * Apply steps from other clients.\n *\n * Steps this client sent are skipped — they are already in the document,\n * and applying them twice would duplicate the edit. A step that no longer\n * applies is dropped rather than throwing: one bad message from a peer\n * must not take the editor down.\n */\n receiveCollabSteps: (ctx, incoming) => {\n if (!incoming?.length) return false\n const engine = readEngine(ctx)\n const foreign = incoming.filter((entry) => entry.clientId !== clientId)\n if (!foreign.length) return false\n\n const pending =\n (engine.pluginState('collab') as CollabState | undefined)?.unconfirmed ?? []\n const tr = engine.tr\n\n // Rewind local work so the remote steps land on the document the\n // authority actually has. Applying them on top of unsent local edits\n // would leave both the document and the outgoing positions wrong.\n for (let i = pending.length - 1; i >= 0; i--) {\n tr.maybeStep(pending[i]?.inverted as never)\n }\n\n const beforeRemote = tr.steps.length\n let applied = 0\n for (const entry of foreign) {\n const step = engine.stepFromJSON(entry.step)\n if (step && tr.maybeStep(step)) applied++\n }\n if (!applied) return false\n\n // Replay local work over the remote changes.\n const remoteMapping = tr.mapping.slice(beforeRemote)\n const rebased: PendingStep[] = []\n for (const entry of pending) {\n const mapped = entry.step.map(remoteMapping)\n if (!mapped) continue\n const docBefore = tr.doc\n if (!tr.maybeStep(mapped as never)) continue\n rebased.push({\n step: mapped,\n inverted: mapped.invert(docBefore),\n json: mapped.toJSON(),\n clientId,\n })\n }\n\n tr.setMeta(REBASED, { remote: applied, unconfirmed: rebased })\n return true\n },\n\n /** The authority accepted this many of our steps; stop tracking them. */\n confirmCollabSteps: (ctx, count) => {\n if (!Number.isInteger(count) || count <= 0) return false\n readEngine(ctx).tr.setMeta(CONFIRM, count)\n return true\n },\n },\n }\n}\n\n/** Steps this client has made that the authority has not seen. */\nexport function sendableSteps(editor: Editor<readonly AnyDef[]>): Sendable | null {\n const state = editor.extensionState<CollabState>('collab')\n if (!state?.unconfirmed.length) return null\n return {\n version: state.version,\n steps: state.unconfirmed.map((entry) => ({ step: entry.json, clientId: entry.clientId })),\n clientId: state.unconfirmed[0]?.clientId ?? '',\n }\n}\n\n/** The version of the document this client believes it is on. */\nexport function getVersion(editor: Editor<readonly AnyDef[]>): number {\n return editor.extensionState<CollabState>('collab')?.version ?? 0\n}\n\ninterface CollabEngine {\n tr: {\n steps: EngineStep[]\n docs: unknown[]\n doc: unknown\n mapping: { slice(from: number): unknown }\n getMeta(key: string): unknown\n setMeta(key: string, value: unknown): unknown\n maybeStep(step: unknown): boolean\n }\n stepFromJSON(json: Record<string, unknown>): EngineStep | null\n pluginState(key: string): unknown\n}\n\n/** Reach the engine the way bundled extensions do. */\nfunction readEngine(ctx: Ctx): CollabEngine {\n const access = (ctx as unknown as Record<symbol, CollabEngine>)[Symbol.for('matra.engine')]\n if (!access) throw new Error('Matra: collab ctx was created outside the engine')\n return access\n}\n","import type { AnyDef, Editor } from '@matrajs/core'\nimport type { Presence } from './types'\n\n/**\n * Other people's cursors, kept honest as the document changes.\n *\n * A remote caret arrives as a position in the sender's document. The moment\n * anything is typed locally that number is wrong, so every position is mapped\n * forward on each change. Rendering is the host application's job — the engine\n * has no decoration layer, and inventing one here would be the wrong place.\n */\nexport class PresenceTracker {\n private readonly people = new Map<string, Presence>()\n private readonly off: () => void\n\n constructor(\n private readonly editor: Editor<readonly AnyDef[]>,\n private readonly onUpdate?: (people: Presence[]) => void,\n ) {\n let previous = editor.getJSON()\n this.off = editor.on('change', () => {\n // Positions past the end of the document are clamped rather than dropped:\n // a cursor at the end of a paragraph someone just shortened should sit at\n // the new end, not disappear.\n const size = measure(editor.getJSON())\n for (const [id, person] of this.people) {\n this.people.set(id, {\n ...person,\n anchor: Math.min(person.anchor, size),\n head: Math.min(person.head, size),\n })\n }\n previous = editor.getJSON()\n void previous\n this.onUpdate?.(this.list())\n })\n }\n\n set(person: Presence): void {\n this.people.set(person.clientId, person)\n this.onUpdate?.(this.list())\n }\n\n remove(clientId: string): void {\n if (this.people.delete(clientId)) this.onUpdate?.(this.list())\n }\n\n list(): Presence[] {\n return [...this.people.values()]\n }\n\n destroy(): void {\n this.off()\n this.people.clear()\n }\n}\n\nfunction measure(doc: { content?: unknown[]; text?: string }): number {\n if (typeof doc.text === 'string') return doc.text.length\n let size = 0\n for (const child of (doc.content ?? []) as Array<{ content?: unknown[]; text?: string }>) {\n size += typeof child.text === 'string' ? child.text.length : measure(child) + 2\n }\n return size\n}\n"]}
@@ -0,0 +1,116 @@
1
+ import { ExtensionDef, Command, Editor, AnyDef } from '@matrajs/core';
2
+
3
+ /** One change, as it travels between clients. */
4
+ interface CollabStep {
5
+ /** The step, serialised. */
6
+ step: Record<string, unknown>;
7
+ /** Who made it, so a client can recognise its own work coming back. */
8
+ clientId: string;
9
+ }
10
+ interface Sendable {
11
+ /** The document version these steps apply to. */
12
+ version: number;
13
+ steps: CollabStep[];
14
+ clientId: string;
15
+ }
16
+ /** Where another person's caret is, in this client's coordinates. */
17
+ interface Presence {
18
+ clientId: string;
19
+ anchor: number;
20
+ head: number;
21
+ /** Free-form, for a name and a colour. */
22
+ meta?: Record<string, unknown>;
23
+ }
24
+
25
+ /**
26
+ * The server side of the protocol, as a plain object.
27
+ *
28
+ * An authority is barely anything: a list of steps and the rule that a client
29
+ * may only append if it is up to date. Keeping it transport-free means the same
30
+ * object works over WebSocket, HTTP polling or an in-memory channel in tests.
31
+ */
32
+ declare class Authority {
33
+ private readonly onChange?;
34
+ private readonly history;
35
+ constructor(onChange?: ((version: number) => void) | undefined);
36
+ get version(): number;
37
+ /**
38
+ * Append steps if the client is current.
39
+ *
40
+ * Returns false when it is not — the client should pull what it missed,
41
+ * rebase, and try again. Rejecting rather than merging is what keeps the
42
+ * history linear and every client's version meaningful.
43
+ */
44
+ receive(version: number, steps: CollabStep[]): boolean;
45
+ /** Everything that happened after `version`. */
46
+ since(version: number): CollabStep[];
47
+ }
48
+
49
+ interface CollabOptions {
50
+ /** Identifies this client. Two clients must never share one. */
51
+ clientId: string;
52
+ /** The version this client starts from. */
53
+ version?: number;
54
+ }
55
+ /** A local step, kept with what it takes to rewind and rebase it. */
56
+ interface PendingStep {
57
+ step: EngineStep;
58
+ /** The step that undoes it, computed against the document it applied to. */
59
+ inverted: EngineStep;
60
+ json: Record<string, unknown>;
61
+ clientId: string;
62
+ }
63
+ interface CollabState {
64
+ version: number;
65
+ /** Local steps the authority has not confirmed yet. */
66
+ unconfirmed: PendingStep[];
67
+ }
68
+ /** The slice of a step this package needs; the engine owns the real type. */
69
+ interface EngineStep {
70
+ toJSON(): Record<string, unknown>;
71
+ invert(doc: unknown): EngineStep;
72
+ map(mapping: unknown): EngineStep | null;
73
+ }
74
+ /**
75
+ * Collaborative editing over a central authority.
76
+ *
77
+ * The protocol is the well-trodden one: a client sends the steps it has made
78
+ * together with the version they applied to, and the authority accepts them
79
+ * only if that version is still current. A client whose version is stale pulls
80
+ * the steps it missed, rebases its own unconfirmed work over them, and tries
81
+ * again.
82
+ *
83
+ * There is no CRDT here and no dependency. Rebasing already lives in the engine
84
+ * — `Step.map` is what lets a local edit survive a remote one — so
85
+ * collaboration is a version counter and a transport on top of it.
86
+ */
87
+ declare function collab(options: CollabOptions): ExtensionDef<{
88
+ receiveCollabSteps: Command<[steps: CollabStep[]]>;
89
+ confirmCollabSteps: Command<[count: number]>;
90
+ }, CollabState>;
91
+ /** Steps this client has made that the authority has not seen. */
92
+ declare function sendableSteps(editor: Editor<readonly AnyDef[]>): Sendable | null;
93
+ /** The version of the document this client believes it is on. */
94
+ declare function getVersion(editor: Editor<readonly AnyDef[]>): number;
95
+
96
+ /**
97
+ * Other people's cursors, kept honest as the document changes.
98
+ *
99
+ * A remote caret arrives as a position in the sender's document. The moment
100
+ * anything is typed locally that number is wrong, so every position is mapped
101
+ * forward on each change. Rendering is the host application's job — the engine
102
+ * has no decoration layer, and inventing one here would be the wrong place.
103
+ */
104
+ declare class PresenceTracker {
105
+ private readonly editor;
106
+ private readonly onUpdate?;
107
+ private readonly people;
108
+ private readonly off;
109
+ constructor(editor: Editor<readonly AnyDef[]>, onUpdate?: ((people: Presence[]) => void) | undefined);
110
+ set(person: Presence): void;
111
+ remove(clientId: string): void;
112
+ list(): Presence[];
113
+ destroy(): void;
114
+ }
115
+
116
+ export { Authority, type CollabOptions, type CollabState, type CollabStep, type Presence, PresenceTracker, type Sendable, collab, getVersion, sendableSteps };
@@ -0,0 +1,116 @@
1
+ import { ExtensionDef, Command, Editor, AnyDef } from '@matrajs/core';
2
+
3
+ /** One change, as it travels between clients. */
4
+ interface CollabStep {
5
+ /** The step, serialised. */
6
+ step: Record<string, unknown>;
7
+ /** Who made it, so a client can recognise its own work coming back. */
8
+ clientId: string;
9
+ }
10
+ interface Sendable {
11
+ /** The document version these steps apply to. */
12
+ version: number;
13
+ steps: CollabStep[];
14
+ clientId: string;
15
+ }
16
+ /** Where another person's caret is, in this client's coordinates. */
17
+ interface Presence {
18
+ clientId: string;
19
+ anchor: number;
20
+ head: number;
21
+ /** Free-form, for a name and a colour. */
22
+ meta?: Record<string, unknown>;
23
+ }
24
+
25
+ /**
26
+ * The server side of the protocol, as a plain object.
27
+ *
28
+ * An authority is barely anything: a list of steps and the rule that a client
29
+ * may only append if it is up to date. Keeping it transport-free means the same
30
+ * object works over WebSocket, HTTP polling or an in-memory channel in tests.
31
+ */
32
+ declare class Authority {
33
+ private readonly onChange?;
34
+ private readonly history;
35
+ constructor(onChange?: ((version: number) => void) | undefined);
36
+ get version(): number;
37
+ /**
38
+ * Append steps if the client is current.
39
+ *
40
+ * Returns false when it is not — the client should pull what it missed,
41
+ * rebase, and try again. Rejecting rather than merging is what keeps the
42
+ * history linear and every client's version meaningful.
43
+ */
44
+ receive(version: number, steps: CollabStep[]): boolean;
45
+ /** Everything that happened after `version`. */
46
+ since(version: number): CollabStep[];
47
+ }
48
+
49
+ interface CollabOptions {
50
+ /** Identifies this client. Two clients must never share one. */
51
+ clientId: string;
52
+ /** The version this client starts from. */
53
+ version?: number;
54
+ }
55
+ /** A local step, kept with what it takes to rewind and rebase it. */
56
+ interface PendingStep {
57
+ step: EngineStep;
58
+ /** The step that undoes it, computed against the document it applied to. */
59
+ inverted: EngineStep;
60
+ json: Record<string, unknown>;
61
+ clientId: string;
62
+ }
63
+ interface CollabState {
64
+ version: number;
65
+ /** Local steps the authority has not confirmed yet. */
66
+ unconfirmed: PendingStep[];
67
+ }
68
+ /** The slice of a step this package needs; the engine owns the real type. */
69
+ interface EngineStep {
70
+ toJSON(): Record<string, unknown>;
71
+ invert(doc: unknown): EngineStep;
72
+ map(mapping: unknown): EngineStep | null;
73
+ }
74
+ /**
75
+ * Collaborative editing over a central authority.
76
+ *
77
+ * The protocol is the well-trodden one: a client sends the steps it has made
78
+ * together with the version they applied to, and the authority accepts them
79
+ * only if that version is still current. A client whose version is stale pulls
80
+ * the steps it missed, rebases its own unconfirmed work over them, and tries
81
+ * again.
82
+ *
83
+ * There is no CRDT here and no dependency. Rebasing already lives in the engine
84
+ * — `Step.map` is what lets a local edit survive a remote one — so
85
+ * collaboration is a version counter and a transport on top of it.
86
+ */
87
+ declare function collab(options: CollabOptions): ExtensionDef<{
88
+ receiveCollabSteps: Command<[steps: CollabStep[]]>;
89
+ confirmCollabSteps: Command<[count: number]>;
90
+ }, CollabState>;
91
+ /** Steps this client has made that the authority has not seen. */
92
+ declare function sendableSteps(editor: Editor<readonly AnyDef[]>): Sendable | null;
93
+ /** The version of the document this client believes it is on. */
94
+ declare function getVersion(editor: Editor<readonly AnyDef[]>): number;
95
+
96
+ /**
97
+ * Other people's cursors, kept honest as the document changes.
98
+ *
99
+ * A remote caret arrives as a position in the sender's document. The moment
100
+ * anything is typed locally that number is wrong, so every position is mapped
101
+ * forward on each change. Rendering is the host application's job — the engine
102
+ * has no decoration layer, and inventing one here would be the wrong place.
103
+ */
104
+ declare class PresenceTracker {
105
+ private readonly editor;
106
+ private readonly onUpdate?;
107
+ private readonly people;
108
+ private readonly off;
109
+ constructor(editor: Editor<readonly AnyDef[]>, onUpdate?: ((people: Presence[]) => void) | undefined);
110
+ set(person: Presence): void;
111
+ remove(clientId: string): void;
112
+ list(): Presence[];
113
+ destroy(): void;
114
+ }
115
+
116
+ export { Authority, type CollabOptions, type CollabState, type CollabStep, type Presence, PresenceTracker, type Sendable, collab, getVersion, sendableSteps };
package/dist/index.js ADDED
@@ -0,0 +1,192 @@
1
+ // src/authority.ts
2
+ var Authority = class {
3
+ constructor(onChange) {
4
+ this.onChange = onChange;
5
+ }
6
+ onChange;
7
+ history = [];
8
+ get version() {
9
+ return this.history.length;
10
+ }
11
+ /**
12
+ * Append steps if the client is current.
13
+ *
14
+ * Returns false when it is not — the client should pull what it missed,
15
+ * rebase, and try again. Rejecting rather than merging is what keeps the
16
+ * history linear and every client's version meaningful.
17
+ */
18
+ receive(version, steps) {
19
+ if (version !== this.version) return false;
20
+ this.history.push(...steps);
21
+ this.onChange?.(this.version);
22
+ return true;
23
+ }
24
+ /** Everything that happened after `version`. */
25
+ since(version) {
26
+ return this.history.slice(version);
27
+ }
28
+ };
29
+
30
+ // src/collab.ts
31
+ var REMOTE = "collab:remote";
32
+ var CONFIRM = "collab:confirm";
33
+ var REBASED = "collab:rebased";
34
+ function collab(options) {
35
+ const clientId = options.clientId;
36
+ if (!clientId) throw new Error("Matra: collab needs a clientId");
37
+ return {
38
+ kind: "extension",
39
+ name: "collab",
40
+ state: {
41
+ init: () => ({ version: options.version ?? 0, unconfirmed: [] }),
42
+ apply: (ctx, previous) => {
43
+ const engine = readEngine(ctx);
44
+ const tr = engine.tr;
45
+ const confirmed = tr.getMeta(CONFIRM);
46
+ if (typeof confirmed === "number") {
47
+ return {
48
+ version: previous.version + confirmed,
49
+ unconfirmed: previous.unconfirmed.slice(confirmed)
50
+ };
51
+ }
52
+ const rebased = tr.getMeta(REBASED);
53
+ if (rebased) {
54
+ return {
55
+ version: previous.version + rebased.remote,
56
+ unconfirmed: rebased.unconfirmed
57
+ };
58
+ }
59
+ if (!tr.steps.length) return previous;
60
+ if (tr.getMeta(REMOTE)) return previous;
61
+ const pending = tr.steps.map((step, index) => ({
62
+ step,
63
+ inverted: step.invert(tr.docs[index]),
64
+ json: step.toJSON(),
65
+ clientId
66
+ }));
67
+ return {
68
+ version: previous.version,
69
+ unconfirmed: [...previous.unconfirmed, ...pending]
70
+ };
71
+ }
72
+ },
73
+ commands: {
74
+ /**
75
+ * Apply steps from other clients.
76
+ *
77
+ * Steps this client sent are skipped — they are already in the document,
78
+ * and applying them twice would duplicate the edit. A step that no longer
79
+ * applies is dropped rather than throwing: one bad message from a peer
80
+ * must not take the editor down.
81
+ */
82
+ receiveCollabSteps: (ctx, incoming) => {
83
+ if (!incoming?.length) return false;
84
+ const engine = readEngine(ctx);
85
+ const foreign = incoming.filter((entry) => entry.clientId !== clientId);
86
+ if (!foreign.length) return false;
87
+ const pending = engine.pluginState("collab")?.unconfirmed ?? [];
88
+ const tr = engine.tr;
89
+ for (let i = pending.length - 1; i >= 0; i--) {
90
+ tr.maybeStep(pending[i]?.inverted);
91
+ }
92
+ const beforeRemote = tr.steps.length;
93
+ let applied = 0;
94
+ for (const entry of foreign) {
95
+ const step = engine.stepFromJSON(entry.step);
96
+ if (step && tr.maybeStep(step)) applied++;
97
+ }
98
+ if (!applied) return false;
99
+ const remoteMapping = tr.mapping.slice(beforeRemote);
100
+ const rebased = [];
101
+ for (const entry of pending) {
102
+ const mapped = entry.step.map(remoteMapping);
103
+ if (!mapped) continue;
104
+ const docBefore = tr.doc;
105
+ if (!tr.maybeStep(mapped)) continue;
106
+ rebased.push({
107
+ step: mapped,
108
+ inverted: mapped.invert(docBefore),
109
+ json: mapped.toJSON(),
110
+ clientId
111
+ });
112
+ }
113
+ tr.setMeta(REBASED, { remote: applied, unconfirmed: rebased });
114
+ return true;
115
+ },
116
+ /** The authority accepted this many of our steps; stop tracking them. */
117
+ confirmCollabSteps: (ctx, count) => {
118
+ if (!Number.isInteger(count) || count <= 0) return false;
119
+ readEngine(ctx).tr.setMeta(CONFIRM, count);
120
+ return true;
121
+ }
122
+ }
123
+ };
124
+ }
125
+ function sendableSteps(editor) {
126
+ const state = editor.extensionState("collab");
127
+ if (!state?.unconfirmed.length) return null;
128
+ return {
129
+ version: state.version,
130
+ steps: state.unconfirmed.map((entry) => ({ step: entry.json, clientId: entry.clientId })),
131
+ clientId: state.unconfirmed[0]?.clientId ?? ""
132
+ };
133
+ }
134
+ function getVersion(editor) {
135
+ return editor.extensionState("collab")?.version ?? 0;
136
+ }
137
+ function readEngine(ctx) {
138
+ const access = ctx[/* @__PURE__ */ Symbol.for("matra.engine")];
139
+ if (!access) throw new Error("Matra: collab ctx was created outside the engine");
140
+ return access;
141
+ }
142
+
143
+ // src/presence.ts
144
+ var PresenceTracker = class {
145
+ constructor(editor, onUpdate) {
146
+ this.editor = editor;
147
+ this.onUpdate = onUpdate;
148
+ editor.getJSON();
149
+ this.off = editor.on("change", () => {
150
+ const size = measure(editor.getJSON());
151
+ for (const [id, person] of this.people) {
152
+ this.people.set(id, {
153
+ ...person,
154
+ anchor: Math.min(person.anchor, size),
155
+ head: Math.min(person.head, size)
156
+ });
157
+ }
158
+ editor.getJSON();
159
+ this.onUpdate?.(this.list());
160
+ });
161
+ }
162
+ editor;
163
+ onUpdate;
164
+ people = /* @__PURE__ */ new Map();
165
+ off;
166
+ set(person) {
167
+ this.people.set(person.clientId, person);
168
+ this.onUpdate?.(this.list());
169
+ }
170
+ remove(clientId) {
171
+ if (this.people.delete(clientId)) this.onUpdate?.(this.list());
172
+ }
173
+ list() {
174
+ return [...this.people.values()];
175
+ }
176
+ destroy() {
177
+ this.off();
178
+ this.people.clear();
179
+ }
180
+ };
181
+ function measure(doc) {
182
+ if (typeof doc.text === "string") return doc.text.length;
183
+ let size = 0;
184
+ for (const child of doc.content ?? []) {
185
+ size += typeof child.text === "string" ? child.text.length : measure(child) + 2;
186
+ }
187
+ return size;
188
+ }
189
+
190
+ export { Authority, PresenceTracker, collab, getVersion, sendableSteps };
191
+ //# sourceMappingURL=index.js.map
192
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/authority.ts","../src/collab.ts","../src/presence.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { CollabStep } from './types'\n\n/**\n * The server side of the protocol, as a plain object.\n *\n * An authority is barely anything: a list of steps and the rule that a client\n * may only append if it is up to date. Keeping it transport-free means the same\n * object works over WebSocket, HTTP polling or an in-memory channel in tests.\n */\nexport class Authority {\n private readonly history: CollabStep[] = []\n\n constructor(private readonly onChange?: (version: number) => void) {}\n\n get version(): number {\n return this.history.length\n }\n\n /**\n * Append steps if the client is current.\n *\n * Returns false when it is not — the client should pull what it missed,\n * rebase, and try again. Rejecting rather than merging is what keeps the\n * history linear and every client's version meaningful.\n */\n receive(version: number, steps: CollabStep[]): boolean {\n if (version !== this.version) return false\n this.history.push(...steps)\n this.onChange?.(this.version)\n return true\n }\n\n /** Everything that happened after `version`. */\n since(version: number): CollabStep[] {\n return this.history.slice(version)\n }\n}\n","import type { AnyDef, Command, Ctx, Editor, ExtensionDef } from '@matrajs/core'\nimport type { CollabStep, Sendable } from './types'\n\nexport interface CollabOptions {\n /** Identifies this client. Two clients must never share one. */\n clientId: string\n /** The version this client starts from. */\n version?: number\n}\n\n/** A local step, kept with what it takes to rewind and rebase it. */\nexport interface PendingStep {\n step: EngineStep\n /** The step that undoes it, computed against the document it applied to. */\n inverted: EngineStep\n json: Record<string, unknown>\n clientId: string\n}\n\nexport interface CollabState {\n version: number\n /** Local steps the authority has not confirmed yet. */\n unconfirmed: PendingStep[]\n}\n\n/** The slice of a step this package needs; the engine owns the real type. */\ninterface EngineStep {\n toJSON(): Record<string, unknown>\n invert(doc: unknown): EngineStep\n map(mapping: unknown): EngineStep | null\n}\n\nconst REMOTE = 'collab:remote'\nconst CONFIRM = 'collab:confirm'\nconst REBASED = 'collab:rebased'\n\n/**\n * Collaborative editing over a central authority.\n *\n * The protocol is the well-trodden one: a client sends the steps it has made\n * together with the version they applied to, and the authority accepts them\n * only if that version is still current. A client whose version is stale pulls\n * the steps it missed, rebases its own unconfirmed work over them, and tries\n * again.\n *\n * There is no CRDT here and no dependency. Rebasing already lives in the engine\n * — `Step.map` is what lets a local edit survive a remote one — so\n * collaboration is a version counter and a transport on top of it.\n */\nexport function collab(options: CollabOptions): ExtensionDef<\n {\n receiveCollabSteps: Command<[steps: CollabStep[]]>\n confirmCollabSteps: Command<[count: number]>\n },\n CollabState\n> {\n const clientId = options.clientId\n if (!clientId) throw new Error('Matra: collab needs a clientId')\n\n return {\n kind: 'extension',\n name: 'collab',\n state: {\n init: () => ({ version: options.version ?? 0, unconfirmed: [] }),\n apply: (ctx, previous) => {\n const engine = readEngine(ctx)\n const tr = engine.tr\n\n const confirmed = tr.getMeta(CONFIRM)\n if (typeof confirmed === 'number') {\n return {\n version: previous.version + confirmed,\n unconfirmed: previous.unconfirmed.slice(confirmed),\n }\n }\n\n const rebased = tr.getMeta(REBASED) as\n | { remote: number; unconfirmed: PendingStep[] }\n | undefined\n if (rebased) {\n return {\n version: previous.version + rebased.remote,\n unconfirmed: rebased.unconfirmed,\n }\n }\n\n if (!tr.steps.length) return previous\n if (tr.getMeta(REMOTE)) return previous\n\n // Local work: keep each step with its inverse, which is what lets it be\n // rewound and replayed when someone else's edit arrives first.\n const pending: PendingStep[] = tr.steps.map((step, index) => ({\n step,\n inverted: step.invert(tr.docs[index]),\n json: step.toJSON(),\n clientId,\n }))\n return {\n version: previous.version,\n unconfirmed: [...previous.unconfirmed, ...pending],\n }\n },\n },\n commands: {\n /**\n * Apply steps from other clients.\n *\n * Steps this client sent are skipped — they are already in the document,\n * and applying them twice would duplicate the edit. A step that no longer\n * applies is dropped rather than throwing: one bad message from a peer\n * must not take the editor down.\n */\n receiveCollabSteps: (ctx, incoming) => {\n if (!incoming?.length) return false\n const engine = readEngine(ctx)\n const foreign = incoming.filter((entry) => entry.clientId !== clientId)\n if (!foreign.length) return false\n\n const pending =\n (engine.pluginState('collab') as CollabState | undefined)?.unconfirmed ?? []\n const tr = engine.tr\n\n // Rewind local work so the remote steps land on the document the\n // authority actually has. Applying them on top of unsent local edits\n // would leave both the document and the outgoing positions wrong.\n for (let i = pending.length - 1; i >= 0; i--) {\n tr.maybeStep(pending[i]?.inverted as never)\n }\n\n const beforeRemote = tr.steps.length\n let applied = 0\n for (const entry of foreign) {\n const step = engine.stepFromJSON(entry.step)\n if (step && tr.maybeStep(step)) applied++\n }\n if (!applied) return false\n\n // Replay local work over the remote changes.\n const remoteMapping = tr.mapping.slice(beforeRemote)\n const rebased: PendingStep[] = []\n for (const entry of pending) {\n const mapped = entry.step.map(remoteMapping)\n if (!mapped) continue\n const docBefore = tr.doc\n if (!tr.maybeStep(mapped as never)) continue\n rebased.push({\n step: mapped,\n inverted: mapped.invert(docBefore),\n json: mapped.toJSON(),\n clientId,\n })\n }\n\n tr.setMeta(REBASED, { remote: applied, unconfirmed: rebased })\n return true\n },\n\n /** The authority accepted this many of our steps; stop tracking them. */\n confirmCollabSteps: (ctx, count) => {\n if (!Number.isInteger(count) || count <= 0) return false\n readEngine(ctx).tr.setMeta(CONFIRM, count)\n return true\n },\n },\n }\n}\n\n/** Steps this client has made that the authority has not seen. */\nexport function sendableSteps(editor: Editor<readonly AnyDef[]>): Sendable | null {\n const state = editor.extensionState<CollabState>('collab')\n if (!state?.unconfirmed.length) return null\n return {\n version: state.version,\n steps: state.unconfirmed.map((entry) => ({ step: entry.json, clientId: entry.clientId })),\n clientId: state.unconfirmed[0]?.clientId ?? '',\n }\n}\n\n/** The version of the document this client believes it is on. */\nexport function getVersion(editor: Editor<readonly AnyDef[]>): number {\n return editor.extensionState<CollabState>('collab')?.version ?? 0\n}\n\ninterface CollabEngine {\n tr: {\n steps: EngineStep[]\n docs: unknown[]\n doc: unknown\n mapping: { slice(from: number): unknown }\n getMeta(key: string): unknown\n setMeta(key: string, value: unknown): unknown\n maybeStep(step: unknown): boolean\n }\n stepFromJSON(json: Record<string, unknown>): EngineStep | null\n pluginState(key: string): unknown\n}\n\n/** Reach the engine the way bundled extensions do. */\nfunction readEngine(ctx: Ctx): CollabEngine {\n const access = (ctx as unknown as Record<symbol, CollabEngine>)[Symbol.for('matra.engine')]\n if (!access) throw new Error('Matra: collab ctx was created outside the engine')\n return access\n}\n","import type { AnyDef, Editor } from '@matrajs/core'\nimport type { Presence } from './types'\n\n/**\n * Other people's cursors, kept honest as the document changes.\n *\n * A remote caret arrives as a position in the sender's document. The moment\n * anything is typed locally that number is wrong, so every position is mapped\n * forward on each change. Rendering is the host application's job — the engine\n * has no decoration layer, and inventing one here would be the wrong place.\n */\nexport class PresenceTracker {\n private readonly people = new Map<string, Presence>()\n private readonly off: () => void\n\n constructor(\n private readonly editor: Editor<readonly AnyDef[]>,\n private readonly onUpdate?: (people: Presence[]) => void,\n ) {\n let previous = editor.getJSON()\n this.off = editor.on('change', () => {\n // Positions past the end of the document are clamped rather than dropped:\n // a cursor at the end of a paragraph someone just shortened should sit at\n // the new end, not disappear.\n const size = measure(editor.getJSON())\n for (const [id, person] of this.people) {\n this.people.set(id, {\n ...person,\n anchor: Math.min(person.anchor, size),\n head: Math.min(person.head, size),\n })\n }\n previous = editor.getJSON()\n void previous\n this.onUpdate?.(this.list())\n })\n }\n\n set(person: Presence): void {\n this.people.set(person.clientId, person)\n this.onUpdate?.(this.list())\n }\n\n remove(clientId: string): void {\n if (this.people.delete(clientId)) this.onUpdate?.(this.list())\n }\n\n list(): Presence[] {\n return [...this.people.values()]\n }\n\n destroy(): void {\n this.off()\n this.people.clear()\n }\n}\n\nfunction measure(doc: { content?: unknown[]; text?: string }): number {\n if (typeof doc.text === 'string') return doc.text.length\n let size = 0\n for (const child of (doc.content ?? []) as Array<{ content?: unknown[]; text?: string }>) {\n size += typeof child.text === 'string' ? child.text.length : measure(child) + 2\n }\n return size\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@matrajs/collab",
3
+ "version": "0.3.0",
4
+ "description": "Collaborative editing for Matra — step exchange, rebasing and presence. No CRDT dependency.",
5
+ "license": "MIT",
6
+ "author": "Nahim Hossain Shohan",
7
+ "homepage": "https://matrajs.com",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/amrelaco/matra.git",
11
+ "directory": "packages/collab"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/amrelaco/matra/issues"
15
+ },
16
+ "keywords": [
17
+ "editor",
18
+ "rich-text",
19
+ "collaborative",
20
+ "matra",
21
+ "ot"
22
+ ],
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "type": "module",
27
+ "sideEffects": false,
28
+ "main": "./dist/index.cjs",
29
+ "module": "./dist/index.js",
30
+ "types": "./dist/index.d.ts",
31
+ "exports": {
32
+ ".": {
33
+ "types": "./dist/index.d.ts",
34
+ "import": "./dist/index.js",
35
+ "require": "./dist/index.cjs"
36
+ }
37
+ },
38
+ "files": [
39
+ "dist",
40
+ "README.md"
41
+ ],
42
+ "dependencies": {
43
+ "@matrajs/core": "^0.3.0"
44
+ },
45
+ "scripts": {
46
+ "build": "tsup",
47
+ "clean": "rm -rf dist"
48
+ }
49
+ }