@mocanvas/sync 1.0.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 Symbio Digital
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,142 @@
1
+ # @mocanvas/sync
2
+
3
+ Multiplayer for a mocanvas store: document changes travel as record diffs,
4
+ cursors and selections travel as presence records.
5
+
6
+ Part of [mocanvas](https://github.com/SYMBIO/mocanvas).
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ npm install @mocanvas/sync react
12
+ ```
13
+
14
+ `react` (>= 18) is a peer dependency, used by `useSync` and
15
+ `<CollaboratorCursors />`.
16
+
17
+ ## Quick start
18
+
19
+ ```ts
20
+ import { createSyncClient, createBroadcastChannelTransport } from "@mocanvas/sync"
21
+
22
+ const client = createSyncClient({
23
+ store: editor.store,
24
+ roomId: "my-doc",
25
+ transport: createBroadcastChannelTransport("my-doc"),
26
+ presence: { editor },
27
+ })
28
+ client.connect()
29
+ ```
30
+
31
+ `getStatus()` returns a signal holding `"offline" | "connecting" | "online"`,
32
+ so it can be read from a reactor or with `useValue`. `dispose()` disconnects
33
+ and closes the transport.
34
+
35
+ ## Integration
36
+
37
+ Three lines in an app that already renders `<Mocanvas>`:
38
+
39
+ ```tsx
40
+ const { status } = useSync(editor, { roomId, transport: () => createBroadcastChannelTransport(roomId) })
41
+ // ...inside <Mocanvas onMount={setEditor}>:
42
+ {editor ? <CollaboratorCursors editor={editor} /> : null}
43
+ ```
44
+
45
+ `CollaboratorCursors` draws every other person's pointer, name chip and
46
+ selection outlines in screen space. It positions itself absolutely, so it goes
47
+ anywhere inside the canvas container.
48
+
49
+ ## Protocol
50
+
51
+ Every message is one JSON object on the wire.
52
+
53
+ | message | fields | meaning |
54
+ | ---------- | ---------------------------- | ---------------------------------------------------- |
55
+ | `hello` | `clientId`, `version` | I joined. Established peers answer with a `snapshot`. |
56
+ | `snapshot` | `records` | Every document-scope record as the sender sees them. |
57
+ | `diff` | `clientId`, `seq`, `diff` | One squashed `RecordsDiff` of document records. |
58
+ | `presence` | `clientId`, `record` | The sender's `instance_presence` record. |
59
+ | `bye` | `clientId` | I am leaving; drop my presence. |
60
+
61
+ - Outgoing diffs come from `store.listen(cb, { source: "user", scope: "document" })`,
62
+ so nothing applied from a peer is ever echoed back and nothing in `session`
63
+ or `presence` scope is persisted into the document stream.
64
+ - Incoming diffs are applied inside
65
+ `store.mergeRemoteChanges(() => store.applyDiff(diff))`, which marks them
66
+ `source: "remote"`: they do not enter the local undo stack and do not
67
+ re-broadcast.
68
+ - A newcomer applies the first `snapshot` it is offered and ignores the rest.
69
+ Answering a `hello` makes a peer "established", so a snapshot meant for
70
+ someone else can never overwrite its work.
71
+ - Presence records are derived from the editor's camera,
72
+ `inputs.currentPagePoint` and selection, throttled to at most 30 Hz, and
73
+ re-sent on a heartbeat. A collaborator is dropped on `bye` or after 10s of
74
+ silence.
75
+ - `decodeMessage` returns `null` for anything malformed, so a peer running a
76
+ different version cannot crash this one. `PROTOCOL_VERSION` is checked on
77
+ `hello`.
78
+
79
+ ## Conflict policy: last writer wins, per record
80
+
81
+ There is no operational transform and no CRDT here. Diffs are applied in
82
+ arrival order and the last write to a record id is the one that survives.
83
+
84
+ What that means in practice:
85
+
86
+ - Two people dragging **different** shapes is always fine.
87
+ - Two people dragging the **same** shape converge on whoever sent last; the
88
+ other person's drag is discarded, not merged.
89
+ - Concurrent edits to **different fields of the same record** (one person
90
+ moves a shape while another recolours it) lose one of the two changes: a
91
+ record is replaced wholesale, not merged field by field.
92
+ - A delete racing an update can resurrect the record: the update carries the
93
+ full record and is applied after the removal.
94
+ - Peers that were offline while others edited rejoin with `hello` and take a
95
+ peer's snapshot, so anything they changed offline is overwritten.
96
+
97
+ This is enough for cursors-and-shapes collaboration on a small trusted room.
98
+ Anything needing real convergence guarantees wants a server-authoritative log
99
+ or a CRDT under `applyDiff`; the transport and message types here do not have
100
+ to change for that.
101
+
102
+ ## Transports
103
+
104
+ ```ts
105
+ createBroadcastChannelTransport(roomId) // tabs of one browser, no server
106
+ createWebSocketTransport(url, { reconnect: true }) // a relay; exponential backoff with jitter
107
+ createMemoryTransportPair() // two peers in one process (tests)
108
+ createMemoryHub() // n peers in one process (tests)
109
+ ```
110
+
111
+ A `Transport` is `{ send, onMessage, onOpen?, onClose?, close }` and owns its
112
+ own serialization and reconnection. Anything implementing that interface works
113
+ — a WebRTC data channel, a shared worker, a mock.
114
+
115
+ ## Running the relay
116
+
117
+ The relay forwards messages between the sockets in a room and keeps no state,
118
+ so restarting it is harmless: peers re-share the document with
119
+ `hello`/`snapshot`. The room is the URL path.
120
+
121
+ ```sh
122
+ pnpm --filter @mocanvas/sync relay # ws://localhost:5858/<room>
123
+ PORT=9000 pnpm --filter @mocanvas/sync relay
124
+ ```
125
+
126
+ ```ts
127
+ createWebSocketTransport(`ws://localhost:5858/${roomId}`, { reconnect: true })
128
+ ```
129
+
130
+ The relay script ships in the package but `ws` does not — it is a
131
+ devDependency here, because the relay is a development tool and not part of
132
+ the client. Install `ws` yourself to run it outside this repository.
133
+
134
+ ## Tests
135
+
136
+ ```sh
137
+ pnpm --filter @mocanvas/sync test
138
+ ```
139
+
140
+ ## License
141
+
142
+ MIT
@@ -0,0 +1,278 @@
1
+ import { Signal } from '@mocanvas/state';
2
+ import { Store, UnknownRecord, RecordsDiff } from '@mocanvas/store';
3
+ import { InstancePresence, InstancePresenceId, Editor } from '@mocanvas/editor';
4
+ import * as react from 'react';
5
+
6
+ /** Presence is sent at most this often (30 Hz). */
7
+ declare const DEFAULT_PRESENCE_THROTTLE_MS: number;
8
+ /** A presence record is re-sent this often even when nothing changed. */
9
+ declare const DEFAULT_PRESENCE_HEARTBEAT_MS = 3000;
10
+ /** A collaborator we have not heard from in this long is dropped. */
11
+ declare const DEFAULT_PRESENCE_TIMEOUT_MS = 10000;
12
+ /**
13
+ * The part of the editor presence reads. Declared structurally so that the
14
+ * sync layer does not need a live `Editor` (and its WASM engine) to be tested.
15
+ */
16
+ interface PresenceEditor {
17
+ readonly store: Store<any, any>;
18
+ readonly inputs: {
19
+ readonly currentPagePoint: {
20
+ x: number;
21
+ y: number;
22
+ };
23
+ };
24
+ readonly user: {
25
+ getId(): string;
26
+ getName(): string;
27
+ getColor(): string;
28
+ };
29
+ getCurrentPageId(): string;
30
+ getCamera(): {
31
+ x: number;
32
+ y: number;
33
+ z: number;
34
+ };
35
+ getSelectedShapeIds(): readonly string[];
36
+ getInstanceState(): {
37
+ cursor: {
38
+ type: string;
39
+ rotation: number;
40
+ };
41
+ brush: {
42
+ x: number;
43
+ y: number;
44
+ w: number;
45
+ h: number;
46
+ } | null;
47
+ scribbles: readonly unknown[];
48
+ followingUserId: string | null;
49
+ };
50
+ /** Optional: used to sample the cursor, which is not itself a signal. */
51
+ on?(name: "event", fn: () => void): () => void;
52
+ }
53
+ interface PresenceSyncOptions {
54
+ editor: PresenceEditor;
55
+ /** Identifies this tab; one user may have several. */
56
+ clientId: string;
57
+ send(record: InstancePresence): void;
58
+ throttleMs?: number;
59
+ heartbeatMs?: number;
60
+ }
61
+ interface PresenceSync {
62
+ start(): void;
63
+ stop(): void;
64
+ /** Ask for a send; collapses with any other request inside the throttle window. */
65
+ poke(): void;
66
+ /** The record last handed to `send`, for tests and debugging. */
67
+ getLastSent(): InstancePresence | null;
68
+ dispose(): void;
69
+ }
70
+ /** The id a client's presence record always uses, so updates overwrite in place. */
71
+ declare function presenceIdForClient(clientId: string): InstancePresenceId;
72
+ /**
73
+ * Watches the editor and pushes the local presence record out, at most once per
74
+ * throttle window, plus a heartbeat so that idle collaborators do not time out.
75
+ */
76
+ declare function createPresenceSync(options: PresenceSyncOptions): PresenceSync;
77
+ /** Equal apart from `lastActivityTimestamp`, which always moves. */
78
+ declare function isSamePresence(a: InstancePresence, b: InstancePresence): boolean;
79
+ /**
80
+ * Holds the presence records of everyone else: puts them in the store as they
81
+ * arrive, drops them on `bye`, and sweeps clients that went quiet.
82
+ */
83
+ declare class PresenceRoom<R extends UnknownRecord> {
84
+ private readonly store;
85
+ private readonly timeoutMs;
86
+ private readonly seen;
87
+ private sweeper;
88
+ constructor(store: Store<R, any>, timeoutMs?: number);
89
+ /** Store an incoming presence record and remember when we saw its client. */
90
+ receive(clientId: string, record: R): void;
91
+ /** Drop one client's presence (it said goodbye). */
92
+ remove(clientId: string): void;
93
+ /** Drop every client we have not heard from within the timeout. */
94
+ sweep(now?: number): void;
95
+ startSweeping(intervalMs?: number): void;
96
+ stopSweeping(): void;
97
+ /** Remove every record this room put in the store. */
98
+ clear(): void;
99
+ private removeRecord;
100
+ }
101
+
102
+ /** Bumped when the wire format changes incompatibly. */
103
+ declare const PROTOCOL_VERSION = 1;
104
+ /**
105
+ * Every message is one JSON object with a `type` discriminator.
106
+ *
107
+ * ```
108
+ * hello a client joined; existing peers answer with a snapshot
109
+ * snapshot the document-scope records as the sender currently sees them
110
+ * diff one squashed store diff, in document scope
111
+ * presence one presence record (cursor, camera, selection)
112
+ * bye the client is leaving; drop its presence
113
+ * ```
114
+ */
115
+ type SyncMessage<R extends UnknownRecord = UnknownRecord> = HelloMessage | SnapshotMessage<R> | DiffMessage<R> | PresenceMessage<R> | ByeMessage;
116
+ interface HelloMessage {
117
+ type: "hello";
118
+ clientId: string;
119
+ version: number;
120
+ }
121
+ interface SnapshotMessage<R extends UnknownRecord = UnknownRecord> {
122
+ type: "snapshot";
123
+ records: R[];
124
+ }
125
+ interface DiffMessage<R extends UnknownRecord = UnknownRecord> {
126
+ type: "diff";
127
+ clientId: string;
128
+ /** Per-client counter; only used for logging and de-duplication. */
129
+ seq: number;
130
+ diff: RecordsDiff<R>;
131
+ }
132
+ interface PresenceMessage<R extends UnknownRecord = UnknownRecord> {
133
+ type: "presence";
134
+ clientId: string;
135
+ record: R;
136
+ }
137
+ interface ByeMessage {
138
+ type: "bye";
139
+ clientId: string;
140
+ }
141
+ declare function encodeMessage<R extends UnknownRecord>(message: SyncMessage<R>): string;
142
+ /**
143
+ * Parse a message off the wire. Returns `null` for anything malformed: a peer
144
+ * on a newer protocol must never be able to crash this one.
145
+ */
146
+ declare function decodeMessage<R extends UnknownRecord = UnknownRecord>(data: unknown): SyncMessage<R> | null;
147
+
148
+ /**
149
+ * A duplex channel that carries `SyncMessage`s between peers in one room.
150
+ *
151
+ * The transport owns serialization and reconnection; the client above it only
152
+ * sees decoded messages. `onOpen` fires every time the channel becomes usable
153
+ * (including after a reconnect), `onClose` every time it stops being usable.
154
+ * A transport with neither is treated as open from the moment it is connected.
155
+ */
156
+ interface Transport<R extends UnknownRecord = UnknownRecord> {
157
+ send(message: SyncMessage<R>): void;
158
+ onMessage(callback: (message: SyncMessage<R>) => void): () => void;
159
+ onOpen?(callback: () => void): () => void;
160
+ onClose?(callback: () => void): () => void;
161
+ close(): void;
162
+ }
163
+ interface BroadcastChannelTransportOptions {
164
+ /** Channel name prefix, so two apps on one origin do not collide. */
165
+ prefix?: string;
166
+ }
167
+ /**
168
+ * Same-origin tabs of one browser. There is no server, so every peer is
169
+ * equally authoritative and the channel is open as soon as it is created.
170
+ */
171
+ declare function createBroadcastChannelTransport<R extends UnknownRecord = UnknownRecord>(roomId: string, options?: BroadcastChannelTransportOptions): Transport<R>;
172
+ interface WebSocketTransportOptions {
173
+ /** Reconnect with exponential backoff after an unexpected close. Default `true`. */
174
+ reconnect?: boolean;
175
+ /** First backoff delay in ms. Default 500. */
176
+ minDelayMs?: number;
177
+ /** Backoff ceiling in ms. Default 15000. */
178
+ maxDelayMs?: number;
179
+ /** Injectable for tests and for Node (`ws`). Defaults to `globalThis.WebSocket`. */
180
+ WebSocketImpl?: typeof WebSocket;
181
+ }
182
+ /**
183
+ * A WebSocket to a relay (see `scripts/relay.mjs`). Messages sent while the
184
+ * socket is down are dropped rather than queued: the next `hello`/`snapshot`
185
+ * exchange re-establishes the document anyway, and presence is re-sent on a
186
+ * heartbeat.
187
+ */
188
+ declare function createWebSocketTransport<R extends UnknownRecord = UnknownRecord>(url: string, options?: WebSocketTransportOptions): Transport<R>;
189
+ /**
190
+ * Two transports wired to each other, going through the same JSON encode and
191
+ * decode as the real ones. Delivery is asynchronous (a microtask) so that a
192
+ * peer never observes a message inside its own `send()` call stack.
193
+ */
194
+ declare function createMemoryTransportPair<R extends UnknownRecord = UnknownRecord>(): [Transport<R>, Transport<R>];
195
+ interface MemoryHub<R extends UnknownRecord = UnknownRecord> {
196
+ /** Add another peer to the room. */
197
+ join(): Transport<R>;
198
+ }
199
+ /** A room every joined transport broadcasts into (itself excluded). */
200
+ declare function createMemoryHub<R extends UnknownRecord = UnknownRecord>(): MemoryHub<R>;
201
+
202
+ type SyncStatus = "offline" | "connecting" | "online";
203
+ interface SyncClientOptions<R extends UnknownRecord = UnknownRecord> {
204
+ store: Store<R, any>;
205
+ /** Identifies the document. The transport is expected to be scoped to it too. */
206
+ roomId: string;
207
+ transport: Transport<R>;
208
+ /** Turn on cursor/selection sharing for an editor. */
209
+ presence?: {
210
+ editor: PresenceEditor;
211
+ } | undefined;
212
+ /** Defaults to a fresh random id; one per tab, not per user. */
213
+ clientId?: string | undefined;
214
+ /** Drop a collaborator after this long without a presence message. Default 10s. */
215
+ presenceTimeoutMs?: number | undefined;
216
+ /** Upper bound on presence send rate. Default 34ms (~30 Hz). */
217
+ presenceThrottleMs?: number | undefined;
218
+ presenceHeartbeatMs?: number | undefined;
219
+ /** Called for protocol-level problems (a peer on another version, say). */
220
+ onError?: ((error: Error) => void) | undefined;
221
+ }
222
+ interface SyncClient {
223
+ readonly clientId: string;
224
+ readonly roomId: string;
225
+ connect(): void;
226
+ disconnect(): void;
227
+ /** A signal, so React and reactors can follow the connection. */
228
+ getStatus(): Signal<SyncStatus>;
229
+ dispose(): void;
230
+ }
231
+ /**
232
+ * Joins one room over `transport` and keeps `store` in step with its peers.
233
+ *
234
+ * Document changes made locally by the user are broadcast as diffs; incoming
235
+ * diffs are applied as remote changes so they neither echo back nor land in
236
+ * the undo stack. Conflicts are resolved last-writer-wins per record.
237
+ */
238
+ declare function createSyncClient<R extends UnknownRecord = UnknownRecord>(options: SyncClientOptions<R>): SyncClient;
239
+
240
+ interface CollaboratorCursorsProps {
241
+ editor: Editor;
242
+ /** Draw what each collaborator has selected. Default `true`. */
243
+ showSelection?: boolean;
244
+ /** Draw a name chip next to each cursor. Default `true`. */
245
+ showNames?: boolean;
246
+ }
247
+ /**
248
+ * Other people's cursors and selections, drawn in screen space above the
249
+ * canvas. Drop it inside `<Mocanvas>` or `<Canvas>`; it positions itself.
250
+ */
251
+ declare const CollaboratorCursors: ({ editor, showSelection, showNames, }: CollaboratorCursorsProps) => react.JSX.Element | null;
252
+ interface UseSyncOptions {
253
+ roomId: string;
254
+ /**
255
+ * The channel to the room. Pass a factory when the transport should be
256
+ * recreated with the room (the usual case); it is read once per connection.
257
+ */
258
+ transport: Transport | (() => Transport);
259
+ /** Share this editor's cursor and selection. Default `true`. */
260
+ presence?: boolean;
261
+ /** Set `false` to stay offline (a read-only view, say). Default `true`. */
262
+ enabled?: boolean;
263
+ clientId?: string;
264
+ presenceTimeoutMs?: number;
265
+ presenceThrottleMs?: number;
266
+ onError?: (error: Error) => void;
267
+ }
268
+ interface UseSyncResult {
269
+ status: SyncStatus;
270
+ client: SyncClient | null;
271
+ }
272
+ /**
273
+ * Connect an editor's store to a room for as long as the component is mounted.
274
+ * Everything is torn down (including the transport) on unmount.
275
+ */
276
+ declare function useSync(editor: Editor | null, options: UseSyncOptions): UseSyncResult;
277
+
278
+ export { type BroadcastChannelTransportOptions, type ByeMessage, CollaboratorCursors, type CollaboratorCursorsProps, DEFAULT_PRESENCE_HEARTBEAT_MS, DEFAULT_PRESENCE_THROTTLE_MS, DEFAULT_PRESENCE_TIMEOUT_MS, type DiffMessage, type HelloMessage, type MemoryHub, PROTOCOL_VERSION, type PresenceEditor, type PresenceMessage, PresenceRoom, type PresenceSync, type PresenceSyncOptions, type SnapshotMessage, type SyncClient, type SyncClientOptions, type SyncMessage, type SyncStatus, type Transport, type UseSyncOptions, type UseSyncResult, type WebSocketTransportOptions, createBroadcastChannelTransport, createMemoryHub, createMemoryTransportPair, createPresenceSync, createSyncClient, createWebSocketTransport, decodeMessage, encodeMessage, isSamePresence, presenceIdForClient, useSync };
package/dist/index.js ADDED
@@ -0,0 +1,660 @@
1
+ import { react, atom } from '@mocanvas/state';
2
+ import { uniqueId, isRecordsDiffEmpty } from '@mocanvas/store';
3
+ import { InstancePresenceRecordType } from '@mocanvas/editor';
4
+ import { track, useValue } from '@mocanvas/state/react';
5
+ import { useRef, useState, useEffect } from 'react';
6
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
7
+
8
+ // src/SyncClient.ts
9
+ var DEFAULT_PRESENCE_THROTTLE_MS = Math.ceil(1e3 / 30);
10
+ var DEFAULT_PRESENCE_HEARTBEAT_MS = 3e3;
11
+ var DEFAULT_PRESENCE_TIMEOUT_MS = 1e4;
12
+ function presenceIdForClient(clientId) {
13
+ return InstancePresenceRecordType.createId(clientId);
14
+ }
15
+ function createPresenceSync(options) {
16
+ const { editor, clientId, send } = options;
17
+ const throttleMs = options.throttleMs ?? DEFAULT_PRESENCE_THROTTLE_MS;
18
+ const heartbeatMs = options.heartbeatMs ?? DEFAULT_PRESENCE_HEARTBEAT_MS;
19
+ const id = presenceIdForClient(clientId);
20
+ let timer = null;
21
+ let heartbeat = null;
22
+ let stopReactor = null;
23
+ let stopEvents = null;
24
+ let last = null;
25
+ let running = false;
26
+ const build = () => {
27
+ const instance = editor.getInstanceState();
28
+ const point = editor.inputs.currentPagePoint;
29
+ return InstancePresenceRecordType.create({
30
+ id,
31
+ userId: editor.user.getId(),
32
+ userName: editor.user.getName(),
33
+ color: editor.user.getColor(),
34
+ currentPageId: editor.getCurrentPageId(),
35
+ cursor: { x: point.x, y: point.y, type: instance.cursor.type, rotation: instance.cursor.rotation },
36
+ camera: { ...editor.getCamera() },
37
+ selectedShapeIds: [...editor.getSelectedShapeIds()],
38
+ brush: instance.brush ? { ...instance.brush } : null,
39
+ scribbles: [...instance.scribbles],
40
+ followingUserId: instance.followingUserId,
41
+ lastActivityTimestamp: Date.now(),
42
+ chatMessage: "",
43
+ meta: {}
44
+ });
45
+ };
46
+ const flush = (force) => {
47
+ if (!running) return;
48
+ const next = build();
49
+ if (!force && last && isSamePresence(last, next)) return;
50
+ last = next;
51
+ send(next);
52
+ };
53
+ const poke = () => {
54
+ if (!running || timer !== null) return;
55
+ timer = setTimeout(() => {
56
+ timer = null;
57
+ flush(false);
58
+ }, throttleMs);
59
+ };
60
+ return {
61
+ start() {
62
+ if (running) return;
63
+ running = true;
64
+ stopReactor = react("sync.presence", () => {
65
+ editor.getCamera();
66
+ editor.getCurrentPageId();
67
+ editor.getSelectedShapeIds();
68
+ editor.getInstanceState();
69
+ editor.user.getName();
70
+ editor.user.getColor();
71
+ poke();
72
+ });
73
+ stopEvents = editor.on?.("event", poke) ?? null;
74
+ heartbeat = setInterval(() => flush(true), heartbeatMs);
75
+ flush(true);
76
+ },
77
+ stop() {
78
+ running = false;
79
+ if (timer !== null) {
80
+ clearTimeout(timer);
81
+ timer = null;
82
+ }
83
+ if (heartbeat !== null) {
84
+ clearInterval(heartbeat);
85
+ heartbeat = null;
86
+ }
87
+ stopReactor?.();
88
+ stopReactor = null;
89
+ stopEvents?.();
90
+ stopEvents = null;
91
+ },
92
+ poke,
93
+ getLastSent: () => last,
94
+ dispose() {
95
+ this.stop();
96
+ last = null;
97
+ }
98
+ };
99
+ }
100
+ function isSamePresence(a, b) {
101
+ return a.userId === b.userId && a.userName === b.userName && a.color === b.color && a.currentPageId === b.currentPageId && a.chatMessage === b.chatMessage && a.followingUserId === b.followingUserId && a.cursor.x === b.cursor.x && a.cursor.y === b.cursor.y && a.cursor.type === b.cursor.type && a.cursor.rotation === b.cursor.rotation && a.camera.x === b.camera.x && a.camera.y === b.camera.y && a.camera.z === b.camera.z && sameIds(a.selectedShapeIds, b.selectedShapeIds) && sameBrush(a.brush, b.brush) && a.scribbles.length === b.scribbles.length && a.scribbles.every((s, i) => s === b.scribbles[i]);
102
+ }
103
+ function sameIds(a, b) {
104
+ return a.length === b.length && a.every((id, i) => id === b[i]);
105
+ }
106
+ function sameBrush(a, b) {
107
+ if (a === b) return true;
108
+ if (!a || !b) return false;
109
+ return a.x === b.x && a.y === b.y && a.w === b.w && a.h === b.h;
110
+ }
111
+ var PresenceRoom = class {
112
+ constructor(store, timeoutMs = DEFAULT_PRESENCE_TIMEOUT_MS) {
113
+ this.store = store;
114
+ this.timeoutMs = timeoutMs;
115
+ }
116
+ store;
117
+ timeoutMs;
118
+ seen = /* @__PURE__ */ new Map();
119
+ sweeper = null;
120
+ /** Store an incoming presence record and remember when we saw its client. */
121
+ receive(clientId, record) {
122
+ const previous = this.seen.get(clientId);
123
+ if (previous && previous.id !== record.id) this.removeRecord(previous.id);
124
+ this.seen.set(clientId, { id: record.id, at: Date.now() });
125
+ this.store.mergeRemoteChanges(() => {
126
+ this.store.put([record]);
127
+ });
128
+ }
129
+ /** Drop one client's presence (it said goodbye). */
130
+ remove(clientId) {
131
+ const entry = this.seen.get(clientId);
132
+ if (!entry) return;
133
+ this.seen.delete(clientId);
134
+ this.removeRecord(entry.id);
135
+ }
136
+ /** Drop every client we have not heard from within the timeout. */
137
+ sweep(now = Date.now()) {
138
+ const stale = [];
139
+ for (const [clientId, entry] of this.seen) {
140
+ if (now - entry.at > this.timeoutMs) stale.push(clientId);
141
+ }
142
+ for (const clientId of stale) this.remove(clientId);
143
+ }
144
+ startSweeping(intervalMs = Math.max(1e3, Math.floor(this.timeoutMs / 4))) {
145
+ if (this.sweeper !== null) return;
146
+ this.sweeper = setInterval(() => this.sweep(), intervalMs);
147
+ }
148
+ stopSweeping() {
149
+ if (this.sweeper === null) return;
150
+ clearInterval(this.sweeper);
151
+ this.sweeper = null;
152
+ }
153
+ /** Remove every record this room put in the store. */
154
+ clear() {
155
+ const ids = Array.from(this.seen.values(), (entry) => entry.id);
156
+ this.seen.clear();
157
+ for (const id of ids) this.removeRecord(id);
158
+ }
159
+ removeRecord(id) {
160
+ const recordId = id;
161
+ if (!this.store.unsafeGetWithoutCapture(recordId)) return;
162
+ this.store.mergeRemoteChanges(() => {
163
+ this.store.remove([recordId]);
164
+ });
165
+ }
166
+ };
167
+
168
+ // src/protocol.ts
169
+ var PROTOCOL_VERSION = 1;
170
+ function encodeMessage(message) {
171
+ return JSON.stringify(message);
172
+ }
173
+ function decodeMessage(data) {
174
+ let value = data;
175
+ if (typeof data === "string") {
176
+ try {
177
+ value = JSON.parse(data);
178
+ } catch {
179
+ return null;
180
+ }
181
+ }
182
+ if (typeof value !== "object" || value === null) return null;
183
+ const message = value;
184
+ switch (message["type"]) {
185
+ case "hello":
186
+ if (typeof message["clientId"] !== "string") return null;
187
+ if (typeof message["version"] !== "number") return null;
188
+ return { type: "hello", clientId: message["clientId"], version: message["version"] };
189
+ case "snapshot": {
190
+ const records = message["records"];
191
+ if (!Array.isArray(records)) return null;
192
+ if (!records.every(isRecordLike)) return null;
193
+ return { type: "snapshot", records };
194
+ }
195
+ case "diff": {
196
+ if (typeof message["clientId"] !== "string") return null;
197
+ if (typeof message["seq"] !== "number") return null;
198
+ if (!isDiffLike(message["diff"])) return null;
199
+ return {
200
+ type: "diff",
201
+ clientId: message["clientId"],
202
+ seq: message["seq"],
203
+ diff: message["diff"]
204
+ };
205
+ }
206
+ case "presence": {
207
+ if (typeof message["clientId"] !== "string") return null;
208
+ if (!isRecordLike(message["record"])) return null;
209
+ return { type: "presence", clientId: message["clientId"], record: message["record"] };
210
+ }
211
+ case "bye":
212
+ if (typeof message["clientId"] !== "string") return null;
213
+ return { type: "bye", clientId: message["clientId"] };
214
+ default:
215
+ return null;
216
+ }
217
+ }
218
+ function isRecordLike(value) {
219
+ return typeof value === "object" && value !== null && typeof value.id === "string" && typeof value.typeName === "string";
220
+ }
221
+ function isDiffLike(value) {
222
+ if (typeof value !== "object" || value === null) return false;
223
+ const diff = value;
224
+ for (const key of ["added", "updated", "removed"]) {
225
+ const part = diff[key];
226
+ if (typeof part !== "object" || part === null || Array.isArray(part)) return false;
227
+ }
228
+ return true;
229
+ }
230
+
231
+ // src/SyncClient.ts
232
+ function createSyncClient(options) {
233
+ const { store, roomId, transport } = options;
234
+ const clientId = options.clientId ?? uniqueId(12);
235
+ const status = atom(`sync.status:${roomId}`, "offline");
236
+ const room = new PresenceRoom(store, options.presenceTimeoutMs ?? DEFAULT_PRESENCE_TIMEOUT_MS);
237
+ const unsubscribes = [];
238
+ let presence = null;
239
+ let seq = 0;
240
+ let awaitingSnapshot = false;
241
+ let connected = false;
242
+ let disposed = false;
243
+ const send = (message) => {
244
+ try {
245
+ transport.send(message);
246
+ } catch (error) {
247
+ options.onError?.(error instanceof Error ? error : new Error(String(error)));
248
+ }
249
+ };
250
+ const handle = (message) => {
251
+ if (disposed) return;
252
+ switch (message.type) {
253
+ case "hello": {
254
+ if (message.clientId === clientId) return;
255
+ if (message.version !== PROTOCOL_VERSION) {
256
+ options.onError?.(
257
+ new Error(`Peer ${message.clientId} speaks protocol ${message.version}, we speak ${PROTOCOL_VERSION}`)
258
+ );
259
+ return;
260
+ }
261
+ awaitingSnapshot = false;
262
+ send({ type: "snapshot", records: Object.values(store.serialize("document")) });
263
+ presence?.poke();
264
+ return;
265
+ }
266
+ case "snapshot": {
267
+ if (!awaitingSnapshot) return;
268
+ awaitingSnapshot = false;
269
+ store.mergeRemoteChanges(() => {
270
+ store.put(message.records);
271
+ });
272
+ return;
273
+ }
274
+ case "diff": {
275
+ if (message.clientId === clientId) return;
276
+ if (isRecordsDiffEmpty(message.diff)) return;
277
+ store.mergeRemoteChanges(() => {
278
+ store.applyDiff(message.diff);
279
+ });
280
+ return;
281
+ }
282
+ case "presence": {
283
+ if (message.clientId === clientId) return;
284
+ room.receive(message.clientId, message.record);
285
+ return;
286
+ }
287
+ case "bye": {
288
+ if (message.clientId === clientId) return;
289
+ room.remove(message.clientId);
290
+ return;
291
+ }
292
+ }
293
+ };
294
+ const onOpen = () => {
295
+ if (disposed || !connected) return;
296
+ status.set("online");
297
+ send({ type: "hello", clientId, version: PROTOCOL_VERSION });
298
+ presence?.poke();
299
+ };
300
+ const onClose = () => {
301
+ if (disposed || !connected) return;
302
+ status.set("connecting");
303
+ };
304
+ return {
305
+ clientId,
306
+ roomId,
307
+ connect() {
308
+ if (disposed || connected) return;
309
+ connected = true;
310
+ awaitingSnapshot = true;
311
+ status.set("connecting");
312
+ unsubscribes.push(transport.onMessage(handle));
313
+ if (transport.onOpen) unsubscribes.push(transport.onOpen(onOpen));
314
+ if (transport.onClose) unsubscribes.push(transport.onClose(onClose));
315
+ unsubscribes.push(
316
+ store.listen(
317
+ (entry) => {
318
+ if (isRecordsDiffEmpty(entry.changes)) return;
319
+ send({ type: "diff", clientId, seq: seq++, diff: entry.changes });
320
+ },
321
+ { source: "user", scope: "document" }
322
+ )
323
+ );
324
+ if (options.presence) {
325
+ presence = createPresenceSync({
326
+ editor: options.presence.editor,
327
+ clientId,
328
+ send: (record) => send({ type: "presence", clientId, record }),
329
+ ...options.presenceThrottleMs !== void 0 ? { throttleMs: options.presenceThrottleMs } : {},
330
+ ...options.presenceHeartbeatMs !== void 0 ? { heartbeatMs: options.presenceHeartbeatMs } : {}
331
+ });
332
+ presence.start();
333
+ }
334
+ room.startSweeping();
335
+ if (!transport.onOpen) onOpen();
336
+ },
337
+ disconnect() {
338
+ if (!connected) return;
339
+ connected = false;
340
+ send({ type: "bye", clientId });
341
+ presence?.stop();
342
+ presence = null;
343
+ room.stopSweeping();
344
+ room.clear();
345
+ for (const off of unsubscribes.splice(0)) off();
346
+ awaitingSnapshot = false;
347
+ status.set("offline");
348
+ },
349
+ getStatus() {
350
+ return status;
351
+ },
352
+ dispose() {
353
+ if (disposed) return;
354
+ this.disconnect();
355
+ disposed = true;
356
+ transport.close();
357
+ }
358
+ };
359
+ }
360
+
361
+ // src/transport.ts
362
+ function createEmitter() {
363
+ const callbacks = /* @__PURE__ */ new Set();
364
+ return {
365
+ add(cb) {
366
+ callbacks.add(cb);
367
+ return () => {
368
+ callbacks.delete(cb);
369
+ };
370
+ },
371
+ emit() {
372
+ for (const cb of Array.from(callbacks)) cb();
373
+ },
374
+ clear() {
375
+ callbacks.clear();
376
+ }
377
+ };
378
+ }
379
+ function createBroadcastChannelTransport(roomId, options = {}) {
380
+ const name = `${options.prefix ?? "mocanvas-sync"}:${roomId}`;
381
+ const channel = new BroadcastChannel(name);
382
+ const listeners = /* @__PURE__ */ new Set();
383
+ let closed = false;
384
+ channel.onmessage = (event) => {
385
+ const message = decodeMessage(event.data);
386
+ if (!message) return;
387
+ for (const listener of Array.from(listeners)) listener(message);
388
+ };
389
+ return {
390
+ send(message) {
391
+ if (closed) return;
392
+ channel.postMessage(encodeMessage(message));
393
+ },
394
+ onMessage(callback) {
395
+ listeners.add(callback);
396
+ return () => {
397
+ listeners.delete(callback);
398
+ };
399
+ },
400
+ onOpen(callback) {
401
+ let cancelled = false;
402
+ queueMicrotask(() => {
403
+ if (!cancelled && !closed) callback();
404
+ });
405
+ return () => {
406
+ cancelled = true;
407
+ };
408
+ },
409
+ close() {
410
+ closed = true;
411
+ listeners.clear();
412
+ channel.onmessage = null;
413
+ channel.close();
414
+ }
415
+ };
416
+ }
417
+ function createWebSocketTransport(url, options = {}) {
418
+ const reconnect = options.reconnect ?? true;
419
+ const minDelay = options.minDelayMs ?? 500;
420
+ const maxDelay = options.maxDelayMs ?? 15e3;
421
+ const Impl = options.WebSocketImpl ?? globalThis.WebSocket;
422
+ if (!Impl) throw new Error("No WebSocket implementation available; pass options.WebSocketImpl");
423
+ const listeners = /* @__PURE__ */ new Set();
424
+ const open = createEmitter();
425
+ const close = createEmitter();
426
+ let socket = null;
427
+ let attempt = 0;
428
+ let timer = null;
429
+ let disposed = false;
430
+ const connect = () => {
431
+ if (disposed) return;
432
+ const ws = new Impl(url);
433
+ socket = ws;
434
+ ws.onopen = () => {
435
+ attempt = 0;
436
+ open.emit();
437
+ };
438
+ ws.onmessage = (event) => {
439
+ const message = decodeMessage(event.data);
440
+ if (!message) return;
441
+ for (const listener of Array.from(listeners)) listener(message);
442
+ };
443
+ ws.onerror = () => {
444
+ };
445
+ ws.onclose = () => {
446
+ if (socket === ws) socket = null;
447
+ close.emit();
448
+ if (!disposed && reconnect) schedule();
449
+ };
450
+ };
451
+ const schedule = () => {
452
+ if (timer !== null) return;
453
+ const ceiling = Math.min(maxDelay, minDelay * 2 ** attempt);
454
+ attempt++;
455
+ const delay = Math.random() * ceiling;
456
+ timer = setTimeout(() => {
457
+ timer = null;
458
+ connect();
459
+ }, delay);
460
+ };
461
+ connect();
462
+ return {
463
+ send(message) {
464
+ if (!socket || socket.readyState !== 1) return;
465
+ socket.send(encodeMessage(message));
466
+ },
467
+ onMessage(callback) {
468
+ listeners.add(callback);
469
+ return () => {
470
+ listeners.delete(callback);
471
+ };
472
+ },
473
+ onOpen(callback) {
474
+ const remove = open.add(callback);
475
+ if (socket && socket.readyState === 1) queueMicrotask(callback);
476
+ return remove;
477
+ },
478
+ onClose(callback) {
479
+ return close.add(callback);
480
+ },
481
+ close() {
482
+ disposed = true;
483
+ if (timer !== null) {
484
+ clearTimeout(timer);
485
+ timer = null;
486
+ }
487
+ listeners.clear();
488
+ open.clear();
489
+ close.clear();
490
+ const ws = socket;
491
+ socket = null;
492
+ if (ws) {
493
+ ws.onopen = null;
494
+ ws.onmessage = null;
495
+ ws.onerror = null;
496
+ ws.onclose = null;
497
+ if (ws.readyState === 0 || ws.readyState === 1) ws.close();
498
+ }
499
+ }
500
+ };
501
+ }
502
+ function createMemoryTransportPair() {
503
+ const hub = createMemoryHub();
504
+ return [hub.join(), hub.join()];
505
+ }
506
+ function createMemoryHub() {
507
+ const peers = /* @__PURE__ */ new Set();
508
+ return {
509
+ join() {
510
+ const listeners = /* @__PURE__ */ new Set();
511
+ let closed = false;
512
+ const peer = {
513
+ deliver(data) {
514
+ if (closed) return;
515
+ const message = decodeMessage(data);
516
+ if (!message) return;
517
+ for (const listener of Array.from(listeners)) listener(message);
518
+ }
519
+ };
520
+ peers.add(peer);
521
+ return {
522
+ send(message) {
523
+ if (closed) return;
524
+ const data = encodeMessage(message);
525
+ for (const other of Array.from(peers)) {
526
+ if (other === peer) continue;
527
+ queueMicrotask(() => other.deliver(data));
528
+ }
529
+ },
530
+ onMessage(callback) {
531
+ listeners.add(callback);
532
+ return () => {
533
+ listeners.delete(callback);
534
+ };
535
+ },
536
+ onOpen(callback) {
537
+ let cancelled = false;
538
+ queueMicrotask(() => {
539
+ if (!cancelled && !closed) callback();
540
+ });
541
+ return () => {
542
+ cancelled = true;
543
+ };
544
+ },
545
+ close() {
546
+ closed = true;
547
+ listeners.clear();
548
+ peers.delete(peer);
549
+ }
550
+ };
551
+ }
552
+ };
553
+ }
554
+ var layerStyle = {
555
+ position: "absolute",
556
+ inset: 0,
557
+ width: "100%",
558
+ height: "100%",
559
+ pointerEvents: "none",
560
+ overflow: "visible"
561
+ };
562
+ var CollaboratorCursors = track(function CollaboratorCursors2({
563
+ editor,
564
+ showSelection = true,
565
+ showNames = true
566
+ }) {
567
+ const collaborators = editor.getCollaboratorsOnCurrentPage();
568
+ if (collaborators.length === 0) return null;
569
+ return /* @__PURE__ */ jsx("svg", { className: "mocanvas-collaborators", style: layerStyle, children: collaborators.map((presence) => /* @__PURE__ */ jsxs("g", { children: [
570
+ showSelection ? /* @__PURE__ */ jsx(CollaboratorSelection, { editor, presence }) : null,
571
+ /* @__PURE__ */ jsx(CollaboratorCursor, { editor, presence, showName: showNames })
572
+ ] }, presence.id)) });
573
+ });
574
+ var CollaboratorCursor = track(function CollaboratorCursor2({
575
+ editor,
576
+ presence,
577
+ showName
578
+ }) {
579
+ const point = editor.pageToScreen(presence.cursor);
580
+ const name = presence.userName || "Anonymous";
581
+ return /* @__PURE__ */ jsxs("g", { transform: `translate(${point.x}, ${point.y}) rotate(${presence.cursor.rotation})`, children: [
582
+ /* @__PURE__ */ jsx(
583
+ "path",
584
+ {
585
+ d: "M0 0 L0 17.3 L4.3 13.4 L7 19.4 L9.9 18 L7.2 12.2 L12.8 12 Z",
586
+ fill: presence.color,
587
+ stroke: "#fff",
588
+ strokeWidth: 1,
589
+ strokeLinejoin: "round"
590
+ }
591
+ ),
592
+ showName ? /* @__PURE__ */ jsxs("g", { transform: "translate(11, 18)", children: [
593
+ /* @__PURE__ */ jsx("rect", { rx: 4, ry: 4, width: Math.max(24, name.length * 7 + 12), height: 18, fill: presence.color }),
594
+ /* @__PURE__ */ jsx("text", { x: 6, y: 13, fontSize: 11, fontFamily: "system-ui, sans-serif", fill: "#fff", children: name })
595
+ ] }) : null
596
+ ] });
597
+ });
598
+ var CollaboratorSelection = track(function CollaboratorSelection2({
599
+ editor,
600
+ presence
601
+ }) {
602
+ const outlines = [];
603
+ for (const id of presence.selectedShapeIds) {
604
+ const shape = editor.getShape(id);
605
+ if (!shape) continue;
606
+ const bounds = editor.getShapeGeometryBounds(shape);
607
+ if (!bounds) continue;
608
+ const m = editor.getShapePageTransform(shape);
609
+ const corners = [
610
+ [bounds.x, bounds.y],
611
+ [bounds.maxX, bounds.y],
612
+ [bounds.maxX, bounds.maxY],
613
+ [bounds.x, bounds.maxY]
614
+ ];
615
+ const points = corners.map(([x, y]) => {
616
+ const screen = editor.pageToScreen({ x: m.a * x + m.c * y + m.e, y: m.b * x + m.d * y + m.f });
617
+ return `${screen.x},${screen.y}`;
618
+ });
619
+ outlines.push(points.join(" "));
620
+ }
621
+ if (outlines.length === 0) return null;
622
+ return /* @__PURE__ */ jsx(Fragment, { children: outlines.map((points, i) => /* @__PURE__ */ jsx("polygon", { points, fill: "none", stroke: presence.color, strokeWidth: 1.5, opacity: 0.8 }, i)) });
623
+ });
624
+ function useSync(editor, options) {
625
+ const { roomId, enabled = true, presence = true } = options;
626
+ const latest = useRef(options);
627
+ latest.current = options;
628
+ const [client, setClient] = useState(null);
629
+ useEffect(() => {
630
+ if (!editor || !enabled) {
631
+ setClient(null);
632
+ return;
633
+ }
634
+ const current = latest.current;
635
+ const transport = typeof current.transport === "function" ? current.transport() : current.transport;
636
+ const clientOptions = {
637
+ store: editor.store,
638
+ roomId,
639
+ transport,
640
+ ...presence ? { presence: { editor } } : {},
641
+ ...current.clientId !== void 0 ? { clientId: current.clientId } : {},
642
+ ...current.presenceTimeoutMs !== void 0 ? { presenceTimeoutMs: current.presenceTimeoutMs } : {},
643
+ ...current.presenceThrottleMs !== void 0 ? { presenceThrottleMs: current.presenceThrottleMs } : {},
644
+ onError: (error) => latest.current.onError?.(error)
645
+ };
646
+ const next = createSyncClient(clientOptions);
647
+ setClient(next);
648
+ next.connect();
649
+ return () => {
650
+ setClient(null);
651
+ next.dispose();
652
+ };
653
+ }, [editor, roomId, enabled, presence]);
654
+ const status = useValue("sync.status", () => client?.getStatus().get() ?? "offline", [client]);
655
+ return { status, client };
656
+ }
657
+
658
+ export { CollaboratorCursors, DEFAULT_PRESENCE_HEARTBEAT_MS, DEFAULT_PRESENCE_THROTTLE_MS, DEFAULT_PRESENCE_TIMEOUT_MS, PROTOCOL_VERSION, PresenceRoom, createBroadcastChannelTransport, createMemoryHub, createMemoryTransportPair, createPresenceSync, createSyncClient, createWebSocketTransport, decodeMessage, encodeMessage, isSamePresence, presenceIdForClient, useSync };
659
+ //# sourceMappingURL=index.js.map
660
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/presence.ts","../src/protocol.ts","../src/SyncClient.ts","../src/transport.ts","../src/react.tsx"],"names":["CollaboratorCursors","CollaboratorCursor","CollaboratorSelection"],"mappings":"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","file":"index.js","sourcesContent":["import { react } from \"@mocanvas/state\"\nimport {\n InstancePresenceRecordType,\n type Editor,\n type InstancePresence,\n type InstancePresenceId,\n} from \"@mocanvas/editor\"\nimport type { IdOf, Store, UnknownRecord } from \"@mocanvas/store\"\n\n/** Presence is sent at most this often (30 Hz). */\nexport const DEFAULT_PRESENCE_THROTTLE_MS = Math.ceil(1000 / 30)\n/** A presence record is re-sent this often even when nothing changed. */\nexport const DEFAULT_PRESENCE_HEARTBEAT_MS = 3000\n/** A collaborator we have not heard from in this long is dropped. */\nexport const DEFAULT_PRESENCE_TIMEOUT_MS = 10_000\n\n/**\n * The part of the editor presence reads. Declared structurally so that the\n * sync layer does not need a live `Editor` (and its WASM engine) to be tested.\n */\nexport interface PresenceEditor {\n readonly store: Store<any, any>\n readonly inputs: { readonly currentPagePoint: { x: number; y: number } }\n readonly user: { getId(): string; getName(): string; getColor(): string }\n getCurrentPageId(): string\n getCamera(): { x: number; y: number; z: number }\n getSelectedShapeIds(): readonly string[]\n getInstanceState(): {\n cursor: { type: string; rotation: number }\n brush: { x: number; y: number; w: number; h: number } | null\n scribbles: readonly unknown[]\n followingUserId: string | null\n }\n /** Optional: used to sample the cursor, which is not itself a signal. */\n on?(name: \"event\", fn: () => void): () => void\n}\n\n// A real Editor must satisfy the structural interface above; this fails to\n// compile if the two ever drift apart.\ntype Assert<T extends true> = T\nexport type EditorIsPresenceEditor = Assert<Editor extends PresenceEditor ? true : false>\n\nexport interface PresenceSyncOptions {\n editor: PresenceEditor\n /** Identifies this tab; one user may have several. */\n clientId: string\n send(record: InstancePresence): void\n throttleMs?: number\n heartbeatMs?: number\n}\n\nexport interface PresenceSync {\n start(): void\n stop(): void\n /** Ask for a send; collapses with any other request inside the throttle window. */\n poke(): void\n /** The record last handed to `send`, for tests and debugging. */\n getLastSent(): InstancePresence | null\n dispose(): void\n}\n\n/** The id a client's presence record always uses, so updates overwrite in place. */\nexport function presenceIdForClient(clientId: string): InstancePresenceId {\n return InstancePresenceRecordType.createId(clientId) as InstancePresenceId\n}\n\n/**\n * Watches the editor and pushes the local presence record out, at most once per\n * throttle window, plus a heartbeat so that idle collaborators do not time out.\n */\nexport function createPresenceSync(options: PresenceSyncOptions): PresenceSync {\n const { editor, clientId, send } = options\n const throttleMs = options.throttleMs ?? DEFAULT_PRESENCE_THROTTLE_MS\n const heartbeatMs = options.heartbeatMs ?? DEFAULT_PRESENCE_HEARTBEAT_MS\n const id = presenceIdForClient(clientId)\n\n let timer: ReturnType<typeof setTimeout> | null = null\n let heartbeat: ReturnType<typeof setInterval> | null = null\n let stopReactor: (() => void) | null = null\n let stopEvents: (() => void) | null = null\n let last: InstancePresence | null = null\n let running = false\n\n const build = (): InstancePresence => {\n const instance = editor.getInstanceState()\n const point = editor.inputs.currentPagePoint\n return InstancePresenceRecordType.create({\n id,\n userId: editor.user.getId(),\n userName: editor.user.getName(),\n color: editor.user.getColor(),\n currentPageId: editor.getCurrentPageId() as InstancePresence[\"currentPageId\"],\n cursor: { x: point.x, y: point.y, type: instance.cursor.type, rotation: instance.cursor.rotation },\n camera: { ...editor.getCamera() },\n selectedShapeIds: [...editor.getSelectedShapeIds()] as InstancePresence[\"selectedShapeIds\"],\n brush: instance.brush ? { ...instance.brush } : null,\n scribbles: [...instance.scribbles] as InstancePresence[\"scribbles\"],\n followingUserId: instance.followingUserId,\n lastActivityTimestamp: Date.now(),\n chatMessage: \"\",\n meta: {},\n })\n }\n\n const flush = (force: boolean) => {\n if (!running) return\n const next = build()\n if (!force && last && isSamePresence(last, next)) return\n last = next\n send(next)\n }\n\n const poke = () => {\n if (!running || timer !== null) return\n timer = setTimeout(() => {\n timer = null\n flush(false)\n }, throttleMs)\n }\n\n return {\n start() {\n if (running) return\n running = true\n // Camera, page and selection are signals, so a reactor covers them.\n stopReactor = react(\"sync.presence\", () => {\n editor.getCamera()\n editor.getCurrentPageId()\n editor.getSelectedShapeIds()\n editor.getInstanceState()\n editor.user.getName()\n editor.user.getColor()\n poke()\n })\n // The pointer position lives on `inputs`, which is not reactive.\n stopEvents = editor.on?.(\"event\", poke) ?? null\n heartbeat = setInterval(() => flush(true), heartbeatMs)\n flush(true)\n },\n stop() {\n running = false\n if (timer !== null) {\n clearTimeout(timer)\n timer = null\n }\n if (heartbeat !== null) {\n clearInterval(heartbeat)\n heartbeat = null\n }\n stopReactor?.()\n stopReactor = null\n stopEvents?.()\n stopEvents = null\n },\n poke,\n getLastSent: () => last,\n dispose() {\n this.stop()\n last = null\n },\n }\n}\n\n/** Equal apart from `lastActivityTimestamp`, which always moves. */\nexport function isSamePresence(a: InstancePresence, b: InstancePresence): boolean {\n return (\n a.userId === b.userId &&\n a.userName === b.userName &&\n a.color === b.color &&\n a.currentPageId === b.currentPageId &&\n a.chatMessage === b.chatMessage &&\n a.followingUserId === b.followingUserId &&\n a.cursor.x === b.cursor.x &&\n a.cursor.y === b.cursor.y &&\n a.cursor.type === b.cursor.type &&\n a.cursor.rotation === b.cursor.rotation &&\n a.camera.x === b.camera.x &&\n a.camera.y === b.camera.y &&\n a.camera.z === b.camera.z &&\n sameIds(a.selectedShapeIds, b.selectedShapeIds) &&\n sameBrush(a.brush, b.brush) &&\n a.scribbles.length === b.scribbles.length &&\n a.scribbles.every((s, i) => s === b.scribbles[i])\n )\n}\n\nfunction sameIds(a: readonly string[], b: readonly string[]): boolean {\n return a.length === b.length && a.every((id, i) => id === b[i])\n}\n\nfunction sameBrush(a: InstancePresence[\"brush\"], b: InstancePresence[\"brush\"]): boolean {\n if (a === b) return true\n if (!a || !b) return false\n return a.x === b.x && a.y === b.y && a.w === b.w && a.h === b.h\n}\n\n/**\n * Holds the presence records of everyone else: puts them in the store as they\n * arrive, drops them on `bye`, and sweeps clients that went quiet.\n */\nexport class PresenceRoom<R extends UnknownRecord> {\n private readonly seen = new Map<string, { id: string; at: number }>()\n private sweeper: ReturnType<typeof setInterval> | null = null\n\n constructor(\n private readonly store: Store<R, any>,\n private readonly timeoutMs: number = DEFAULT_PRESENCE_TIMEOUT_MS,\n ) {}\n\n /** Store an incoming presence record and remember when we saw its client. */\n receive(clientId: string, record: R): void {\n const previous = this.seen.get(clientId)\n if (previous && previous.id !== record.id) this.removeRecord(previous.id)\n this.seen.set(clientId, { id: record.id, at: Date.now() })\n this.store.mergeRemoteChanges(() => {\n this.store.put([record])\n })\n }\n\n /** Drop one client's presence (it said goodbye). */\n remove(clientId: string): void {\n const entry = this.seen.get(clientId)\n if (!entry) return\n this.seen.delete(clientId)\n this.removeRecord(entry.id)\n }\n\n /** Drop every client we have not heard from within the timeout. */\n sweep(now: number = Date.now()): void {\n const stale: string[] = []\n for (const [clientId, entry] of this.seen) {\n if (now - entry.at > this.timeoutMs) stale.push(clientId)\n }\n for (const clientId of stale) this.remove(clientId)\n }\n\n startSweeping(intervalMs: number = Math.max(1000, Math.floor(this.timeoutMs / 4))): void {\n if (this.sweeper !== null) return\n this.sweeper = setInterval(() => this.sweep(), intervalMs)\n }\n\n stopSweeping(): void {\n if (this.sweeper === null) return\n clearInterval(this.sweeper)\n this.sweeper = null\n }\n\n /** Remove every record this room put in the store. */\n clear(): void {\n const ids = Array.from(this.seen.values(), (entry) => entry.id)\n this.seen.clear()\n for (const id of ids) this.removeRecord(id)\n }\n\n private removeRecord(id: string): void {\n const recordId = id as IdOf<R>\n if (!this.store.unsafeGetWithoutCapture(recordId)) return\n this.store.mergeRemoteChanges(() => {\n this.store.remove([recordId])\n })\n }\n}\n","import type { RecordsDiff, UnknownRecord } from \"@mocanvas/store\"\n\n/** Bumped when the wire format changes incompatibly. */\nexport const PROTOCOL_VERSION = 1\n\n/**\n * Every message is one JSON object with a `type` discriminator.\n *\n * ```\n * hello a client joined; existing peers answer with a snapshot\n * snapshot the document-scope records as the sender currently sees them\n * diff one squashed store diff, in document scope\n * presence one presence record (cursor, camera, selection)\n * bye the client is leaving; drop its presence\n * ```\n */\nexport type SyncMessage<R extends UnknownRecord = UnknownRecord> =\n | HelloMessage\n | SnapshotMessage<R>\n | DiffMessage<R>\n | PresenceMessage<R>\n | ByeMessage\n\nexport interface HelloMessage {\n type: \"hello\"\n clientId: string\n version: number\n}\n\nexport interface SnapshotMessage<R extends UnknownRecord = UnknownRecord> {\n type: \"snapshot\"\n records: R[]\n}\n\nexport interface DiffMessage<R extends UnknownRecord = UnknownRecord> {\n type: \"diff\"\n clientId: string\n /** Per-client counter; only used for logging and de-duplication. */\n seq: number\n diff: RecordsDiff<R>\n}\n\nexport interface PresenceMessage<R extends UnknownRecord = UnknownRecord> {\n type: \"presence\"\n clientId: string\n record: R\n}\n\nexport interface ByeMessage {\n type: \"bye\"\n clientId: string\n}\n\nexport function encodeMessage<R extends UnknownRecord>(message: SyncMessage<R>): string {\n return JSON.stringify(message)\n}\n\n/**\n * Parse a message off the wire. Returns `null` for anything malformed: a peer\n * on a newer protocol must never be able to crash this one.\n */\nexport function decodeMessage<R extends UnknownRecord = UnknownRecord>(data: unknown): SyncMessage<R> | null {\n let value: unknown = data\n if (typeof data === \"string\") {\n try {\n value = JSON.parse(data)\n } catch {\n return null\n }\n }\n if (typeof value !== \"object\" || value === null) return null\n const message = value as Record<string, unknown>\n switch (message[\"type\"]) {\n case \"hello\":\n if (typeof message[\"clientId\"] !== \"string\") return null\n if (typeof message[\"version\"] !== \"number\") return null\n return { type: \"hello\", clientId: message[\"clientId\"], version: message[\"version\"] }\n case \"snapshot\": {\n const records = message[\"records\"]\n if (!Array.isArray(records)) return null\n if (!records.every(isRecordLike)) return null\n return { type: \"snapshot\", records: records as R[] }\n }\n case \"diff\": {\n if (typeof message[\"clientId\"] !== \"string\") return null\n if (typeof message[\"seq\"] !== \"number\") return null\n if (!isDiffLike(message[\"diff\"])) return null\n return {\n type: \"diff\",\n clientId: message[\"clientId\"],\n seq: message[\"seq\"],\n diff: message[\"diff\"] as RecordsDiff<R>,\n }\n }\n case \"presence\": {\n if (typeof message[\"clientId\"] !== \"string\") return null\n if (!isRecordLike(message[\"record\"])) return null\n return { type: \"presence\", clientId: message[\"clientId\"], record: message[\"record\"] as R }\n }\n case \"bye\":\n if (typeof message[\"clientId\"] !== \"string\") return null\n return { type: \"bye\", clientId: message[\"clientId\"] }\n default:\n return null\n }\n}\n\nfunction isRecordLike(value: unknown): boolean {\n return (\n typeof value === \"object\" &&\n value !== null &&\n typeof (value as { id?: unknown }).id === \"string\" &&\n typeof (value as { typeName?: unknown }).typeName === \"string\"\n )\n}\n\nfunction isDiffLike(value: unknown): boolean {\n if (typeof value !== \"object\" || value === null) return false\n const diff = value as Record<string, unknown>\n for (const key of [\"added\", \"updated\", \"removed\"]) {\n const part = diff[key]\n if (typeof part !== \"object\" || part === null || Array.isArray(part)) return false\n }\n return true\n}\n","import { atom, type Signal } from \"@mocanvas/state\"\nimport { isRecordsDiffEmpty, uniqueId, type Store, type UnknownRecord } from \"@mocanvas/store\"\nimport {\n createPresenceSync,\n DEFAULT_PRESENCE_TIMEOUT_MS,\n PresenceRoom,\n type PresenceEditor,\n type PresenceSync,\n} from \"./presence\"\nimport { PROTOCOL_VERSION, type SyncMessage } from \"./protocol\"\nimport type { Transport } from \"./transport\"\n\nexport type SyncStatus = \"offline\" | \"connecting\" | \"online\"\n\nexport interface SyncClientOptions<R extends UnknownRecord = UnknownRecord> {\n store: Store<R, any>\n /** Identifies the document. The transport is expected to be scoped to it too. */\n roomId: string\n transport: Transport<R>\n /** Turn on cursor/selection sharing for an editor. */\n presence?: { editor: PresenceEditor } | undefined\n /** Defaults to a fresh random id; one per tab, not per user. */\n clientId?: string | undefined\n /** Drop a collaborator after this long without a presence message. Default 10s. */\n presenceTimeoutMs?: number | undefined\n /** Upper bound on presence send rate. Default 34ms (~30 Hz). */\n presenceThrottleMs?: number | undefined\n presenceHeartbeatMs?: number | undefined\n /** Called for protocol-level problems (a peer on another version, say). */\n onError?: ((error: Error) => void) | undefined\n}\n\nexport interface SyncClient {\n readonly clientId: string\n readonly roomId: string\n connect(): void\n disconnect(): void\n /** A signal, so React and reactors can follow the connection. */\n getStatus(): Signal<SyncStatus>\n dispose(): void\n}\n\n/**\n * Joins one room over `transport` and keeps `store` in step with its peers.\n *\n * Document changes made locally by the user are broadcast as diffs; incoming\n * diffs are applied as remote changes so they neither echo back nor land in\n * the undo stack. Conflicts are resolved last-writer-wins per record.\n */\nexport function createSyncClient<R extends UnknownRecord = UnknownRecord>(\n options: SyncClientOptions<R>,\n): SyncClient {\n const { store, roomId, transport } = options\n const clientId = options.clientId ?? uniqueId(12)\n const status = atom<SyncStatus>(`sync.status:${roomId}`, \"offline\")\n const room = new PresenceRoom<R>(store, options.presenceTimeoutMs ?? DEFAULT_PRESENCE_TIMEOUT_MS)\n\n const unsubscribes: (() => void)[] = []\n let presence: PresenceSync | null = null\n let seq = 0\n /** True between joining and either receiving a snapshot or serving one. */\n let awaitingSnapshot = false\n let connected = false\n let disposed = false\n\n const send = (message: SyncMessage<R>) => {\n try {\n transport.send(message)\n } catch (error) {\n options.onError?.(error instanceof Error ? error : new Error(String(error)))\n }\n }\n\n const handle = (message: SyncMessage<R>) => {\n if (disposed) return\n switch (message.type) {\n case \"hello\": {\n if (message.clientId === clientId) return\n if (message.version !== PROTOCOL_VERSION) {\n options.onError?.(\n new Error(`Peer ${message.clientId} speaks protocol ${message.version}, we speak ${PROTOCOL_VERSION}`),\n )\n return\n }\n // A newcomer needs the document; every established peer answers and the\n // newcomer keeps the first answer. Answering also makes us established,\n // so a snapshot meant for that newcomer cannot overwrite what we have.\n awaitingSnapshot = false\n send({ type: \"snapshot\", records: Object.values(store.serialize(\"document\")) as R[] })\n // Re-announce our presence so the newcomer sees us immediately.\n presence?.poke()\n return\n }\n case \"snapshot\": {\n // Only a newcomer applies a snapshot; otherwise a late peer would\n // clobber work done since we joined.\n if (!awaitingSnapshot) return\n awaitingSnapshot = false\n store.mergeRemoteChanges(() => {\n store.put(message.records)\n })\n return\n }\n case \"diff\": {\n if (message.clientId === clientId) return\n if (isRecordsDiffEmpty(message.diff)) return\n store.mergeRemoteChanges(() => {\n store.applyDiff(message.diff)\n })\n return\n }\n case \"presence\": {\n if (message.clientId === clientId) return\n room.receive(message.clientId, message.record)\n return\n }\n case \"bye\": {\n if (message.clientId === clientId) return\n room.remove(message.clientId)\n return\n }\n }\n }\n\n const onOpen = () => {\n if (disposed || !connected) return\n status.set(\"online\")\n send({ type: \"hello\", clientId, version: PROTOCOL_VERSION })\n presence?.poke()\n }\n\n const onClose = () => {\n if (disposed || !connected) return\n // The transport reconnects on its own; from here it is just \"not online\".\n status.set(\"connecting\")\n }\n\n return {\n clientId,\n roomId,\n\n connect() {\n if (disposed || connected) return\n connected = true\n awaitingSnapshot = true\n status.set(\"connecting\")\n\n unsubscribes.push(transport.onMessage(handle))\n if (transport.onOpen) unsubscribes.push(transport.onOpen(onOpen))\n if (transport.onClose) unsubscribes.push(transport.onClose(onClose))\n\n // Outgoing document changes: only what this user did, never what we\n // applied from a peer (those arrive with source \"remote\").\n unsubscribes.push(\n store.listen(\n (entry) => {\n if (isRecordsDiffEmpty(entry.changes)) return\n send({ type: \"diff\", clientId, seq: seq++, diff: entry.changes })\n },\n { source: \"user\", scope: \"document\" },\n ),\n )\n\n if (options.presence) {\n presence = createPresenceSync({\n editor: options.presence.editor,\n clientId,\n send: (record) => send({ type: \"presence\", clientId, record: record as unknown as R }),\n ...(options.presenceThrottleMs !== undefined ? { throttleMs: options.presenceThrottleMs } : {}),\n ...(options.presenceHeartbeatMs !== undefined ? { heartbeatMs: options.presenceHeartbeatMs } : {}),\n })\n presence.start()\n }\n room.startSweeping()\n\n if (!transport.onOpen) onOpen()\n },\n\n disconnect() {\n if (!connected) return\n connected = false\n send({ type: \"bye\", clientId })\n presence?.stop()\n presence = null\n room.stopSweeping()\n room.clear()\n for (const off of unsubscribes.splice(0)) off()\n awaitingSnapshot = false\n status.set(\"offline\")\n },\n\n getStatus() {\n return status\n },\n\n dispose() {\n if (disposed) return\n this.disconnect()\n disposed = true\n transport.close()\n },\n }\n}\n","import type { UnknownRecord } from \"@mocanvas/store\"\nimport { decodeMessage, encodeMessage, type SyncMessage } from \"./protocol\"\n\n/**\n * A duplex channel that carries `SyncMessage`s between peers in one room.\n *\n * The transport owns serialization and reconnection; the client above it only\n * sees decoded messages. `onOpen` fires every time the channel becomes usable\n * (including after a reconnect), `onClose` every time it stops being usable.\n * A transport with neither is treated as open from the moment it is connected.\n */\nexport interface Transport<R extends UnknownRecord = UnknownRecord> {\n send(message: SyncMessage<R>): void\n onMessage(callback: (message: SyncMessage<R>) => void): () => void\n onOpen?(callback: () => void): () => void\n onClose?(callback: () => void): () => void\n close(): void\n}\n\n/** @internal A tiny callback set with an unsubscribe function. */\nexport function createEmitter(): {\n add(cb: () => void): () => void\n emit(): void\n clear(): void\n} {\n const callbacks = new Set<() => void>()\n return {\n add(cb) {\n callbacks.add(cb)\n return () => {\n callbacks.delete(cb)\n }\n },\n emit() {\n for (const cb of Array.from(callbacks)) cb()\n },\n clear() {\n callbacks.clear()\n },\n }\n}\n\n/* -------------------------------------------------------------------------- */\n/* BroadcastChannel */\n/* -------------------------------------------------------------------------- */\n\nexport interface BroadcastChannelTransportOptions {\n /** Channel name prefix, so two apps on one origin do not collide. */\n prefix?: string\n}\n\n/**\n * Same-origin tabs of one browser. There is no server, so every peer is\n * equally authoritative and the channel is open as soon as it is created.\n */\nexport function createBroadcastChannelTransport<R extends UnknownRecord = UnknownRecord>(\n roomId: string,\n options: BroadcastChannelTransportOptions = {},\n): Transport<R> {\n const name = `${options.prefix ?? \"mocanvas-sync\"}:${roomId}`\n const channel = new BroadcastChannel(name)\n const listeners = new Set<(message: SyncMessage<R>) => void>()\n let closed = false\n\n channel.onmessage = (event: MessageEvent) => {\n const message = decodeMessage<R>(event.data)\n if (!message) return\n for (const listener of Array.from(listeners)) listener(message)\n }\n\n return {\n send(message) {\n if (closed) return\n channel.postMessage(encodeMessage(message))\n },\n onMessage(callback) {\n listeners.add(callback)\n return () => {\n listeners.delete(callback)\n }\n },\n onOpen(callback) {\n // Already usable; hand control back to the caller before firing so that\n // `connect()` has finished wiring itself up.\n let cancelled = false\n queueMicrotask(() => {\n if (!cancelled && !closed) callback()\n })\n return () => {\n cancelled = true\n }\n },\n close() {\n closed = true\n listeners.clear()\n channel.onmessage = null\n channel.close()\n },\n }\n}\n\n/* -------------------------------------------------------------------------- */\n/* WebSocket */\n/* -------------------------------------------------------------------------- */\n\nexport interface WebSocketTransportOptions {\n /** Reconnect with exponential backoff after an unexpected close. Default `true`. */\n reconnect?: boolean\n /** First backoff delay in ms. Default 500. */\n minDelayMs?: number\n /** Backoff ceiling in ms. Default 15000. */\n maxDelayMs?: number\n /** Injectable for tests and for Node (`ws`). Defaults to `globalThis.WebSocket`. */\n WebSocketImpl?: typeof WebSocket\n}\n\n/**\n * A WebSocket to a relay (see `scripts/relay.mjs`). Messages sent while the\n * socket is down are dropped rather than queued: the next `hello`/`snapshot`\n * exchange re-establishes the document anyway, and presence is re-sent on a\n * heartbeat.\n */\nexport function createWebSocketTransport<R extends UnknownRecord = UnknownRecord>(\n url: string,\n options: WebSocketTransportOptions = {},\n): Transport<R> {\n const reconnect = options.reconnect ?? true\n const minDelay = options.minDelayMs ?? 500\n const maxDelay = options.maxDelayMs ?? 15_000\n const Impl = options.WebSocketImpl ?? (globalThis as { WebSocket?: typeof WebSocket }).WebSocket\n if (!Impl) throw new Error(\"No WebSocket implementation available; pass options.WebSocketImpl\")\n\n const listeners = new Set<(message: SyncMessage<R>) => void>()\n const open = createEmitter()\n const close = createEmitter()\n\n let socket: WebSocket | null = null\n let attempt = 0\n let timer: ReturnType<typeof setTimeout> | null = null\n let disposed = false\n\n const connect = () => {\n if (disposed) return\n const ws = new Impl(url)\n socket = ws\n ws.onopen = () => {\n attempt = 0\n open.emit()\n }\n ws.onmessage = (event: MessageEvent) => {\n const message = decodeMessage<R>(event.data)\n if (!message) return\n for (const listener of Array.from(listeners)) listener(message)\n }\n ws.onerror = () => {\n // `onclose` always follows; the retry is scheduled there.\n }\n ws.onclose = () => {\n if (socket === ws) socket = null\n close.emit()\n if (!disposed && reconnect) schedule()\n }\n }\n\n const schedule = () => {\n if (timer !== null) return\n // Exponential backoff with full jitter, so a relay restart does not get a\n // thundering herd from every open tab.\n const ceiling = Math.min(maxDelay, minDelay * 2 ** attempt)\n attempt++\n const delay = Math.random() * ceiling\n timer = setTimeout(() => {\n timer = null\n connect()\n }, delay)\n }\n\n connect()\n\n return {\n send(message) {\n if (!socket || socket.readyState !== 1 /* OPEN */) return\n socket.send(encodeMessage(message))\n },\n onMessage(callback) {\n listeners.add(callback)\n return () => {\n listeners.delete(callback)\n }\n },\n onOpen(callback) {\n const remove = open.add(callback)\n if (socket && socket.readyState === 1) queueMicrotask(callback)\n return remove\n },\n onClose(callback) {\n return close.add(callback)\n },\n close() {\n disposed = true\n if (timer !== null) {\n clearTimeout(timer)\n timer = null\n }\n listeners.clear()\n open.clear()\n close.clear()\n const ws = socket\n socket = null\n if (ws) {\n ws.onopen = null\n ws.onmessage = null\n ws.onerror = null\n ws.onclose = null\n if (ws.readyState === 0 || ws.readyState === 1) ws.close()\n }\n },\n }\n}\n\n/* -------------------------------------------------------------------------- */\n/* in-memory */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Two transports wired to each other, going through the same JSON encode and\n * decode as the real ones. Delivery is asynchronous (a microtask) so that a\n * peer never observes a message inside its own `send()` call stack.\n */\nexport function createMemoryTransportPair<R extends UnknownRecord = UnknownRecord>(): [Transport<R>, Transport<R>] {\n const hub = createMemoryHub<R>()\n return [hub.join(), hub.join()]\n}\n\nexport interface MemoryHub<R extends UnknownRecord = UnknownRecord> {\n /** Add another peer to the room. */\n join(): Transport<R>\n}\n\n/** A room every joined transport broadcasts into (itself excluded). */\nexport function createMemoryHub<R extends UnknownRecord = UnknownRecord>(): MemoryHub<R> {\n const peers = new Set<{ deliver(data: string): void }>()\n return {\n join(): Transport<R> {\n const listeners = new Set<(message: SyncMessage<R>) => void>()\n let closed = false\n const peer = {\n deliver(data: string) {\n if (closed) return\n const message = decodeMessage<R>(data)\n if (!message) return\n for (const listener of Array.from(listeners)) listener(message)\n },\n }\n peers.add(peer)\n return {\n send(message) {\n if (closed) return\n const data = encodeMessage(message)\n for (const other of Array.from(peers)) {\n if (other === peer) continue\n queueMicrotask(() => other.deliver(data))\n }\n },\n onMessage(callback) {\n listeners.add(callback)\n return () => {\n listeners.delete(callback)\n }\n },\n onOpen(callback) {\n let cancelled = false\n queueMicrotask(() => {\n if (!cancelled && !closed) callback()\n })\n return () => {\n cancelled = true\n }\n },\n close() {\n closed = true\n listeners.clear()\n peers.delete(peer)\n },\n }\n },\n }\n}\n","import { track, useValue } from \"@mocanvas/state/react\"\nimport type { Editor, EditorRecord, InstancePresence, UnknownShape } from \"@mocanvas/editor\"\nimport { useEffect, useRef, useState, type CSSProperties } from \"react\"\nimport { createSyncClient, type SyncClient, type SyncClientOptions, type SyncStatus } from \"./SyncClient\"\nimport type { Transport } from \"./transport\"\n\nconst layerStyle: CSSProperties = {\n position: \"absolute\",\n inset: 0,\n width: \"100%\",\n height: \"100%\",\n pointerEvents: \"none\",\n overflow: \"visible\",\n}\n\nexport interface CollaboratorCursorsProps {\n editor: Editor\n /** Draw what each collaborator has selected. Default `true`. */\n showSelection?: boolean\n /** Draw a name chip next to each cursor. Default `true`. */\n showNames?: boolean\n}\n\n/**\n * Other people's cursors and selections, drawn in screen space above the\n * canvas. Drop it inside `<Mocanvas>` or `<Canvas>`; it positions itself.\n */\nexport const CollaboratorCursors = track(function CollaboratorCursors({\n editor,\n showSelection = true,\n showNames = true,\n}: CollaboratorCursorsProps) {\n const collaborators = editor.getCollaboratorsOnCurrentPage()\n if (collaborators.length === 0) return null\n return (\n <svg className=\"mocanvas-collaborators\" style={layerStyle}>\n {collaborators.map((presence) => (\n <g key={presence.id}>\n {showSelection ? <CollaboratorSelection editor={editor} presence={presence} /> : null}\n <CollaboratorCursor editor={editor} presence={presence} showName={showNames} />\n </g>\n ))}\n </svg>\n )\n})\n\nconst CollaboratorCursor = track(function CollaboratorCursor({\n editor,\n presence,\n showName,\n}: {\n editor: Editor\n presence: InstancePresence\n showName: boolean\n}) {\n const point = editor.pageToScreen(presence.cursor)\n const name = presence.userName || \"Anonymous\"\n // A 16x22 arrow drawn from the hotspot, then the name chip below it.\n return (\n <g transform={`translate(${point.x}, ${point.y}) rotate(${presence.cursor.rotation})`}>\n <path\n d=\"M0 0 L0 17.3 L4.3 13.4 L7 19.4 L9.9 18 L7.2 12.2 L12.8 12 Z\"\n fill={presence.color}\n stroke=\"#fff\"\n strokeWidth={1}\n strokeLinejoin=\"round\"\n />\n {showName ? (\n <g transform=\"translate(11, 18)\">\n <rect rx={4} ry={4} width={Math.max(24, name.length * 7 + 12)} height={18} fill={presence.color} />\n <text x={6} y={13} fontSize={11} fontFamily=\"system-ui, sans-serif\" fill=\"#fff\">\n {name}\n </text>\n </g>\n ) : null}\n </g>\n )\n})\n\nconst CollaboratorSelection = track(function CollaboratorSelection({\n editor,\n presence,\n}: {\n editor: Editor\n presence: InstancePresence\n}) {\n const outlines: string[] = []\n for (const id of presence.selectedShapeIds) {\n const shape: UnknownShape | undefined = editor.getShape(id)\n if (!shape) continue\n const bounds = editor.getShapeGeometryBounds(shape)\n if (!bounds) continue\n const m = editor.getShapePageTransform(shape)\n // Transform the local corners to page space, then to screen space, so the\n // outline follows rotation without a nested SVG transform.\n const corners: [number, number][] = [\n [bounds.x, bounds.y],\n [bounds.maxX, bounds.y],\n [bounds.maxX, bounds.maxY],\n [bounds.x, bounds.maxY],\n ]\n const points = corners.map(([x, y]) => {\n const screen = editor.pageToScreen({ x: m.a * x + m.c * y + m.e, y: m.b * x + m.d * y + m.f })\n return `${screen.x},${screen.y}`\n })\n outlines.push(points.join(\" \"))\n }\n if (outlines.length === 0) return null\n return (\n <>\n {outlines.map((points, i) => (\n <polygon key={i} points={points} fill=\"none\" stroke={presence.color} strokeWidth={1.5} opacity={0.8} />\n ))}\n </>\n )\n})\n\nexport interface UseSyncOptions {\n roomId: string\n /**\n * The channel to the room. Pass a factory when the transport should be\n * recreated with the room (the usual case); it is read once per connection.\n */\n transport: Transport | (() => Transport)\n /** Share this editor's cursor and selection. Default `true`. */\n presence?: boolean\n /** Set `false` to stay offline (a read-only view, say). Default `true`. */\n enabled?: boolean\n clientId?: string\n presenceTimeoutMs?: number\n presenceThrottleMs?: number\n onError?: (error: Error) => void\n}\n\nexport interface UseSyncResult {\n status: SyncStatus\n client: SyncClient | null\n}\n\n/**\n * Connect an editor's store to a room for as long as the component is mounted.\n * Everything is torn down (including the transport) on unmount.\n */\nexport function useSync(editor: Editor | null, options: UseSyncOptions): UseSyncResult {\n const { roomId, enabled = true, presence = true } = options\n // Kept in a ref so that inline factories and callbacks do not reconnect.\n const latest = useRef(options)\n latest.current = options\n\n const [client, setClient] = useState<SyncClient | null>(null)\n\n useEffect(() => {\n if (!editor || !enabled) {\n setClient(null)\n return\n }\n const current = latest.current\n const transport = typeof current.transport === \"function\" ? current.transport() : current.transport\n const clientOptions: SyncClientOptions<EditorRecord> = {\n store: editor.store,\n roomId,\n transport: transport as Transport<EditorRecord>,\n ...(presence ? { presence: { editor } } : {}),\n ...(current.clientId !== undefined ? { clientId: current.clientId } : {}),\n ...(current.presenceTimeoutMs !== undefined ? { presenceTimeoutMs: current.presenceTimeoutMs } : {}),\n ...(current.presenceThrottleMs !== undefined ? { presenceThrottleMs: current.presenceThrottleMs } : {}),\n onError: (error: Error) => latest.current.onError?.(error),\n }\n const next = createSyncClient<EditorRecord>(clientOptions)\n setClient(next)\n next.connect()\n return () => {\n setClient(null)\n next.dispose()\n }\n }, [editor, roomId, enabled, presence])\n\n const status = useValue(\"sync.status\", () => client?.getStatus().get() ?? \"offline\", [client])\n return { status, client }\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@mocanvas/sync",
3
+ "version": "1.0.0",
4
+ "description": "Multiplayer for a mocanvas store: record diffs over a transport, plus presence and cursors.",
5
+ "license": "MIT",
6
+ "author": "Symbio Digital",
7
+ "homepage": "https://github.com/SYMBIO/mocanvas/tree/main/packages/sync#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/SYMBIO/mocanvas.git",
11
+ "directory": "packages/sync"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/SYMBIO/mocanvas/issues"
15
+ },
16
+ "keywords": [
17
+ "multiplayer",
18
+ "collaboration",
19
+ "presence",
20
+ "websocket",
21
+ "sync",
22
+ "mocanvas"
23
+ ],
24
+ "type": "module",
25
+ "engines": {
26
+ "node": ">=20"
27
+ },
28
+ "files": [
29
+ "scripts",
30
+ "dist",
31
+ "README.md",
32
+ "LICENSE"
33
+ ],
34
+ "sideEffects": false,
35
+ "main": "./dist/index.js",
36
+ "types": "./dist/index.d.ts",
37
+ "exports": {
38
+ ".": {
39
+ "types": "./dist/index.d.ts",
40
+ "import": "./dist/index.js"
41
+ },
42
+ "./package.json": "./package.json"
43
+ },
44
+ "publishConfig": {
45
+ "access": "public"
46
+ },
47
+ "dependencies": {
48
+ "@mocanvas/state": "1.0.0",
49
+ "@mocanvas/store": "1.0.0",
50
+ "@mocanvas/editor": "1.0.0"
51
+ },
52
+ "devDependencies": {
53
+ "@types/react": "^19.1.0",
54
+ "@types/ws": "^8.5.0",
55
+ "react": "^19.1.0",
56
+ "ws": "^8.18.0"
57
+ },
58
+ "peerDependencies": {
59
+ "react": ">=18"
60
+ },
61
+ "scripts": {
62
+ "build": "tsup",
63
+ "test": "vitest run",
64
+ "typecheck": "tsc --noEmit",
65
+ "relay": "node scripts/relay.mjs"
66
+ }
67
+ }
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+ // A room relay for @mocanvas/sync: every message received on a socket is
3
+ // forwarded verbatim to the other sockets in the same room. The room is the
4
+ // URL path, so ws://localhost:5858/my-doc is one room and /other is another.
5
+ //
6
+ // It keeps no document state. Peers hand each other the document with the
7
+ // hello/snapshot exchange, so the relay can be restarted at any time.
8
+ //
9
+ // node scripts/relay.mjs # port 5858
10
+ // PORT=9000 node scripts/relay.mjs
11
+ import { WebSocketServer } from "ws"
12
+
13
+ const port = Number(process.env["PORT"] ?? 5858)
14
+ const server = new WebSocketServer({ port })
15
+ /** @type {Map<string, Set<import("ws").WebSocket>>} */
16
+ const rooms = new Map()
17
+
18
+ server.on("connection", (socket, request) => {
19
+ const room = new URL(request.url ?? "/", "http://localhost").pathname || "/"
20
+ let peers = rooms.get(room)
21
+ if (!peers) {
22
+ peers = new Set()
23
+ rooms.set(room, peers)
24
+ }
25
+ peers.add(socket)
26
+ log(`+ ${room} (${peers.size} peer${peers.size === 1 ? "" : "s"})`)
27
+
28
+ socket.on("message", (data, isBinary) => {
29
+ if (isBinary) return
30
+ const text = data.toString()
31
+ for (const peer of peers) {
32
+ if (peer === socket) continue
33
+ if (peer.readyState !== peer.OPEN) continue
34
+ peer.send(text)
35
+ }
36
+ })
37
+
38
+ socket.on("error", () => socket.close())
39
+
40
+ socket.on("close", () => {
41
+ peers.delete(socket)
42
+ if (peers.size === 0) rooms.delete(room)
43
+ log(`- ${room} (${peers.size} peer${peers.size === 1 ? "" : "s"})`)
44
+ })
45
+ })
46
+
47
+ server.on("listening", () => log(`relay listening on ws://localhost:${port}/<room>`))
48
+
49
+ function log(message) {
50
+ process.stdout.write(`[mocanvas-relay] ${message}\n`)
51
+ }
52
+
53
+ for (const signal of ["SIGINT", "SIGTERM"]) {
54
+ process.on(signal, () => {
55
+ server.close(() => process.exit(0))
56
+ })
57
+ }