@oh-just-another/collab 0.57.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.
Files changed (53) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/LICENSE +21 -0
  3. package/README.md +57 -0
  4. package/dist/.tsbuildinfo +1 -0
  5. package/dist/awareness.d.ts +60 -0
  6. package/dist/awareness.d.ts.map +1 -0
  7. package/dist/awareness.js +89 -0
  8. package/dist/awareness.js.map +1 -0
  9. package/dist/bind-awareness.d.ts +33 -0
  10. package/dist/bind-awareness.d.ts.map +1 -0
  11. package/dist/bind-awareness.js +125 -0
  12. package/dist/bind-awareness.js.map +1 -0
  13. package/dist/bind-editor.d.ts +34 -0
  14. package/dist/bind-editor.d.ts.map +1 -0
  15. package/dist/bind-editor.js +87 -0
  16. package/dist/bind-editor.js.map +1 -0
  17. package/dist/branch-doc.d.ts +53 -0
  18. package/dist/branch-doc.d.ts.map +1 -0
  19. package/dist/branch-doc.js +188 -0
  20. package/dist/branch-doc.js.map +1 -0
  21. package/dist/constants.d.ts +26 -0
  22. package/dist/constants.d.ts.map +1 -0
  23. package/dist/constants.js +26 -0
  24. package/dist/constants.js.map +1 -0
  25. package/dist/encryption.d.ts +74 -0
  26. package/dist/encryption.d.ts.map +1 -0
  27. package/dist/encryption.js +124 -0
  28. package/dist/encryption.js.map +1 -0
  29. package/dist/index.d.ts +12 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +13 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/mentions.d.ts +33 -0
  34. package/dist/mentions.d.ts.map +1 -0
  35. package/dist/mentions.js +57 -0
  36. package/dist/mentions.js.map +1 -0
  37. package/dist/merge.d.ts +59 -0
  38. package/dist/merge.d.ts.map +1 -0
  39. package/dist/merge.js +2 -0
  40. package/dist/merge.js.map +1 -0
  41. package/dist/scene-doc.d.ts +41 -0
  42. package/dist/scene-doc.d.ts.map +1 -0
  43. package/dist/scene-doc.js +111 -0
  44. package/dist/scene-doc.js.map +1 -0
  45. package/dist/transport-provider.d.ts +21 -0
  46. package/dist/transport-provider.d.ts.map +1 -0
  47. package/dist/transport-provider.js +105 -0
  48. package/dist/transport-provider.js.map +1 -0
  49. package/dist/yjs-history.d.ts +68 -0
  50. package/dist/yjs-history.d.ts.map +1 -0
  51. package/dist/yjs-history.js +215 -0
  52. package/dist/yjs-history.js.map +1 -0
  53. package/package.json +59 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encryption.d.ts","sourceRoot":"","sources":["../src/encryption.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAO1D;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,wDAAwD;AACxD,MAAM,WAAW,eAAe;IAC9B,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,6DAA6D;IAC7D,QAAQ,CAAC,GAAG,EAAE,SAAS,CAAC;CACzB;AAED;;;;GAIG;AACH,eAAO,MAAM,eAAe,QAAa,OAAO,CAAC,eAAe,CAU/D,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,aAAa,GAAU,WAAW,MAAM,KAAG,OAAO,CAAC,SAAS,CAcxE,CAAC;AAEF;;;;;;;;;GASG;AACH,qBAAa,kBAAmB,YAAW,SAAS;IAKhD,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAN1B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA4C;IACrE,OAAO,CAAC,WAAW,CAA6B;gBAG7B,KAAK,EAAE,SAAS,EAChB,GAAG,EAAE,SAAS,EACd,OAAO,GAAE;QAAE,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAA;KAAO;IAOrF,IAAI,CAAC,OAAO,EAAE,UAAU,GAAG,IAAI;IAI/B,SAAS,CAAC,OAAO,EAAE,CAAC,OAAO,EAAE,UAAU,KAAK,IAAI,GAAG,MAAM,IAAI;IAO7D,KAAK,IAAI,IAAI;YASC,cAAc;YAcd,kBAAkB;CAqBjC"}
