@oh-just-another/collab 0.57.0 → 0.58.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/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # @oh-just-another/collab
2
2
 
3
+ ## 0.58.0
4
+
5
+ ### Minor Changes
6
+
7
+ - d1627f2: Renamed `YjsHistory` / `YjsHistoryOptions` to `CollabHistory` / `CollabHistoryOptions`, so the public name describes the role (collaborative undo scoped to the local client) rather than the backing implementation.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [b4b252b]
12
+ - Updated dependencies [d20d50a]
13
+ - Updated dependencies [0152ed6]
14
+ - Updated dependencies [938e7c8]
15
+ - Updated dependencies [9673846]
16
+ - Updated dependencies [f370dba]
17
+ - Updated dependencies [8f00738]
18
+ - Updated dependencies [da91d59]
19
+ - Updated dependencies [3152317]
20
+ - Updated dependencies [fc47ecc]
21
+ - Updated dependencies [8fc6b69]
22
+ - Updated dependencies [f98730f]
23
+ - Updated dependencies [904cc09]
24
+ - Updated dependencies [edde5d0]
25
+ - Updated dependencies [1c7cc6c]
26
+ - Updated dependencies [c5be6e5]
27
+ - @oh-just-another/state@0.59.0
28
+ - @oh-just-another/scene@0.59.0
29
+ - @oh-just-another/history@0.57.2
30
+
31
+ ## 0.57.1
32
+
33
+ ### Patch Changes
34
+
35
+ - Updated dependencies [d1b96d9]
36
+ - @oh-just-another/scene@0.58.0
37
+ - @oh-just-another/state@0.58.0
38
+ - @oh-just-another/history@0.57.1
39
+ - @oh-just-another/serialization@0.57.1
40
+
3
41
  ## 0.57.0
4
42
 
5
43
  ### Minor Changes
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # @oh-just-another/collab
2
2
 
3
- Real-time collaboration on `@oh-just-another/scene` documents — Yjs CRDT binding for the scene + awareness (presence) for cursors and peer lists. Pair with a `@oh-just-another/network` transport (BroadcastChannel for tabs, WebSocket for cross-machine).
3
+ CRDT collaboration layer for `@oh-just-another/scene` documents.
4
+
5
+ Real-time multi-peer editing built on Yjs (L4): a CRDT-backed scene mirror, presence/awareness for cursors and selections, mentions, branch-and-merge, CRDT-aware undo, and client-side encryption. Pair with a `@oh-just-another/network` transport (BroadcastChannel for tabs, WebSocket for cross-machine).
4
6
 
5
7
  ## Install
6
8
 
@@ -8,29 +10,37 @@ Real-time collaboration on `@oh-just-another/scene` documents — Yjs CRDT bindi
8
10
  pnpm add @oh-just-another/collab
9
11
  ```
10
12
 
11
- Direct deps: `yjs` and `y-protocols`.
13
+ Peer deps: `yjs` and `y-protocols`.
12
14
 
13
15
  ## Quick start
14
16
 
15
- Same-tab demo (e.g. for testing):
16
-
17
17
  ```ts
18
18
  import * as Y from "yjs";
19
19
  import { Editor } from "@oh-just-another/state";
20
20
  import { BroadcastChannelTransport } from "@oh-just-another/network";
21
- import { SceneDoc, CollabAwareness, TransportProvider, bindEditor } from "@oh-just-another/collab";
21
+ import {
22
+ SceneDoc,
23
+ CollabAwareness,
24
+ TransportProvider,
25
+ bindEditor,
26
+ bindAwareness,
27
+ } from "@oh-just-another/collab";
22
28
 
23
29
  const doc = new Y.Doc();
24
30
  const sceneDoc = new SceneDoc(doc);
25
31
  const awareness = new CollabAwareness(doc);
26
- awareness.updateLocal({ user: { id: "me", name: "Alice", color: "#1a73e8" } });
27
32
 
28
33
  const transport = new BroadcastChannelTransport("room-foo");
29
34
  const provider = new TransportProvider({ doc, transport, awareness: awareness.awareness });
30
35
 
31
- const unbind = bindEditor(editor, sceneDoc);
36
+ const unbindScene = bindEditor(editor, sceneDoc);
37
+ const unbindPresence = bindAwareness(editor, awareness, {
38
+ user: { id: "me", name: "Alice", color: "#1a73e8" },
39
+ });
40
+
32
41
  // …later
33
- unbind();
42
+ unbindPresence();
43
+ unbindScene();
34
44
  provider.destroy();