@@ -0,0 +1,124 @@
1
+ import { ENCRYPTION_IV_BYTES, ENCRYPTION_KEY_BITS, ROOM_ID_BYTES, } from "./constants.js";
2
+ /**
3
+ * Mint a fresh credentials pair for a new collab session. Uses
4
+ * `crypto.getRandomValues` for the roomId and `crypto.subtle` for
5
+ * the AES key.
6
+ */
7
+ export const generateRoomKey = async () => {
8
+ const roomId = randomHex(ROOM_ID_BYTES);
9
+ const key = await crypto.subtle.generateKey({ name: "AES-GCM", length: ENCRYPTION_KEY_BITS },
10
+ /* extractable */ true, ["encrypt", "decrypt"]);
11
+ const raw = await crypto.subtle.exportKey("raw", key);
12
+ const keyBase64 = base64UrlEncode(new Uint8Array(raw));
13
+ return { roomId, keyBase64, key };
14
+ };
15
+ /**
16
+ * Re-import a key the second peer parsed out of the URL fragment.
17
+ * Throws on malformed base64 / wrong length so the caller can
18
+ * surface "broken invite link" cleanly.
19
+ */
20
+ export const importRoomKey = async (keyBase64) => {
21
+ const raw = base64UrlDecode(keyBase64);
22
+ if (raw.byteLength !== ENCRYPTION_KEY_BITS / 8) {
23
+ throw new Error(`importRoomKey: expected ${ENCRYPTION_KEY_BITS / 8} bytes, got ${raw.byteLength}`);
24
+ }
25
+ return crypto.subtle.importKey("raw", raw, { name: "AES-GCM", length: ENCRYPTION_KEY_BITS },
26
+ /* extractable */ false, ["encrypt", "decrypt"]);
27
+ };
28
+ /**
29
+ * Wrap any {@link Transport} so every outgoing payload is AES-GCM
30
+ * encrypted and every incoming payload is decrypted before reaching
31
+ * subscribers. The relay sees nothing but binary blobs.
32
+ *
33
+ * Decryption errors are silently dropped — a tampered or
34
+ * wrong-key frame should not surface as a noisy console error
35
+ * (that would let a malicious peer flood the log). Callers can opt
36
+ * into observability by passing `onDecryptError`.
37
+ */
38
+ export class EncryptedTransport {
39
+ inner;
40
+ key;
41
+ options;
42
+ handlers = new Set();
43
+ unsubscribe = null;
44
+ constructor(inner, key, options = {}) {
45
+ this.inner = inner;
46
+ this.key = key;
47
+ this.options = options;
48
+ this.unsubscribe = inner.onMessage((cipher) => {
49
+ void this.decryptAndDispatch(cipher);
50
+ });
51
+ }
52
+ send(payload) {
53
+ void this.encryptAndSend(payload);
54
+ }
55
+ onMessage(handler) {
56
+ this.handlers.add(handler);
57
+ return () => {
58
+ this.handlers.delete(handler);
59
+ };
60
+ }
61
+ close() {
62
+ if (this.unsubscribe) {
63
+ this.unsubscribe();
64
+ this.unsubscribe = null;
65
+ }
66
+ this.handlers.clear();
67
+ this.inner.close();
68
+ }
69
+ async encryptAndSend(plaintext) {
70
+ const iv = crypto.getRandomValues(new Uint8Array(ENCRYPTION_IV_BYTES));
71
+ const cipherBuf = await crypto.subtle.encrypt({ name: "AES-GCM", iv: iv }, this.key, plaintext);
72
+ // Frame = IV || ciphertext (with auth tag suffix from AES-GCM).
73
+ const cipher = new Uint8Array(iv.byteLength + cipherBuf.byteLength);
74
+ cipher.set(iv, 0);
75
+ cipher.set(new Uint8Array(cipherBuf), iv.byteLength);
76
+ this.inner.send(cipher);
77
+ }
78
+ async decryptAndDispatch(cipher) {
79
+ if (cipher.byteLength <= ENCRYPTION_IV_BYTES) {
80
+ this.options.onDecryptError?.(new Error("payload smaller than IV"));
81
+ return;
82
+ }
83
+ const iv = cipher.subarray(0, ENCRYPTION_IV_BYTES);
84
+ const body = cipher.subarray(ENCRYPTION_IV_BYTES);
85
+ let plain;
86
+ try {
87
+ plain = await crypto.subtle.decrypt({ name: "AES-GCM", iv: iv }, this.key, body);
88
+ }
89
+ catch (err) {
90
+ this.options.onDecryptError?.(err);
91
+ return;
92
+ }
93
+ const out = new Uint8Array(plain);
94
+ for (const handler of this.handlers)
95
+ handler(out);
96
+ }
97
+ }
98
+ // --- Helpers ---------------------------------------------------------------
99
+ /** Random hex string of `byteCount` bytes (length = byteCount * 2). */
100
+ const randomHex = (byteCount) => {
101
+ const bytes = crypto.getRandomValues(new Uint8Array(byteCount));
102
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
103
+ };
104
+ /**
105
+ * Base64url (RFC 4648 §5): standard base64 with `+` → `-`, `/` → `_`,
106
+ * no `=` padding. URL-safe so the key can live in `location.hash`
107
+ * without any escaping.
108
+ */
109
+ const base64UrlEncode = (bytes) => {
110
+ let bin = "";
111
+ for (const byte of bytes)
112
+ bin += String.fromCharCode(byte);
113
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
114
+ };
115
+ const base64UrlDecode = (s) => {
116
+ const pad = (4 - (s.length % 4)) % 4;
117
+ const b64 = s.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat(pad);
118
+ const bin = atob(b64);
119
+ const out = new Uint8Array(bin.length);
120
+ for (let i = 0; i < bin.length; i++)
121
+ out[i] = bin.charCodeAt(i);
122
+ return out;
123
+ };
124
+ //# sourceMappingURL=encryption.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encryption.js","sourceRoot":"","sources":["../src/encryption.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ export { SceneDoc } from "./scene-doc.js";
2
+ export { bindEditor } from "./bind-editor.js";
3
+ export { CollabAwareness, type Peer, type PeerUser } from "./awareness.js";
4
+ export { TransportProvider, type TransportProviderOptions } from "./transport-provider.js";
5
+ export { bindAwareness, type BindAwarenessOptions } from "./bind-awareness.js";
6
+ export { extractMentions, resolveMentions, notifyMention } from "./mentions.js";
7
+ export type { BranchId, BranchMergeAPI, ConflictChoice, ConflictResolution, MergeConflict, MergeReport, } from "./merge.js";
8
+ export { BranchDoc } from "./branch-doc.js";
9
+ export { YjsHistory, type YjsHistoryOptions } from "./yjs-history.js";
10
+ export { generateRoomKey, importRoomKey, EncryptedTransport, type RoomCredentials, } from "./encryption.js";
11
+ export { ROOM_ID_BYTES, ENCRYPTION_KEY_BITS, ENCRYPTION_IV_BYTES, } from "./constants.js";
12
+ //# 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,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,eAAe,EAAE,KAAK,IAAI,EAAE,KAAK,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC3E,OAAO,EAAE,iBAAiB,EAAE,KAAK,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AAC3F,OAAO,EAAE,aAAa,EAAE,KAAK,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC/E,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAGhF,YAAY,EACV,QAAQ,EACR,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,aAAa,EACb,WAAW,GACZ,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAG5C,OAAO,EAAE,UAAU,EAAE,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAGtE,OAAO,EACL,eAAe,EACf,aAAa,EACb,kBAAkB,EAClB,KAAK,eAAe,GACrB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,aAAa,EACb,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,gBAAgB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ export { SceneDoc } from "./scene-doc.js";
2
+ export { bindEditor } from "./bind-editor.js";
3
+ export { CollabAwareness } from "./awareness.js";
4
+ export { TransportProvider } from "./transport-provider.js";
5
+ export { bindAwareness } from "./bind-awareness.js";
6
+ export { extractMentions, resolveMentions, notifyMention } from "./mentions.js";
7
+ export { BranchDoc } from "./branch-doc.js";
8
+ // Y.UndoManager-based history backend.
9
+ export { YjsHistory } from "./yjs-history.js";
10
+ // Client-side AES-GCM for blind-relay collab.
11
+ export { generateRoomKey, importRoomKey, EncryptedTransport, } from "./encryption.js";
12
+ export { ROOM_ID_BYTES, ENCRYPTION_KEY_BITS, ENCRYPTION_IV_BYTES, } from "./constants.js";
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,eAAe,EAA4B,MAAM,gBAAgB,CAAC;AAC3E,OAAO,EAAE,iBAAiB,EAAiC,MAAM,yBAAyB,CAAC;AAC3F,OAAO,EAAE,aAAa,EAA6B,MAAM,qBAAqB,CAAC;AAC/E,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAWhF,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAE5C,uCAAuC;AACvC,OAAO,EAAE,UAAU,EAA0B,MAAM,kBAAkB,CAAC;AAEtE,8CAA8C;AAC9C,OAAO,EACL,eAAe,EACf,aAAa,EACb,kBAAkB,GAEnB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,aAAa,EACb,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,33 @@
1
+ import type { Peer } from "./awareness.js";
2
+ /**
3
+ * Extract `@username` tokens from a comment body. Matches alphanumeric
4
+ * + underscore handles after `@`, lowercased for case-insensitive
5
+ * lookup. Anchors on a word boundary so e-mail addresses (`foo@bar.com`)
6
+ * don't false-match.
7
+ */
8
+ export declare const extractMentions: (body: string) => readonly string[];
9
+ /**
10
+ * Match mention tokens against the peer list (case-insensitive on the
11
+ * peer's display name, with whitespace stripped). Returns matched
12
+ * peers — duplicate filter applied so each peer appears at most once
13
+ * even if mentioned several times.
14
+ */
15
+ export declare const resolveMentions: (mentions: readonly string[], peers: readonly Peer[]) => Peer[];
16
+ /**
17
+ * Fire a browser Notification if the local user has been mentioned.
18
+ * `notif` is the platform's `Notification` constructor (host passes
19
+ * `globalThis.Notification` in browser code). No-op when not granted
20
+ * or constructor missing — keeps `@collab` portable to Node.
21
+ */
22
+ export declare const notifyMention: (notif: {
23
+ new (title: string, options?: {
24
+ body?: string;
25
+ tag?: string;
26
+ }): unknown;
27
+ permission: string;
28
+ } | undefined, opts: {
29
+ readonly authorName: string;
30
+ readonly body: string;
31
+ readonly room?: string;
32
+ }) => void;
33
+ //# sourceMappingURL=mentions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mentions.d.ts","sourceRoot":"","sources":["../src/mentions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAE3C;;;;;GAKG;AACH,eAAO,MAAM,eAAe,GAAI,MAAM,MAAM,KAAG,SAAS,MAAM,EAS7D,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,eAAe,GAAI,UAAU,SAAS,MAAM,EAAE,EAAE,OAAO,SAAS,IAAI,EAAE,KAAG,IAAI,EAczF,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,aAAa,GACxB,OAAO;IAAE,KAAK,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,EAClH,MAAM;IACJ,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB,KACA,IAQF,CAAC"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Extract `@username` tokens from a comment body. Matches alphanumeric
3
+ * + underscore handles after `@`, lowercased for case-insensitive
4
+ * lookup. Anchors on a word boundary so e-mail addresses (`foo@bar.com`)
5
+ * don't false-match.
6
+ */
7
+ export const extractMentions = (body) => {
8
+ const out = [];
9
+ const re = /(^|\s)@(\w+)/g;
10
+ let match;
11
+ while ((match = re.exec(body)) !== null) {
12
+ const handle = match[2];
13
+ if (handle !== undefined)
14
+ out.push(handle.toLowerCase());
15
+ }
16
+ return out;
17
+ };
18
+ /**
19
+ * Match mention tokens against the peer list (case-insensitive on the
20
+ * peer's display name, with whitespace stripped). Returns matched
21
+ * peers — duplicate filter applied so each peer appears at most once
22
+ * even if mentioned several times.
23
+ */
24
+ export const resolveMentions = (mentions, peers) => {
25
+ const seen = new Set();
26
+ const out = [];
27
+ for (const tag of mentions) {
28
+ for (const p of peers) {
29
+ if (seen.has(p.clientId))
30
+ continue;
31
+ const normalised = p.user.name.replace(/\s+/g, "").toLowerCase();
32
+ if (normalised === tag) {
33
+ out.push(p);
34
+ seen.add(p.clientId);
35
+ }
36
+ }
37
+ }
38
+ return out;
39
+ };
40
+ /**
41
+ * Fire a browser Notification if the local user has been mentioned.
42
+ * `notif` is the platform's `Notification` constructor (host passes
43
+ * `globalThis.Notification` in browser code). No-op when not granted
44
+ * or constructor missing — keeps `@collab` portable to Node.
45
+ */
46
+ export const notifyMention = (notif, opts) => {
47
+ if (!notif)
48
+ return;
49
+ if (notif.permission !== "granted")
50
+ return;
51
+ const title = `${opts.authorName} mentioned you`;
52
+ new notif(title, {
53
+ body: opts.body.slice(0, 140),
54
+ tag: opts.room ?? "diagram-mention",
55
+ });
56
+ };
57
+ //# sourceMappingURL=mentions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mentions.js","sourceRoot":"","sources":["../src/mentions.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,IAAY,EAAqB,EAAE;IACjE,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,MAAM,EAAE,GAAG,eAAe,CAAC;IAC3B,IAAI,KAA6B,CAAC;IAClC,OAAO,CAAC,KAAK,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;QACxC,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACxB,IAAI,MAAM,KAAK,SAAS;YAAE,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;IAC3D,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,QAA2B,EAAE,KAAsB,EAAU,EAAE;IAC7F,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,GAAG,GAAW,EAAE,CAAC;IACvB,KAAK,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;QAC3B,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;YACtB,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC;gBAAE,SAAS;YACnC,MAAM,UAAU,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;YACjE,IAAI,UAAU,KAAK,GAAG,EAAE,CAAC;gBACvB,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;gBACZ,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC;YACvB,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC,CAAC;AAEF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAC3B,KAAkH,EAClH,IAIC,EACK,EAAE;IACR,IAAI,CAAC,KAAK;QAAE,OAAO;IACnB,IAAI,KAAK,CAAC,UAAU,KAAK,SAAS;QAAE,OAAO;IAC3C,MAAM,KAAK,GAAG,GAAG,IAAI,CAAC,UAAU,gBAAgB,CAAC;IACjD,IAAI,KAAK,CAAC,KAAK,EAAE;QACf,IAAI,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC;QAC7B,GAAG,EAAE,IAAI,CAAC,IAAI,IAAI,iBAAiB;KACpC,CAAC,CAAC;AACL,CAAC,CAAC"}
@@ -0,0 +1,59 @@
1
+ import type { ElementId } from "@oh-just-another/types";
2
+ import type { Scene } from "@oh-just-another/scene";
3
+ /**
4
+ * CRDT branch merge editor-facing API. The runtime implementation
5
+ * lives on Y.Doc subdocs; this contract lets hosts wire UI
6
+ * (`<MergeDialog>`) against a stable surface.
7
+ *
8
+ * Branch model:
9
+ * Each named branch = its own Y.Doc subdoc inside the parent.
10
+ * Snapshots inside a branch are versioned per its own history;
11
+ * merging a source branch into a target performs a three-way
12
+ * merge against their common ancestor.
13
+ */
14
+ export interface BranchId {
15
+ readonly id: string;
16
+ readonly name: string;
17
+ readonly parentVersionId: string | null;
18
+ }
19
+ export interface MergeConflict {
20
+ readonly elementId: ElementId;
21
+ readonly base: unknown;
22
+ readonly source: unknown;
23
+ readonly target: unknown;
24
+ }
25
+ export interface MergeReport {
26
+ readonly applied: readonly ElementId[];
27
+ readonly conflicts: readonly MergeConflict[];
28
+ /**
29
+ * The resulting scene after auto-applying non-conflicting
30
+ * changes. Conflicts are NOT applied — the host UI resolves
31
+ * them and produces a final scene via `applyConflictResolution`.
32
+ */
33
+ readonly autoMerged: Scene;
34
+ }
35
+ /**
36
+ * Resolution choice made by the user in `<MergeDialog>`.
37
+ *
38
+ * - `ours` keep the target version of the shape.
39
+ * - `theirs` keep the source version of the shape.
40
+ * - `both` keep both — `theirs` lands as a duplicate with a
41
+ * "-copy" suffixed id. Convention not contract;
42
+ * hosts can supply their own id-rewriter.
43
+ */
44
+ export type ConflictChoice = "ours" | "theirs" | "both";
45
+ export interface ConflictResolution {
46
+ readonly elementId: ElementId;
47
+ readonly choice: ConflictChoice;
48
+ }
49
+ /**
50
+ * Implementation hook. The kernel does NOT ship one — collab
51
+ * authors plug their Yjs-backed implementation into a host
52
+ * editor instance.
53
+ */
54
+ export interface BranchMergeAPI {
55
+ branchToDoc(branchId: BranchId): unknown;
56
+ mergeBranch(source: BranchId, target: BranchId): Promise<MergeReport>;
57
+ applyConflictResolution(report: MergeReport, resolutions: readonly ConflictResolution[]): Promise<Scene>;
58
+ }
59
+ //# sourceMappingURL=merge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge.d.ts","sourceRoot":"","sources":["../src/merge.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,wBAAwB,CAAC;AAEpD;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,QAAQ;IACvB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CACzC;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,OAAO,EAAE,SAAS,SAAS,EAAE,CAAC;IACvC,QAAQ,CAAC,SAAS,EAAE,SAAS,aAAa,EAAE,CAAC;IAC7C;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,KAAK,CAAC;CAC5B;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC;AAExD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;CACjC;AAED;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,WAAW,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC;IACzC,WAAW,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IACtE,uBAAuB,CACrB,MAAM,EAAE,WAAW,EACnB,WAAW,EAAE,SAAS,kBAAkB,EAAE,GACzC,OAAO,CAAC,KAAK,CAAC,CAAC;CACnB"}
package/dist/merge.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=merge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge.js","sourceRoot":"","sources":["../src/merge.ts"],"names":[],"mappings":""}
@@ -0,0 +1,41 @@
1
+ import * as Y from "yjs";
2
+ import { type Annotation, type Link, type Layer, type Scene, type Element } from "@oh-just-another/scene";
3
+ /**
4
+ * CRDT-backed mirror of a `Scene`. Wraps a `Y.Doc` whose top-level maps
5
+ * are the canonical source of truth for collaborative editing:
6
+ *
7
+ * - `elements` — `Y.Map<string, Element>`
8
+ * - `links` — `Y.Map<string, Link>`
9
+ * - `layers` — `Y.Map<string, Layer>`
10
+ * - `viewport` — `Y.Map<string, unknown>` (single "current" key)
11
+ *
12
+ * Elements / links / layers are stored as deep-cloned JSON snapshots — Yjs
13
+ * happily ships any structured-cloneable object. Concurrent edits to
14
+ * different ids merge automatically (last-writer-wins per id, classic
15
+ * Yjs `Y.Map` semantics).
16
+ */
17
+ export declare class SceneDoc {
18
+ readonly doc: Y.Doc;
19
+ readonly elements: Y.Map<Element>;
20
+ readonly links: Y.Map<Link>;
21
+ readonly layers: Y.Map<Layer>;
22
+ readonly annotations: Y.Map<Annotation>;
23
+ readonly viewport: Y.Map<unknown>;
24
+ constructor(doc?: Y.Doc);
25
+ /** Build an in-memory `Scene` snapshot from the current CRDT state. */
26
+ snapshot(): Scene;
27
+ /**
28
+ * Replace the entire CRDT state with the given `Scene`. Wrapped in a
29
+ * single Yjs transaction so peers receive one update message instead of
30
+ * one per shape. Marks the transaction with `origin` so observers can
31
+ * skip the event they themselves caused.
32
+ */
33
+ replace(scene: Scene, origin?: unknown): void;
34
+ /**
35
+ * Apply only the *delta* between an old scene and a new one. Cheaper
36
+ * over the wire than `replace` when a single shape changes. Used by
37
+ * `bindEditor` to ferry every editor mutation into the CRDT.
38
+ */
39
+ applyDelta(prev: Scene, next: Scene, origin?: unknown): void;
40
+ }
41
+ //# sourceMappingURL=scene-doc.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scene-doc.d.ts","sourceRoot":"","sources":["../src/scene-doc.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC;AACzB,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,IAAI,EACT,KAAK,KAAK,EACV,KAAK,KAAK,EACV,KAAK,OAAO,EACb,MAAM,wBAAwB,CAAC;AAGhC;;;;;;;;;;;;;GAaG;AACH,qBAAa,QAAQ;IACnB,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAClC,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAC9B,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;IACxC,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;gBAEtB,GAAG,GAAE,CAAC,CAAC,GAAiB;IASpC,uEAAuE;IACvE,QAAQ,IAAI,KAAK;IA2BjB;;;;;OAKG;IACH,OAAO,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI;IAc7C;;;;OAIG;IACH,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI;CAW7D"}
@@ -0,0 +1,111 @@
1
+ import * as Y from "yjs";
2
+ import { DEFAULT_VIEWPORT, } from "@oh-just-another/scene";
3
+ import { annotationId, layerId, linkId, elementId } from "@oh-just-another/types";
4
+ /**
5
+ * CRDT-backed mirror of a `Scene`. Wraps a `Y.Doc` whose top-level maps
6
+ * are the canonical source of truth for collaborative editing:
7
+ *
8
+ * - `elements` — `Y.Map<string, Element>`
9
+ * - `links` — `Y.Map<string, Link>`
10
+ * - `layers` — `Y.Map<string, Layer>`
11
+ * - `viewport` — `Y.Map<string, unknown>` (single "current" key)
12
+ *
13
+ * Elements / links / layers are stored as deep-cloned JSON snapshots — Yjs
14
+ * happily ships any structured-cloneable object. Concurrent edits to
15
+ * different ids merge automatically (last-writer-wins per id, classic
16
+ * Yjs `Y.Map` semantics).
17
+ */
18
+ export class SceneDoc {
19
+ doc;
20
+ elements;
21
+ links;
22
+ layers;
23
+ annotations;
24
+ viewport;
25
+ constructor(doc = new Y.Doc()) {
26
+ this.doc = doc;
27
+ this.elements = doc.getMap("elements");
28
+ this.links = doc.getMap("links");
29
+ this.layers = doc.getMap("layers");
30
+ this.annotations = doc.getMap("annotations");
31
+ this.viewport = doc.getMap("viewport");
32
+ }
33
+ /** Build an in-memory `Scene` snapshot from the current CRDT state. */
34
+ snapshot() {
35
+ const shapeMap = new Map();
36
+ for (const [id, shape] of this.elements)
37
+ shapeMap.set(elementId(id), shape);
38
+ const edgeMap = new Map();
39
+ for (const [id, edge] of this.links)
40
+ edgeMap.set(linkId(id), edge);
41
+ const layerMap = new Map();
42
+ for (const [id, layer] of this.layers)
43
+ layerMap.set(layerId(id), layer);
44
+ const vp = this.viewport.get("current");
45
+ const viewport = (vp ?? DEFAULT_VIEWPORT);
46
+ const annotationMap = new Map();
47
+ for (const [id, ann] of this.annotations)
48
+ annotationMap.set(annotationId(id), ann);
49
+ return {
50
+ elements: shapeMap,
51
+ links: edgeMap,
52
+ layers: layerMap,
53
+ annotations: annotationMap,
54
+ // BinaryFile registry isn't CRDT-replicated (large bytes, awkward
55
+ // through Yjs); hosts that need collab on file uploads keep their
56
+ // own sidecar transport. Snapshot returns an empty map.
57
+ files: new Map(),
58
+ viewport,
59
+ };
60
+ }
61
+ /**
62
+ * Replace the entire CRDT state with the given `Scene`. Wrapped in a
63
+ * single Yjs transaction so peers receive one update message instead of
64
+ * one per shape. Marks the transaction with `origin` so observers can
65
+ * skip the event they themselves caused.
66
+ */
67
+ replace(scene, origin) {
68
+ this.doc.transact(() => {
69
+ this.elements.clear();
70
+ for (const [id, shape] of scene.elements)
71
+ this.elements.set(id, shape);
72
+ this.links.clear();
73
+ for (const [id, edge] of scene.links)
74
+ this.links.set(id, edge);
75
+ this.layers.clear();
76
+ for (const [id, layer] of scene.layers)
77
+ this.layers.set(id, layer);
78
+ this.annotations.clear();
79
+ for (const [id, ann] of scene.annotations)
80
+ this.annotations.set(id, ann);
81
+ this.viewport.set("current", scene.viewport);
82
+ }, origin);
83
+ }
84
+ /**
85
+ * Apply only the *delta* between an old scene and a new one. Cheaper
86
+ * over the wire than `replace` when a single shape changes. Used by
87
+ * `bindEditor` to ferry every editor mutation into the CRDT.
88
+ */
89
+ applyDelta(prev, next, origin) {
90
+ this.doc.transact(() => {
91
+ diffMap(prev.elements, next.elements, this.elements);
92
+ diffMap(prev.links, next.links, this.links);
93
+ diffMap(prev.layers, next.layers, this.layers);
94
+ diffMap(prev.annotations, next.annotations, this.annotations);
95
+ if (prev.viewport !== next.viewport) {
96
+ this.viewport.set("current", next.viewport);
97
+ }
98
+ }, origin);
99
+ }
100
+ }
101
+ const diffMap = (prev, next, target) => {
102
+ for (const [id] of prev) {
103
+ if (!next.has(id))
104
+ target.delete(id);
105
+ }
106
+ for (const [id, value] of next) {
107
+ if (prev.get(id) !== value)
108
+ target.set(id, value);
109
+ }
110
+ };
111
+ //# sourceMappingURL=scene-doc.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scene-doc.js","sourceRoot":"","sources":["../src/scene-doc.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,21 @@
1
+ import * as Y from "yjs";
2
+ import { type Awareness } from "y-protocols/awareness";
3
+ import type { Transport } from "@oh-just-another/network";
4
+ export interface TransportProviderOptions {
5
+ readonly doc: Y.Doc;
6
+ readonly transport: Transport;
7
+ readonly awareness?: Awareness;
8
+ }
9
+ export declare class TransportProvider {
10
+ private readonly doc;
11
+ private readonly transport;
12
+ private readonly awareness;
13
+ private readonly origin;
14
+ private readonly unsubscribeMessage;
15
+ constructor(options: TransportProviderOptions);
16
+ destroy(): void;
17
+ private readonly onInbound;
18
+ private readonly onDocUpdate;
19
+ private readonly onAwarenessUpdate;
20
+ }
21
+ //# sourceMappingURL=transport-provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport-provider.d.ts","sourceRoot":"","sources":["../src/transport-provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC;AACzB,OAAO,EAA+C,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACpG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AA2B1D,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC;IACpB,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;CAChC;AAED,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAQ;IAC5B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAmB;IAC7C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgC;IACvD,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAa;gBAEpC,OAAO,EAAE,wBAAwB;IAkB7C,OAAO,IAAI,IAAI;IAQf,OAAO,CAAC,QAAQ,CAAC,SAAS,CA0BxB;IAIF,OAAO,CAAC,QAAQ,CAAC,WAAW,CAG1B;IAEF,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAShC;CACH"}
@@ -0,0 +1,105 @@
1
+ import * as Y from "yjs";
2
+ import { applyAwarenessUpdate, encodeAwarenessUpdate } from "y-protocols/awareness";
3
+ /**
4
+ * Bridge a `Y.Doc` (and optional `Awareness`) onto a `Transport`. Every
5
+ * Yjs update is wrapped in a tiny 1-byte tag so a single channel can
6
+ * multiplex document and awareness messages.
7
+ *
8
+ * tag 0x00 → Yjs document update
9
+ * tag 0x01 → awareness update
10
+ * tag 0x02 → state-sync request (joiner asks for the current full state)
11
+ *
12
+ * Use:
13
+ *
14
+ * ```ts
15
+ * const provider = new TransportProvider({
16
+ * doc,
17
+ * awareness: new CollabAwareness(doc).awareness,
18
+ * transport: new BroadcastChannelTransport("room-foo"),
19
+ * });
20
+ * // …later
21
+ * provider.destroy();
22
+ * ```
23
+ */
24
+ const TAG_DOC = 0;
25
+ const TAG_AWARENESS = 1;
26
+ const TAG_SYNC_REQUEST = 2;
27
+ export class TransportProvider {
28
+ doc;
29
+ transport;
30
+ awareness;
31
+ origin = Symbol("transport-provider");
32
+ unsubscribeMessage;
33
+ constructor(options) {
34
+ this.doc = options.doc;
35
+ this.transport = options.transport;
36
+ this.awareness = options.awareness ?? null;
37
+ // Forward local doc updates downstream — but only ones we didn't
38
+ // ourselves apply (avoid echo).
39
+ this.doc.on("updateV2", this.onDocUpdate);
40
+ if (this.awareness) {
41
+ this.awareness.on("update", this.onAwarenessUpdate);
42
+ }
43
+ this.unsubscribeMessage = this.transport.onMessage(this.onInbound);
44
+ // Ask peers for their current state on join.
45
+ this.transport.send(new Uint8Array([TAG_SYNC_REQUEST]));
46
+ }
47
+ destroy() {
48
+ this.doc.off("updateV2", this.onDocUpdate);
49
+ if (this.awareness)
50
+ this.awareness.off("update", this.onAwarenessUpdate);
51
+ this.unsubscribeMessage();
52
+ }
53
+ // --- inbound ---
54
+ onInbound = (payload) => {
55
+ const tag = payload[0];
56
+ if (tag === undefined)
57
+ return;
58
+ const body = payload.subarray(1);
59
+ switch (tag) {
60
+ case TAG_DOC:
61
+ Y.applyUpdateV2(this.doc, body, this.origin);
62
+ return;
63
+ case TAG_AWARENESS:
64
+ if (this.awareness)
65
+ applyAwarenessUpdate(this.awareness, body, this.origin);
66
+ return;
67
+ case TAG_SYNC_REQUEST: {
68
+ // Reply with our full doc state + awareness so the joiner catches up.
69
+ const docPayload = Y.encodeStateAsUpdateV2(this.doc);
70
+ this.transport.send(prependTag(TAG_DOC, docPayload));
71
+ if (this.awareness) {
72
+ const clients = [...this.awareness.getStates().keys()];
73
+ const awarenessPayload = encodeAwarenessUpdate(this.awareness, clients);
74
+ this.transport.send(prependTag(TAG_AWARENESS, awarenessPayload));
75
+ }
76
+ return;
77
+ }
78
+ default:
79
+ // Unknown tag — silently ignore; lets the wire format extend later.
80
+ return;
81
+ }
82
+ };
83
+ // --- outbound ---
84
+ onDocUpdate = (update, originOfUpdate) => {
85
+ if (originOfUpdate === this.origin)
86
+ return;
87
+ this.transport.send(prependTag(TAG_DOC, update));
88
+ };
89
+ onAwarenessUpdate = (_, originOfUpdate) => {
90
+ if (originOfUpdate === this.origin)
91
+ return;
92
+ if (!this.awareness)
93
+ return;
94
+ const changedClients = [..._.added, ..._.updated, ..._.removed];
95
+ const payload = encodeAwarenessUpdate(this.awareness, changedClients);
96
+ this.transport.send(prependTag(TAG_AWARENESS, payload));
97
+ };
98
+ }
99
+ const prependTag = (tag, body) => {
100
+ const out = new Uint8Array(body.length + 1);
101
+ out[0] = tag;
102
+ out.set(body, 1);
103
+ return out;
104
+ };
105
+ //# sourceMappingURL=transport-provider.js.map