35
45
  awareness.destroy();
36
46
  transport.close();
@@ -39,19 +49,76 @@ doc.destroy();
39
49
 
40
50
  ## API
41
51
 
42
- | Name | Purpose |
43
- | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
44
- | `SceneDoc(doc?)` | Wraps `Y.Doc` exposing `shapes` / `edges` / `layers` / `viewport` maps. `snapshot()`, `replace(scene)`, `applyDelta(...)`. |
45
- | `bindEditor(editor, sceneDoc)` | Wire an `Editor` to a `SceneDoc`. Returns unbind. |
46
- | `CollabAwareness(doc)` | Typed wrapper over `y-protocols/awareness`: `updateLocal`, `getPeers` / `getOthers` / `onPeers`. |
47
- | `Peer`, `PeerUser` | Public types for presence payload. |
48
- | `TransportProvider({ doc, transport, awareness? })` | Bridge `Y.Doc` (+ awareness) onto a `Transport`. Multiplexes doc / awareness / sync-request over one channel. |
52
+ ### Document & transport
53
+
54
+ | Name | Purpose |
55
+ | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
56
+ | `SceneDoc(doc?)` | CRDT-backed scene mirror over a `Y.Doc`: `elements` / `links` / `layers` / `annotations` / `viewport` maps. `snapshot()`, `replace(scene)`, `applyDelta(...)`. |
57
+ | `bindEditor(editor, sceneDoc)` | Wire an `Editor` to a `SceneDoc`. Self-origin filtered. Returns an unbind function. |
58
+ | `TransportProvider({ doc, transport, awareness? })` | Bridge `Y.Doc` (+ optional awareness) onto a `Transport`. Multiplexes doc / awareness / sync-request over one channel via a 1-byte tag. |
59
+ | `TransportProviderOptions` | Options type for the provider constructor. |
60
+
61
+ ### Awareness (presence)
62
+
63
+ | Name | Purpose |
64
+ | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
65
+ | `CollabAwareness(doc)` | Typed wrapper over `y-protocols/awareness`: `updateLocal`, `getPeers` / `getOthers` / `onPeers`, `clientId`. |
66
+ | `bindAwareness(editor, awareness, opts)` | Publish local user / cursor / selection into awareness (throttled) and paint peer cursors + selection halos back into the editor. Returns an unbind function. |
67
+ | `BindAwarenessOptions` | `{ user: PeerUser; cursorThrottleMs?: number }`. |
68
+ | `Peer`, `PeerUser` | Presence payload types (clientId, user identity, cursor, selection, free-form `extra`). |
69
+
70
+ ### Mentions
71
+
72
+ | Name | Purpose |
73
+ | ----------------------------------- | ----------------------------------------------------------------------------------------------------------- |
74
+ | `extractMentions(body)` | Pull `@handle` tokens out of a comment body (lowercased, e-mail-safe). |
75
+ | `resolveMentions(mentions, peers)` | Match tokens against the peer list by display name; deduped `Peer[]`. |
76
+ | `notifyMention(Notification, opts)` | Fire a browser `Notification` when the local user is mentioned. No-op outside the browser / unless granted. |
77
+
78
+ ### Branch & merge
79
+
80
+ | Name | Purpose |
81
+ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
82
+ | `BranchDoc(doc?)` | Named branches as Yjs subdocs of one parent doc. `ensureRoot`, `createBranch`, `sceneDocFor`, `mergeBranch`, `applyConflictResolution`, `commitMerge`. Implements `BranchMergeAPI`. |
83
+ | `BranchMergeAPI` | Editor-facing branch/merge contract (`branchToDoc`, `mergeBranch`, `applyConflictResolution`). |
84
+ | `BranchId` | `{ id, name, parentVersionId }`. |
85
+ | `MergeReport` | Three-way merge result: `applied`, `conflicts`, `autoMerged` scene. |
86
+ | `MergeConflict` | Per-element conflict: `base` / `source` / `target` values. |
87
+ | `ConflictChoice` | `"ours" \| "theirs" \| "both"`. |
88
+ | `ConflictResolution` | `{ elementId, choice }` — a user's resolution from the merge UI. |
89
+
90
+ ### History
91
+
92
+ | Name | Purpose |
93
+ | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
94
+ | `CollabHistory(sceneDoc, opts?)` | CRDT-aware `HistoryProvider` backed by `Y.UndoManager`. Scopes undo/redo to the local client and emits coalesced patches to peers. Pass as `Editor`'s `history`. |
95
+ | `CollabHistoryOptions` | `{ captureLimit?: number; captureTimeout?: number }` (transaction coalescing window). |
96
+
97
+ ### Encryption
98
+
99
+ | Name | Purpose |
100
+ | --------------------------------------- | ----------------------------------------------------------------------------------------------------- |
101
+ | `generateRoomKey()` | Mint `{ roomId, keyBase64, key }` for a new session via WebCrypto. |
102
+ | `importRoomKey(keyBase64)` | Re-import an AES key parsed from an invite URL fragment. Throws on bad length / base64. |
103
+ | `EncryptedTransport(inner, key, opts?)` | Wrap any `Transport` so payloads are AES-GCM encrypted/decrypted; a blind relay sees only ciphertext. |
104
+ | `RoomCredentials` | Result type of `generateRoomKey`. |
105
+
106
+ ### Constants
107
+
108
+ | Name | Value | Meaning |
109
+ | --------------------- | ----- | ----------------------------------------------- |
110
+ | `ROOM_ID_BYTES` | `10` | Public room id length in bytes (20-hex string). |
111
+ | `ENCRYPTION_KEY_BITS` | `128` | AES-GCM session-key length in bits. |
112
+ | `ENCRYPTION_IV_BYTES` | `12` | AES-GCM initialisation-vector length in bytes. |
49
113
 
50
114
  ## Design notes
51
115
 
52
- - **CRDT as the source of truth.** `SceneDoc.snapshot()` rebuilds a typed `Scene` from `Y.Map`s on demand. We don't try to incrementally patch the local scene from individual Yjs events `editor.loadScene(snapshot)` after every remote update is simpler and within budget for typical scene sizes.
53
- - **`Y.Map<id, Shape>` per kind**, not nested `Y.Map`s. Concurrent edits to different ids merge under classic CRDT last-writer-wins semantics. Per-shape conflict resolution (two peers editing the same shape) is also LWW today — fine for editor UX where one peer's drag obviously stops the moment the other commits.
54
- - **Undo history dropped on remote updates (MVP).** `editor.history` is a linear local stack; CRDT-style multi-author undo needs a `Y.UndoManager`-backed history. Promoted to a follow-up phase.
55
- - **Self-origin filter on Yjs transactions.** `bindEditor` tags its own writes so the `update` listener doesn't ricochetwithout it every keystroke would flicker through `loadScene`.
56
- - **Transport-agnostic.** The same `SceneDoc` works behind BroadcastChannel (same-origin tabs) or WebSocket (any server speaking y-protocols). Sharded WebRTC / WebTransport implementations slot in without touching the binding.
57
- - **Single-tag wire format.** `TransportProvider` prepends one byte to every message: `0x00` doc / `0x01` awareness / `0x02` sync-request. Lets us multiplex over a single binary channel without a separate signalling layer.
116
+ - **CRDT as the source of truth.** `SceneDoc.snapshot()` rebuilds a typed `Scene` from the `Y.Map`s on demand. Rather than incrementally patching the local scene from individual Yjs events, `editor.loadScene(snapshot)` runs after each remote update simpler and within budget for typical scene sizes.
117
+ - **`Y.Map<id, value>` per kind.** Concurrent edits to different ids merge under last-writer-wins. Same-id conflicts are also LWW today — fine for editor UX where one peer's drag stops the moment the other commits.
118
+ - **Self-origin filter.** `bindEditor` and `CollabHistory` tag their own writes so the `update` listener doesn't ricochet through `loadScene`.
119
+ - **Local-scoped undo.** `CollabHistory` tracks only the local origin in `Y.UndoManager`, so undo affects this client's changesnot a peer's. The rewind diff is emitted to peers as one coalesced patch through the same `applyDelta` path `bindEditor` uses.
120
+ - **Branches as subdocs.** Each branch is an independent Yjs subdoc that loads/unloads on demand and replicates over the same provider. Merges are three-way against the source branch's stored ancestor; conflicts wait for host resolution.
121
+ - **Blind-relay encryption.** The AES key lives in the URL fragment, which the browser never transmits — including on the WebSocket upgrade — so the relay routes by `roomId` without ever seeing plaintext. Random IV per frame; tampered frames fail the auth tag and are dropped.
122
+ - **Single-tag wire format.** `TransportProvider` prepends one byte per message: `0x00` doc / `0x01` awareness / `0x02` sync-request — multiplexing over a single binary channel without a separate signalling layer.
123
+
124
+ See [ohjustanother.site](https://ohjustanother.site) for the full docs.