@mmstack/mesh 21.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/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # @mmstack/mesh
2
+
3
+ > **Experimental.** The API may still change and this package is not yet battle-tested in production. Pin a version and expect some churn.
4
+
5
+ Multiplayer for `@mmstack/primitives` signal stores. `meshSync(store, ...)` replicates a store
6
+ across clients through a relay, and a synced store reads exactly like a local one: synchronous,
7
+ no new nullability, no callbacks in your components. Connection state surfaces through a status
8
+ signal and the transition scope, never as an exception from a read.
9
+
10
+ ```bash
11
+ npm install @mmstack/mesh @mmstack/mesh-protocol
12
+ ```
13
+
14
+ The relay is [`@mmstack/mesh-protocol`](https://www.npmjs.com/package/@mmstack/mesh-protocol),
15
+ which has no dependencies and runs on Node, Bun, or a Durable Object. You bring your own socket
16
+ and your own auth.
17
+
18
+ ## meshSync
19
+
20
+ ```ts
21
+ import { store } from '@mmstack/primitives';
22
+ import { meshSync, webSocketTransport } from '@mmstack/mesh';
23
+
24
+ const board = store<Board>(initialBoard());
25
+
26
+ const mesh = meshSync(board, {
27
+ room: 'board-42',
28
+ writer: currentUserId, // an opaque principal id, never a display name
29
+ transport: webSocketTransport('wss://sync.example.com'),
30
+ });
31
+ ```
32
+
33
+ Write to `board` like any store. Local changes emit to the relay, remote changes fold in, and
34
+ both sides converge. `mesh.status()` is `'connecting' | 'live' | 'reconnecting' | 'ejected'`,
35
+ and `mesh.peers()` is the current presence roster. When you provide `register: 'track'`, the
36
+ store also participates in transition scopes, so a reconnect shows up as `pending` to a
37
+ `<mm-suspense>` boundary without any wiring.
38
+
39
+ Reconnection is automatic, with exponential backoff. On reconnect the client resumes from a
40
+ delta when possible, and re-applies any writes made while offline on top of whatever the room
41
+ moved to in the meantime. A relay restart is detected through a room epoch, so a stale sequence
42
+ number never corrupts state.
43
+
44
+ ## Conflict resolution
45
+
46
+ By default the latest write to a path wins, decided by a hybrid logical clock so every peer
47
+ agrees. Because resolution is per path, two people editing different fields of the same record
48
+ merge cleanly. When you need something other than last-writer-wins, attach a policy per path:
49
+
50
+ ```ts
51
+ import { keyedArray, preserve } from '@mmstack/primitives';
52
+
53
+ meshSync(board, {
54
+ room: 'board-42',
55
+ writer: currentUserId,
56
+ transport,
57
+ policies: [
58
+ // reconcile a list by item identity, so concurrent edits to different todos both survive
59
+ { path: 'todos', merge: keyedArray((t) => t.id) },
60
+ // keep both sides of a clashing edit as data instead of dropping one
61
+ { path: 'title', merge: preserve },
62
+ ],
63
+ });
64
+ ```
65
+
66
+ `preserve` turns a clash into a `Conflicted` value holding both sides, so nothing is silently
67
+ lost and a resolution is just a later write. See `@mmstack/primitives` for the full set of
68
+ merge policies; they are the same ones the store uses for forks and tabs.
69
+
70
+ ## Presence
71
+
72
+ ```ts
73
+ mesh.setPresence({ cursor: [x, y], section: 'pricing' });
74
+
75
+ // in a component
76
+ const others = mesh.peers(); // [{ writer, origin, data }, ...]
77
+ ```
78
+
79
+ Presence is an ephemeral side channel. It is never persisted, never conflicts, and drops
80
+ automatically when a peer leaves. The payload is yours to shape: cursors, selection, "who is
81
+ here", or an agent's current activity.
82
+
83
+ ## Trust
84
+
85
+ Pass a `policy` and (when your policy reads claims) a `ctx`, and the client validates each write
86
+ before it hits the wire, matching the relay's own check. An honest client never emits an op the
87
+ relay would reject, so the tripwire only ever fires on a broken or hostile peer.
88
+
89
+ ```ts
90
+ meshSync(store, {
91
+ room,
92
+ writer,
93
+ transport,
94
+ policy: myOpPolicy,
95
+ ctx: { kind: 'human', claims: { role: 'editor' } },
96
+ });
97
+ ```
98
+
99
+ ## Transports
100
+
101
+ - `webSocketTransport(url)` for a relay over WebSocket.
102
+ - `directTransport(relay, ctx)` connects straight to an in-process `createRelay`, with no
103
+ network. It is the backbone of the tests, and useful for a single-process demo or an
104
+ SSR-side room.
105
+
106
+ A transport is a small interface (`send`, `onMessage`, `onClose`, `close`), so wiring a custom
107
+ one is a few lines.
108
+
109
+ ## Peer to peer
110
+
111
+ `webRtcMesh` runs the same convergence over WebRTC data channels, using the relay only for
112
+ signaling and membership. Peers exchange watermarks when a channel opens and catch each other
113
+ up pairwise. It takes an injectable connector, defaulting to a `RTCPeerConnection` adapter with
114
+ perfect-negotiation handling built in.
115
+
116
+ ```ts
117
+ import { webRtcMesh, webSocketTransport } from '@mmstack/mesh';
118
+
119
+ const mesh = webRtcMesh(store, {
120
+ room: 'call-7',
121
+ writer: currentUserId,
122
+ signaling: webSocketTransport('wss://sync.example.com'), // data flows peer to peer
123
+ });
124
+ ```
@@ -0,0 +1,570 @@
1
+ import { inject, Injector, signal, computed, isDevMode, runInInjectionContext, DestroyRef } from '@angular/core';
2
+ import { MESH_PROTO_VERSION, checkEnvelope } from '@mmstack/mesh-protocol';
3
+ import { opSync, registerResource } from '@mmstack/primitives';
4
+
5
+ const RECONNECT_BASE_MS = 500;
6
+ /**
7
+ * Replicates a signal store across clients through a relay room: local writes emit stamped
8
+ * envelopes, remote envelopes fold in convergently, reconnects resume via delta or snapshot
9
+ * with unacknowledged local writes rebased on top, and presence rides an ephemeral channel.
10
+ * A synced store reads exactly like a local one — connection state surfaces only through
11
+ * `status` and the transition scope (op-protocol RFC §10).
12
+ */
13
+ function meshSync(source, opt) {
14
+ const injector = opt.injector ?? inject(Injector);
15
+ const status = signal('connecting', ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
16
+ const peerMap = signal(new Map(), ...(ngDevMode ? [{ debugName: "peerMap" }] : /* istanbul ignore next */ []));
17
+ const peers = computed(() => [...peerMap().values()], ...(ngDevMode ? [{ debugName: "peers" }] : /* istanbul ignore next */ []));
18
+ const policyVersion = opt.policyVersion ?? 0;
19
+ const sync = opSync(source, {
20
+ writer: opt.writer,
21
+ policies: opt.policies,
22
+ policyVersion,
23
+ injector,
24
+ });
25
+ const unacked = new Map();
26
+ let highestAcked = 0;
27
+ let lastSeq = 0;
28
+ let epoch;
29
+ let presenceData;
30
+ let hasPresence = false;
31
+ let transport = null;
32
+ let attempts = 0;
33
+ let reconnectTimer;
34
+ let unsubs = [];
35
+ let closed = false;
36
+ const terminal = (state, reason) => {
37
+ closed = true;
38
+ if (reconnectTimer !== undefined)
39
+ clearTimeout(reconnectTimer);
40
+ for (const unsub of unsubs.splice(0))
41
+ unsub();
42
+ unsubLocal();
43
+ unacked.clear();
44
+ transport?.close();
45
+ transport = null;
46
+ peerMap.set(new Map());
47
+ status.set(state);
48
+ if (reason !== undefined)
49
+ opt.onEject?.(reason);
50
+ };
51
+ const sendEnv = (env) => {
52
+ transport?.send({ t: 'env', room: opt.room, env });
53
+ };
54
+ const flushUnacked = () => {
55
+ for (const env of [...unacked.values()].sort((a, b) => a.version - b.version)) {
56
+ sendEnv(env);
57
+ }
58
+ };
59
+ const handle = (msg) => {
60
+ if (msg.room !== opt.room)
61
+ return;
62
+ switch (msg.t) {
63
+ case 'welcome': {
64
+ if (epoch !== undefined && msg.epoch !== epoch)
65
+ lastSeq = 0;
66
+ epoch = msg.epoch;
67
+ peerMap.set(new Map(msg.peers.map((p) => [p.origin, p])));
68
+ if (msg.mode === 'delta') {
69
+ for (const env of msg.envs)
70
+ applyRemote(env);
71
+ }
72
+ else if (msg.mode === 'snapshot') {
73
+ sync.hydrate(msg.root, { [sync.origin]: highestAcked });
74
+ lastSeq = msg.seq;
75
+ }
76
+ else if (msg.seq === 0 && lastSeq === 0) {
77
+ sync.seed();
78
+ }
79
+ lastSeq = Math.max(lastSeq, msg.seq);
80
+ attempts = 0;
81
+ status.set('live');
82
+ flushUnacked();
83
+ if (hasPresence)
84
+ transport?.send({ t: 'presence', room: opt.room, data: presenceData });
85
+ return;
86
+ }
87
+ case 'env':
88
+ applyRemote(msg.env);
89
+ return;
90
+ case 'presence': {
91
+ const next = new Map(peerMap());
92
+ if (msg.gone)
93
+ next.delete(msg.peer.origin);
94
+ else
95
+ next.set(msg.peer.origin, msg.peer);
96
+ peerMap.set(next);
97
+ return;
98
+ }
99
+ case 'eject':
100
+ if (msg.writer === opt.writer)
101
+ terminal('ejected', msg.reason);
102
+ return;
103
+ case 'reject':
104
+ terminal('ejected', msg.reason);
105
+ return;
106
+ }
107
+ };
108
+ const applyRemote = (env) => {
109
+ lastSeq = Math.max(lastSeq, env.seq);
110
+ if (env.origin === sync.origin) {
111
+ unacked.delete(env.version);
112
+ highestAcked = Math.max(highestAcked, env.version);
113
+ return;
114
+ }
115
+ sync.receive(env);
116
+ };
117
+ const connect = () => {
118
+ if (closed)
119
+ return;
120
+ for (const unsub of unsubs.splice(0))
121
+ unsub();
122
+ const t = opt.transport();
123
+ transport = t;
124
+ unsubs = [
125
+ t.onMessage(handle),
126
+ t.onClose(() => {
127
+ if (closed || transport !== t)
128
+ return;
129
+ transport = null;
130
+ status.set('reconnecting');
131
+ const delay = Math.min(opt.reconnect?.maxDelayMs ?? 15_000, RECONNECT_BASE_MS * 2 ** attempts++);
132
+ reconnectTimer = setTimeout(() => {
133
+ reconnectTimer = undefined;
134
+ connect();
135
+ }, delay + Math.random() * 100);
136
+ }),
137
+ ];
138
+ t.send({
139
+ t: 'hello',
140
+ room: opt.room,
141
+ origin: sync.origin,
142
+ proto: MESH_PROTO_VERSION,
143
+ policyVersion,
144
+ seq: lastSeq > 0 ? lastSeq : undefined,
145
+ });
146
+ };
147
+ const unsubLocal = sync.subscribe((env) => {
148
+ const violation = checkEnvelope(opt.policy, env, { ...opt.ctx, writer: opt.writer });
149
+ if (violation) {
150
+ if (isDevMode()) {
151
+ console.warn('[@mmstack/mesh] local write violates the room policy — not sent', violation);
152
+ }
153
+ return;
154
+ }
155
+ unacked.set(env.version, env);
156
+ if (status() === 'live')
157
+ sendEnv(env);
158
+ });
159
+ if (opt.register) {
160
+ const connection = {
161
+ status: computed(() => {
162
+ const s = status();
163
+ return s === 'connecting'
164
+ ? 'loading'
165
+ : s === 'reconnecting'
166
+ ? 'reloading'
167
+ : s === 'ejected'
168
+ ? 'error'
169
+ : 'resolved';
170
+ }),
171
+ isLoading: computed(() => status() === 'connecting' || status() === 'reconnecting'),
172
+ hasValue: () => true,
173
+ };
174
+ runInInjectionContext(injector, () => registerResource(connection, { suspends: opt.register === 'suspend' }));
175
+ }
176
+ injector.get(DestroyRef).onDestroy(() => {
177
+ if (!closed)
178
+ terminal('closed');
179
+ unsubLocal();
180
+ sync.destroy();
181
+ });
182
+ connect();
183
+ return {
184
+ status: status.asReadonly(),
185
+ peers,
186
+ setPresence: (data) => {
187
+ presenceData = data;
188
+ hasPresence = true;
189
+ if (status() === 'live') {
190
+ transport?.send({ t: 'presence', room: opt.room, data });
191
+ }
192
+ },
193
+ close: () => {
194
+ if (!closed)
195
+ terminal('closed');
196
+ unsubLocal();
197
+ sync.destroy();
198
+ },
199
+ };
200
+ }
201
+
202
+ /**
203
+ * JSON-over-WebSocket transport. Messages sent before the socket opens are buffered and
204
+ * flushed on open; malformed inbound frames are dropped.
205
+ */
206
+ function webSocketTransport(url, protocols) {
207
+ return () => {
208
+ const ws = new WebSocket(url, protocols);
209
+ const messageCbs = new Set();
210
+ const closeCbs = new Set();
211
+ const pending = [];
212
+ ws.onopen = () => {
213
+ for (const frame of pending.splice(0))
214
+ ws.send(frame);
215
+ };
216
+ ws.onmessage = (event) => {
217
+ let msg;
218
+ try {
219
+ msg = JSON.parse(String(event.data));
220
+ }
221
+ catch {
222
+ if (isDevMode()) {
223
+ console.warn('[@mmstack/mesh] dropped unparseable frame (expected JSON text)');
224
+ }
225
+ return;
226
+ }
227
+ for (const cb of [...messageCbs])
228
+ cb(msg);
229
+ };
230
+ ws.onclose = () => {
231
+ for (const cb of [...closeCbs])
232
+ cb();
233
+ };
234
+ return {
235
+ send: (msg) => {
236
+ const frame = JSON.stringify(msg);
237
+ if (ws.readyState === WebSocket.OPEN)
238
+ ws.send(frame);
239
+ else if (ws.readyState === WebSocket.CONNECTING)
240
+ pending.push(frame);
241
+ },
242
+ onMessage: (cb) => {
243
+ messageCbs.add(cb);
244
+ return () => messageCbs.delete(cb);
245
+ },
246
+ onClose: (cb) => {
247
+ closeCbs.add(cb);
248
+ return () => closeCbs.delete(cb);
249
+ },
250
+ close: () => ws.close(),
251
+ };
252
+ };
253
+ }
254
+ /**
255
+ * Connects directly to an in-process `createRelay` — no network. The backbone of full-loop
256
+ * tests, and handy for single-process demos or an SSR-side room.
257
+ */
258
+ function directTransport(relay, ctx) {
259
+ return () => {
260
+ const messageCbs = new Set();
261
+ const closeCbs = new Set();
262
+ let open = true;
263
+ const connection = relay.connect({
264
+ send: (msg) => {
265
+ if (!open)
266
+ return;
267
+ for (const cb of [...messageCbs])
268
+ cb(msg);
269
+ },
270
+ close: () => {
271
+ if (!open)
272
+ return;
273
+ open = false;
274
+ connection.disconnect();
275
+ for (const cb of [...closeCbs])
276
+ cb();
277
+ },
278
+ }, ctx);
279
+ return {
280
+ send: (msg) => {
281
+ if (open)
282
+ connection.receive(msg);
283
+ },
284
+ onMessage: (cb) => {
285
+ messageCbs.add(cb);
286
+ return () => messageCbs.delete(cb);
287
+ },
288
+ onClose: (cb) => {
289
+ closeCbs.add(cb);
290
+ return () => closeCbs.delete(cb);
291
+ },
292
+ close: () => {
293
+ if (!open)
294
+ return;
295
+ open = false;
296
+ connection.disconnect();
297
+ for (const cb of [...closeCbs])
298
+ cb();
299
+ },
300
+ };
301
+ };
302
+ }
303
+
304
+ /** WebRTC `PeerConnector` implementing the perfect-negotiation pattern. Browser-only. */
305
+ function rtcPeerConnector(config) {
306
+ return ({ polite, sendSignal }) => {
307
+ const pc = new RTCPeerConnection(config);
308
+ const messageCbs = new Set();
309
+ const openCbs = new Set();
310
+ const closeCbs = new Set();
311
+ const pending = [];
312
+ let dc = null;
313
+ let makingOffer = false;
314
+ let ignoreOffer = false;
315
+ const attach = (channel) => {
316
+ dc = channel;
317
+ channel.onmessage = (e) => {
318
+ for (const cb of [...messageCbs])
319
+ cb(String(e.data));
320
+ };
321
+ channel.onopen = () => {
322
+ for (const frame of pending.splice(0))
323
+ channel.send(frame);
324
+ for (const cb of [...openCbs])
325
+ cb();
326
+ };
327
+ channel.onclose = () => {
328
+ for (const cb of [...closeCbs])
329
+ cb();
330
+ };
331
+ };
332
+ if (!polite)
333
+ attach(pc.createDataChannel('mmstack-mesh'));
334
+ else
335
+ pc.ondatachannel = (e) => attach(e.channel);
336
+ pc.onicecandidate = (e) => {
337
+ if (e.candidate)
338
+ sendSignal({ ice: e.candidate.toJSON() });
339
+ };
340
+ pc.onnegotiationneeded = async () => {
341
+ try {
342
+ makingOffer = true;
343
+ await pc.setLocalDescription();
344
+ sendSignal({ description: pc.localDescription });
345
+ }
346
+ finally {
347
+ makingOffer = false;
348
+ }
349
+ };
350
+ return {
351
+ channel: {
352
+ send: (frame) => {
353
+ if (dc && dc.readyState === 'open')
354
+ dc.send(frame);
355
+ else
356
+ pending.push(frame);
357
+ },
358
+ onMessage: (cb) => (messageCbs.add(cb), () => messageCbs.delete(cb)),
359
+ onOpen: (cb) => (openCbs.add(cb), () => openCbs.delete(cb)),
360
+ onClose: (cb) => (closeCbs.add(cb), () => closeCbs.delete(cb)),
361
+ close: () => dc?.close(),
362
+ },
363
+ signal: async (data) => {
364
+ const { description, ice } = (data ?? {});
365
+ if (description) {
366
+ const collision = description.type === 'offer' &&
367
+ (makingOffer || pc.signalingState !== 'stable');
368
+ ignoreOffer = !polite && collision;
369
+ if (ignoreOffer)
370
+ return;
371
+ await pc.setRemoteDescription(description);
372
+ if (description.type === 'offer') {
373
+ await pc.setLocalDescription();
374
+ sendSignal({ description: pc.localDescription });
375
+ }
376
+ }
377
+ else if (ice) {
378
+ try {
379
+ await pc.addIceCandidate(ice);
380
+ }
381
+ catch (err) {
382
+ if (!ignoreOffer)
383
+ throw err;
384
+ }
385
+ }
386
+ },
387
+ close: () => {
388
+ dc?.close();
389
+ pc.close();
390
+ },
391
+ };
392
+ };
393
+ }
394
+ /**
395
+ * Peer-to-peer mesh sync (unsequenced topology, op-protocol RFC §4): the relay only signals
396
+ * and tracks membership; envelopes flow over WebRTC data channels and converge via the
397
+ * per-path register map. Catch-up is pairwise: on channel open both sides exchange
398
+ * watermarks; a side whose state is strictly covered hydrates from the other. Two peers that
399
+ * diverged while BOTH held state keep their convergent go-forward guarantees but do not
400
+ * retroactively merge history (same contract as the tab rung).
401
+ */
402
+ function webRtcMesh(source, opt) {
403
+ const injector = opt.injector ?? inject(Injector);
404
+ const connector = opt.connector ?? rtcPeerConnector();
405
+ const status = signal('connecting', ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
406
+ const openPeers = signal(new Set(), ...(ngDevMode ? [{ debugName: "openPeers" }] : /* istanbul ignore next */ []));
407
+ const peers = new Map();
408
+ let signalingUnsubs = [];
409
+ let transport = null;
410
+ let reconnectTimer;
411
+ let closed = false;
412
+ const sync = opSync(source, {
413
+ writer: opt.writer,
414
+ policies: opt.policies,
415
+ policyVersion: opt.policyVersion,
416
+ injector,
417
+ });
418
+ const covered = (mine, theirs) => Object.entries(mine).every(([o, v]) => (theirs[o] ?? 0) >= v);
419
+ const sendTo = (peer, msg) => {
420
+ peer.link.channel.send(JSON.stringify(msg));
421
+ };
422
+ const broadcast = (msg) => {
423
+ for (const peer of peers.values()) {
424
+ if (peer.open)
425
+ sendTo(peer, msg);
426
+ }
427
+ };
428
+ const dropPeer = (origin) => {
429
+ const peer = peers.get(origin);
430
+ if (!peer)
431
+ return;
432
+ peers.delete(origin);
433
+ for (const unsub of peer.unsubs)
434
+ unsub();
435
+ peer.link.close();
436
+ openPeers.update((set) => {
437
+ const next = new Set(set);
438
+ next.delete(origin);
439
+ return next;
440
+ });
441
+ };
442
+ const ensurePeer = (origin) => {
443
+ let peer = peers.get(origin);
444
+ if (peer)
445
+ return peer;
446
+ const link = connector({
447
+ remote: origin,
448
+ polite: sync.origin > origin,
449
+ sendSignal: (data) => transport?.send({ t: 'signal', room: opt.room, to: origin, data }),
450
+ });
451
+ peer = { link, open: false, unsubs: [] };
452
+ peers.set(origin, peer);
453
+ peer.unsubs.push(link.channel.onOpen(() => {
454
+ peer.open = true;
455
+ openPeers.update((set) => new Set(set).add(origin));
456
+ sendTo(peer, { t: 'hello', wm: sync.watermark() });
457
+ }), link.channel.onMessage((frame) => {
458
+ let msg;
459
+ try {
460
+ msg = JSON.parse(frame);
461
+ }
462
+ catch {
463
+ return;
464
+ }
465
+ handlePeerMsg(peer, msg);
466
+ }), link.channel.onClose(() => dropPeer(origin)));
467
+ return peer;
468
+ };
469
+ const handlePeerMsg = (peer, msg) => {
470
+ switch (msg.t) {
471
+ case 'hello': {
472
+ const snap = sync.snapshot();
473
+ if (covered(snap.wm, msg.wm))
474
+ sendTo(peer, { t: 'uptodate' });
475
+ else
476
+ sendTo(peer, { t: 'state', root: snap.root, wm: snap.wm });
477
+ return;
478
+ }
479
+ case 'state': {
480
+ if (covered(sync.watermark(), msg.wm)) {
481
+ sync.hydrate(msg.root, msg.wm);
482
+ }
483
+ return;
484
+ }
485
+ case 'uptodate':
486
+ return;
487
+ case 'env':
488
+ sync.receive(msg.env);
489
+ return;
490
+ }
491
+ };
492
+ const handleSignaling = (msg) => {
493
+ if (msg.room !== opt.room)
494
+ return;
495
+ switch (msg.t) {
496
+ case 'welcome':
497
+ for (const origin of msg.members)
498
+ ensurePeer(origin);
499
+ status.set('live');
500
+ return;
501
+ case 'member':
502
+ if (msg.gone)
503
+ dropPeer(msg.origin);
504
+ else
505
+ ensurePeer(msg.origin);
506
+ return;
507
+ case 'signal':
508
+ ensurePeer(msg.from).link.signal(msg.data);
509
+ return;
510
+ case 'reject':
511
+ close();
512
+ return;
513
+ }
514
+ };
515
+ const connectSignaling = () => {
516
+ if (closed)
517
+ return;
518
+ for (const unsub of signalingUnsubs.splice(0))
519
+ unsub();
520
+ const t = opt.signaling();
521
+ transport = t;
522
+ signalingUnsubs = [
523
+ t.onMessage(handleSignaling),
524
+ t.onClose(() => {
525
+ if (closed || transport !== t)
526
+ return;
527
+ transport = null;
528
+ reconnectTimer = setTimeout(connectSignaling, 1000);
529
+ }),
530
+ ];
531
+ t.send({
532
+ t: 'hello',
533
+ room: opt.room,
534
+ origin: sync.origin,
535
+ proto: MESH_PROTO_VERSION,
536
+ policyVersion: opt.policyVersion ?? 0,
537
+ });
538
+ };
539
+ const unsubLocal = sync.subscribe((env) => broadcast({ t: 'env', env }));
540
+ const close = () => {
541
+ if (closed)
542
+ return;
543
+ closed = true;
544
+ if (reconnectTimer !== undefined)
545
+ clearTimeout(reconnectTimer);
546
+ unsubLocal();
547
+ for (const origin of [...peers.keys()])
548
+ dropPeer(origin);
549
+ for (const unsub of signalingUnsubs.splice(0))
550
+ unsub();
551
+ transport?.close();
552
+ transport = null;
553
+ sync.destroy();
554
+ status.set('connecting');
555
+ };
556
+ injector.get(DestroyRef).onDestroy(close);
557
+ connectSignaling();
558
+ return {
559
+ status: status.asReadonly(),
560
+ peers: computed(() => [...openPeers()]),
561
+ close,
562
+ };
563
+ }
564
+
565
+ /**
566
+ * Generated bundle index. Do not edit.
567
+ */
568
+
569
+ export { directTransport, meshSync, rtcPeerConnector, webRtcMesh, webSocketTransport };
570
+ //# sourceMappingURL=mmstack-mesh.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mmstack-mesh.mjs","sources":["../../../../../packages/mesh/client/src/lib/mesh-sync.ts","../../../../../packages/mesh/client/src/lib/transport.ts","../../../../../packages/mesh/client/src/lib/webrtc-mesh.ts","../../../../../packages/mesh/client/src/mmstack-mesh.ts"],"sourcesContent":["import {\n computed,\n DestroyRef,\n inject,\n Injector,\n isDevMode,\n runInInjectionContext,\n signal,\n type Signal,\n type WritableSignal,\n} from '@angular/core';\nimport {\n checkEnvelope,\n MESH_PROTO_VERSION,\n type OpPolicy,\n type PresenceState,\n type PrincipalCtx,\n type SeqEnvelope,\n type ServerMsg,\n} from '@mmstack/mesh-protocol';\nimport {\n opSync,\n registerResource,\n type MergePolicyEntry,\n type OpEnvelope,\n type ResourceLike,\n} from '@mmstack/primitives';\nimport type { MeshTransport, MeshTransportFactory } from './transport';\n\nexport type MeshStatus =\n | 'connecting'\n | 'live'\n | 'reconnecting'\n | 'ejected'\n | 'closed';\n\nexport type MeshPeer = PresenceState;\n\nexport type MeshSyncOptions = {\n readonly room: string;\n /** Opaque principal pseudonym — provided, never minted (op-protocol RFC §3). */\n readonly writer: string;\n readonly transport: MeshTransportFactory;\n /** Per-path merge policies for rebase/convergence (`lww` default). */\n readonly policies?: readonly MergePolicyEntry[];\n /** Emit-side validation, symmetric with the relay's (tripwire honesty). */\n readonly policy?: OpPolicy;\n /** kind/claims of this principal, so a shared policy evaluates identically on both sides. */\n readonly ctx?: Omit<PrincipalCtx, 'writer'>;\n readonly policyVersion?: number;\n /** Exponential backoff cap for reconnects (default 15s; base 500ms + jitter). */\n readonly reconnect?: { readonly maxDelayMs?: number };\n readonly injector?: Injector;\n /** Register with the nearest transition scope so (re)connection surfaces as `pending`. */\n readonly register?: 'track' | 'suspend';\n readonly onEject?: (reason: string) => void;\n};\n\nexport type MeshSyncRef = {\n readonly status: Signal<MeshStatus>;\n readonly peers: Signal<readonly MeshPeer[]>;\n /** Publish this client's ephemeral presence payload (cursor, section, activity…). */\n setPresence(data: unknown): void;\n close(): void;\n};\n\nconst RECONNECT_BASE_MS = 500;\n\n/**\n * Replicates a signal store across clients through a relay room: local writes emit stamped\n * envelopes, remote envelopes fold in convergently, reconnects resume via delta or snapshot\n * with unacknowledged local writes rebased on top, and presence rides an ephemeral channel.\n * A synced store reads exactly like a local one — connection state surfaces only through\n * `status` and the transition scope (op-protocol RFC §10).\n */\nexport function meshSync<T extends object>(\n source: WritableSignal<T>,\n opt: MeshSyncOptions,\n): MeshSyncRef {\n const injector = opt.injector ?? inject(Injector);\n const status = signal<MeshStatus>('connecting');\n const peerMap = signal<ReadonlyMap<string, MeshPeer>>(new Map());\n const peers = computed(() => [...peerMap().values()]);\n const policyVersion = opt.policyVersion ?? 0;\n\n const sync = opSync(source, {\n writer: opt.writer,\n policies: opt.policies,\n policyVersion,\n injector,\n });\n\n const unacked = new Map<number, OpEnvelope>();\n let highestAcked = 0;\n let lastSeq = 0;\n let epoch: string | undefined;\n let presenceData: unknown;\n let hasPresence = false;\n let transport: MeshTransport | null = null;\n let attempts = 0;\n let reconnectTimer: ReturnType<typeof setTimeout> | undefined;\n let unsubs: (() => void)[] = [];\n let closed = false;\n\n const terminal = (state: 'ejected' | 'closed', reason?: string): void => {\n closed = true;\n if (reconnectTimer !== undefined) clearTimeout(reconnectTimer);\n for (const unsub of unsubs.splice(0)) unsub();\n unsubLocal();\n unacked.clear();\n transport?.close();\n transport = null;\n peerMap.set(new Map());\n status.set(state);\n if (reason !== undefined) opt.onEject?.(reason);\n };\n\n const sendEnv = (env: OpEnvelope): void => {\n transport?.send({ t: 'env', room: opt.room, env });\n };\n\n const flushUnacked = (): void => {\n for (const env of [...unacked.values()].sort((a, b) => a.version - b.version)) {\n sendEnv(env);\n }\n };\n\n const handle = (msg: ServerMsg): void => {\n if (msg.room !== opt.room) return;\n switch (msg.t) {\n case 'welcome': {\n if (epoch !== undefined && msg.epoch !== epoch) lastSeq = 0;\n epoch = msg.epoch;\n peerMap.set(new Map(msg.peers.map((p) => [p.origin, p])));\n if (msg.mode === 'delta') {\n for (const env of msg.envs) applyRemote(env);\n } else if (msg.mode === 'snapshot') {\n sync.hydrate(msg.root as T, { [sync.origin]: highestAcked });\n lastSeq = msg.seq;\n } else if (msg.seq === 0 && lastSeq === 0) {\n sync.seed();\n }\n lastSeq = Math.max(lastSeq, msg.seq);\n attempts = 0;\n status.set('live');\n flushUnacked();\n if (hasPresence) transport?.send({ t: 'presence', room: opt.room, data: presenceData });\n return;\n }\n case 'env':\n applyRemote(msg.env);\n return;\n case 'presence': {\n const next = new Map(peerMap());\n if (msg.gone) next.delete(msg.peer.origin);\n else next.set(msg.peer.origin, msg.peer);\n peerMap.set(next);\n return;\n }\n case 'eject':\n if (msg.writer === opt.writer) terminal('ejected', msg.reason);\n return;\n case 'reject':\n terminal('ejected', msg.reason);\n return;\n }\n };\n\n const applyRemote = (env: SeqEnvelope): void => {\n lastSeq = Math.max(lastSeq, env.seq);\n if (env.origin === sync.origin) {\n unacked.delete(env.version);\n highestAcked = Math.max(highestAcked, env.version);\n return;\n }\n sync.receive(env);\n };\n\n const connect = (): void => {\n if (closed) return;\n for (const unsub of unsubs.splice(0)) unsub();\n const t = opt.transport();\n transport = t;\n unsubs = [\n t.onMessage(handle),\n t.onClose(() => {\n if (closed || transport !== t) return;\n transport = null;\n status.set('reconnecting');\n const delay = Math.min(\n opt.reconnect?.maxDelayMs ?? 15_000,\n RECONNECT_BASE_MS * 2 ** attempts++,\n );\n reconnectTimer = setTimeout(() => {\n reconnectTimer = undefined;\n connect();\n }, delay + Math.random() * 100);\n }),\n ];\n t.send({\n t: 'hello',\n room: opt.room,\n origin: sync.origin,\n proto: MESH_PROTO_VERSION,\n policyVersion,\n seq: lastSeq > 0 ? lastSeq : undefined,\n });\n };\n\n const unsubLocal = sync.subscribe((env) => {\n const violation = checkEnvelope(opt.policy, env, { ...opt.ctx, writer: opt.writer });\n if (violation) {\n if (isDevMode()) {\n console.warn('[@mmstack/mesh] local write violates the room policy — not sent', violation);\n }\n return;\n }\n unacked.set(env.version, env);\n if (status() === 'live') sendEnv(env);\n });\n\n if (opt.register) {\n const connection: ResourceLike = {\n status: computed(() => {\n const s = status();\n return s === 'connecting'\n ? 'loading'\n : s === 'reconnecting'\n ? 'reloading'\n : s === 'ejected'\n ? 'error'\n : 'resolved';\n }),\n isLoading: computed(\n () => status() === 'connecting' || status() === 'reconnecting',\n ),\n hasValue: () => true,\n };\n runInInjectionContext(injector, () =>\n registerResource(connection, { suspends: opt.register === 'suspend' }),\n );\n }\n\n injector.get(DestroyRef).onDestroy(() => {\n if (!closed) terminal('closed');\n unsubLocal();\n sync.destroy();\n });\n\n connect();\n\n return {\n status: status.asReadonly(),\n peers,\n setPresence: (data) => {\n presenceData = data;\n hasPresence = true;\n if (status() === 'live') {\n transport?.send({ t: 'presence', room: opt.room, data });\n }\n },\n close: () => {\n if (!closed) terminal('closed');\n unsubLocal();\n sync.destroy();\n },\n };\n}\n","import { isDevMode } from '@angular/core';\nimport type {\n ClientMsg,\n PrincipalCtx,\n Relay,\n ServerMsg,\n} from '@mmstack/mesh-protocol';\n\n/**\n * One live connection to a relay. `meshSync` calls the factory per (re)connect; a transport\n * represents a single connection and never reconnects itself.\n */\nexport type MeshTransport = {\n send(msg: ClientMsg): void;\n onMessage(cb: (msg: ServerMsg) => void): () => void;\n onClose(cb: () => void): () => void;\n close(): void;\n};\n\nexport type MeshTransportFactory = () => MeshTransport;\n\n/**\n * JSON-over-WebSocket transport. Messages sent before the socket opens are buffered and\n * flushed on open; malformed inbound frames are dropped.\n */\nexport function webSocketTransport(\n url: string,\n protocols?: string | string[],\n): MeshTransportFactory {\n return () => {\n const ws = new WebSocket(url, protocols);\n const messageCbs = new Set<(msg: ServerMsg) => void>();\n const closeCbs = new Set<() => void>();\n const pending: string[] = [];\n\n ws.onopen = () => {\n for (const frame of pending.splice(0)) ws.send(frame);\n };\n ws.onmessage = (event) => {\n let msg: ServerMsg;\n try {\n msg = JSON.parse(String(event.data)) as ServerMsg;\n } catch {\n if (isDevMode()) {\n console.warn('[@mmstack/mesh] dropped unparseable frame (expected JSON text)');\n }\n return;\n }\n for (const cb of [...messageCbs]) cb(msg);\n };\n ws.onclose = () => {\n for (const cb of [...closeCbs]) cb();\n };\n\n return {\n send: (msg) => {\n const frame = JSON.stringify(msg);\n if (ws.readyState === WebSocket.OPEN) ws.send(frame);\n else if (ws.readyState === WebSocket.CONNECTING) pending.push(frame);\n },\n onMessage: (cb) => {\n messageCbs.add(cb);\n return () => messageCbs.delete(cb);\n },\n onClose: (cb) => {\n closeCbs.add(cb);\n return () => closeCbs.delete(cb);\n },\n close: () => ws.close(),\n };\n };\n}\n\n/**\n * Connects directly to an in-process `createRelay` — no network. The backbone of full-loop\n * tests, and handy for single-process demos or an SSR-side room.\n */\nexport function directTransport(\n relay: Relay,\n ctx: PrincipalCtx,\n): MeshTransportFactory {\n return () => {\n const messageCbs = new Set<(msg: ServerMsg) => void>();\n const closeCbs = new Set<() => void>();\n let open = true;\n\n const connection = relay.connect(\n {\n send: (msg) => {\n if (!open) return;\n for (const cb of [...messageCbs]) cb(msg);\n },\n close: () => {\n if (!open) return;\n open = false;\n connection.disconnect();\n for (const cb of [...closeCbs]) cb();\n },\n },\n ctx,\n );\n\n return {\n send: (msg) => {\n if (open) connection.receive(msg);\n },\n onMessage: (cb) => {\n messageCbs.add(cb);\n return () => messageCbs.delete(cb);\n },\n onClose: (cb) => {\n closeCbs.add(cb);\n return () => closeCbs.delete(cb);\n },\n close: () => {\n if (!open) return;\n open = false;\n connection.disconnect();\n for (const cb of [...closeCbs]) cb();\n },\n };\n };\n}\n","import {\n computed,\n DestroyRef,\n inject,\n Injector,\n signal,\n type Signal,\n type WritableSignal,\n} from '@angular/core';\nimport { MESH_PROTO_VERSION, type ServerMsg } from '@mmstack/mesh-protocol';\nimport {\n opSync,\n type MergePolicyEntry,\n type OpEnvelope,\n} from '@mmstack/primitives';\nimport type { MeshTransport, MeshTransportFactory } from './transport';\n\n/** A data channel as the P2P engine needs it; opens later, buffers nothing itself. */\nexport type DataChannelLike = {\n send(frame: string): void;\n onMessage(cb: (frame: string) => void): () => void;\n onOpen(cb: () => void): () => void;\n onClose(cb: () => void): () => void;\n close(): void;\n};\n\n/**\n * Creates one peer link. `polite` assigns the perfect-negotiation role (derived\n * deterministically from origin ordering); `sendSignal` routes offer/answer/ICE payloads\n * through the relay; `signal` delivers the remote side's payloads.\n */\nexport type PeerConnector = (opt: {\n readonly remote: string;\n readonly polite: boolean;\n readonly sendSignal: (data: unknown) => void;\n}) => {\n readonly channel: DataChannelLike;\n signal(data: unknown): void;\n close(): void;\n};\n\n/** WebRTC `PeerConnector` implementing the perfect-negotiation pattern. Browser-only. */\nexport function rtcPeerConnector(config?: RTCConfiguration): PeerConnector {\n return ({ polite, sendSignal }) => {\n const pc = new RTCPeerConnection(config);\n const messageCbs = new Set<(frame: string) => void>();\n const openCbs = new Set<() => void>();\n const closeCbs = new Set<() => void>();\n const pending: string[] = [];\n let dc: RTCDataChannel | null = null;\n let makingOffer = false;\n let ignoreOffer = false;\n\n const attach = (channel: RTCDataChannel): void => {\n dc = channel;\n channel.onmessage = (e) => {\n for (const cb of [...messageCbs]) cb(String(e.data));\n };\n channel.onopen = () => {\n for (const frame of pending.splice(0)) channel.send(frame);\n for (const cb of [...openCbs]) cb();\n };\n channel.onclose = () => {\n for (const cb of [...closeCbs]) cb();\n };\n };\n\n if (!polite) attach(pc.createDataChannel('mmstack-mesh'));\n else pc.ondatachannel = (e) => attach(e.channel);\n\n pc.onicecandidate = (e) => {\n if (e.candidate) sendSignal({ ice: e.candidate.toJSON() });\n };\n pc.onnegotiationneeded = async () => {\n try {\n makingOffer = true;\n await pc.setLocalDescription();\n sendSignal({ description: pc.localDescription });\n } finally {\n makingOffer = false;\n }\n };\n\n return {\n channel: {\n send: (frame) => {\n if (dc && dc.readyState === 'open') dc.send(frame);\n else pending.push(frame);\n },\n onMessage: (cb) => (messageCbs.add(cb), () => messageCbs.delete(cb)),\n onOpen: (cb) => (openCbs.add(cb), () => openCbs.delete(cb)),\n onClose: (cb) => (closeCbs.add(cb), () => closeCbs.delete(cb)),\n close: () => dc?.close(),\n },\n signal: async (data) => {\n const { description, ice } = (data ?? {}) as {\n description?: RTCSessionDescriptionInit;\n ice?: RTCIceCandidateInit;\n };\n if (description) {\n const collision =\n description.type === 'offer' &&\n (makingOffer || pc.signalingState !== 'stable');\n ignoreOffer = !polite && collision;\n if (ignoreOffer) return;\n await pc.setRemoteDescription(description);\n if (description.type === 'offer') {\n await pc.setLocalDescription();\n sendSignal({ description: pc.localDescription });\n }\n } else if (ice) {\n try {\n await pc.addIceCandidate(ice);\n } catch (err) {\n if (!ignoreOffer) throw err;\n }\n }\n },\n close: () => {\n dc?.close();\n pc.close();\n },\n };\n };\n}\n\ntype P2PMsg =\n | { t: 'hello'; wm: Record<string, number> }\n | { t: 'state'; root: unknown; wm: Record<string, number> }\n | { t: 'uptodate' }\n | { t: 'env'; env: OpEnvelope };\n\nexport type WebRtcMeshOptions = {\n readonly room: string;\n readonly writer: string;\n /** Signaling path to the relay (ws or direct) — data flows peer-to-peer. */\n readonly signaling: MeshTransportFactory;\n /** Peer link factory; defaults to {@link rtcPeerConnector}. Injectable for tests. */\n readonly connector?: PeerConnector;\n readonly policies?: readonly MergePolicyEntry[];\n readonly policyVersion?: number;\n readonly injector?: Injector;\n};\n\nexport type WebRtcMeshRef = {\n readonly status: Signal<'connecting' | 'live'>;\n /** Origins with an OPEN data channel. */\n readonly peers: Signal<readonly string[]>;\n close(): void;\n};\n\ntype Peer = {\n link: ReturnType<PeerConnector>;\n open: boolean;\n unsubs: (() => void)[];\n};\n\n/**\n * Peer-to-peer mesh sync (unsequenced topology, op-protocol RFC §4): the relay only signals\n * and tracks membership; envelopes flow over WebRTC data channels and converge via the\n * per-path register map. Catch-up is pairwise: on channel open both sides exchange\n * watermarks; a side whose state is strictly covered hydrates from the other. Two peers that\n * diverged while BOTH held state keep their convergent go-forward guarantees but do not\n * retroactively merge history (same contract as the tab rung).\n */\nexport function webRtcMesh<T extends object>(\n source: WritableSignal<T>,\n opt: WebRtcMeshOptions,\n): WebRtcMeshRef {\n const injector = opt.injector ?? inject(Injector);\n const connector = opt.connector ?? rtcPeerConnector();\n const status = signal<'connecting' | 'live'>('connecting');\n const openPeers = signal<ReadonlySet<string>>(new Set());\n const peers = new Map<string, Peer>();\n let signalingUnsubs: (() => void)[] = [];\n let transport: MeshTransport | null = null;\n let reconnectTimer: ReturnType<typeof setTimeout> | undefined;\n let closed = false;\n\n const sync = opSync(source, {\n writer: opt.writer,\n policies: opt.policies,\n policyVersion: opt.policyVersion,\n injector,\n });\n\n const covered = (\n mine: Record<string, number>,\n theirs: Record<string, number>,\n ): boolean => Object.entries(mine).every(([o, v]) => (theirs[o] ?? 0) >= v);\n\n const sendTo = (peer: Peer, msg: P2PMsg): void => {\n peer.link.channel.send(JSON.stringify(msg));\n };\n\n const broadcast = (msg: P2PMsg): void => {\n for (const peer of peers.values()) {\n if (peer.open) sendTo(peer, msg);\n }\n };\n\n const dropPeer = (origin: string): void => {\n const peer = peers.get(origin);\n if (!peer) return;\n peers.delete(origin);\n for (const unsub of peer.unsubs) unsub();\n peer.link.close();\n openPeers.update((set) => {\n const next = new Set(set);\n next.delete(origin);\n return next;\n });\n };\n\n const ensurePeer = (origin: string): Peer => {\n let peer = peers.get(origin);\n if (peer) return peer;\n const link = connector({\n remote: origin,\n polite: sync.origin > origin,\n sendSignal: (data) =>\n transport?.send({ t: 'signal', room: opt.room, to: origin, data }),\n });\n peer = { link, open: false, unsubs: [] };\n peers.set(origin, peer);\n peer.unsubs.push(\n link.channel.onOpen(() => {\n peer!.open = true;\n openPeers.update((set) => new Set(set).add(origin));\n sendTo(peer!, { t: 'hello', wm: sync.watermark() });\n }),\n link.channel.onMessage((frame) => {\n let msg: P2PMsg;\n try {\n msg = JSON.parse(frame) as P2PMsg;\n } catch {\n return;\n }\n handlePeerMsg(peer!, msg);\n }),\n link.channel.onClose(() => dropPeer(origin)),\n );\n return peer;\n };\n\n const handlePeerMsg = (peer: Peer, msg: P2PMsg): void => {\n switch (msg.t) {\n case 'hello': {\n const snap = sync.snapshot();\n if (covered(snap.wm, msg.wm)) sendTo(peer, { t: 'uptodate' });\n else sendTo(peer, { t: 'state', root: snap.root, wm: snap.wm });\n return;\n }\n case 'state': {\n if (covered(sync.watermark(), msg.wm)) {\n sync.hydrate(msg.root as T, msg.wm);\n }\n return;\n }\n case 'uptodate':\n return;\n case 'env':\n sync.receive(msg.env);\n return;\n }\n };\n\n const handleSignaling = (msg: ServerMsg): void => {\n if (msg.room !== opt.room) return;\n switch (msg.t) {\n case 'welcome':\n for (const origin of msg.members) ensurePeer(origin);\n status.set('live');\n return;\n case 'member':\n if (msg.gone) dropPeer(msg.origin);\n else ensurePeer(msg.origin);\n return;\n case 'signal':\n ensurePeer(msg.from).link.signal(msg.data);\n return;\n case 'reject':\n close();\n return;\n }\n };\n\n const connectSignaling = (): void => {\n if (closed) return;\n for (const unsub of signalingUnsubs.splice(0)) unsub();\n const t = opt.signaling();\n transport = t;\n signalingUnsubs = [\n t.onMessage(handleSignaling),\n t.onClose(() => {\n if (closed || transport !== t) return;\n transport = null;\n reconnectTimer = setTimeout(connectSignaling, 1000);\n }),\n ];\n t.send({\n t: 'hello',\n room: opt.room,\n origin: sync.origin,\n proto: MESH_PROTO_VERSION,\n policyVersion: opt.policyVersion ?? 0,\n });\n };\n\n const unsubLocal = sync.subscribe((env) => broadcast({ t: 'env', env }));\n\n const close = (): void => {\n if (closed) return;\n closed = true;\n if (reconnectTimer !== undefined) clearTimeout(reconnectTimer);\n unsubLocal();\n for (const origin of [...peers.keys()]) dropPeer(origin);\n for (const unsub of signalingUnsubs.splice(0)) unsub();\n transport?.close();\n transport = null;\n sync.destroy();\n status.set('connecting');\n };\n\n injector.get(DestroyRef).onDestroy(close);\n connectSignaling();\n\n return {\n status: status.asReadonly(),\n peers: computed(() => [...openPeers()]),\n close,\n };\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@mmstack/mesh",
3
+ "version": "21.0.0",
4
+ "keywords": [
5
+ "angular",
6
+ "signals",
7
+ "mesh",
8
+ "sync",
9
+ "multiplayer",
10
+ "collaboration",
11
+ "local-first",
12
+ "presence"
13
+ ],
14
+ "license": "MIT",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/mihajm/mmstack.git",
18
+ "directory": "packages/mesh/client"
19
+ },
20
+ "homepage": "https://github.com/mihajm/mmstack/blob/master/packages/mesh/client",
21
+ "peerDependencies": {
22
+ "@angular/core": ">=21 <22",
23
+ "@mmstack/primitives": ">=21.6 <22",
24
+ "@mmstack/mesh-protocol": ">=0.1.0 <1"
25
+ },
26
+ "sideEffects": false,
27
+ "module": "fesm2022/mmstack-mesh.mjs",
28
+ "typings": "types/mmstack-mesh.d.ts",
29
+ "exports": {
30
+ "./package.json": {
31
+ "default": "./package.json"
32
+ },
33
+ ".": {
34
+ "types": "./types/mmstack-mesh.d.ts",
35
+ "default": "./fesm2022/mmstack-mesh.mjs"
36
+ }
37
+ },
38
+ "type": "module",
39
+ "dependencies": {
40
+ "tslib": "^2.3.0"
41
+ }
42
+ }
@@ -0,0 +1,118 @@
1
+ import { Injector, Signal, WritableSignal } from '@angular/core';
2
+ import { ClientMsg, ServerMsg, Relay, PrincipalCtx, PresenceState, OpPolicy } from '@mmstack/mesh-protocol';
3
+ import { MergePolicyEntry } from '@mmstack/primitives';
4
+
5
+ /**
6
+ * One live connection to a relay. `meshSync` calls the factory per (re)connect; a transport
7
+ * represents a single connection and never reconnects itself.
8
+ */
9
+ type MeshTransport = {
10
+ send(msg: ClientMsg): void;
11
+ onMessage(cb: (msg: ServerMsg) => void): () => void;
12
+ onClose(cb: () => void): () => void;
13
+ close(): void;
14
+ };
15
+ type MeshTransportFactory = () => MeshTransport;
16
+ /**
17
+ * JSON-over-WebSocket transport. Messages sent before the socket opens are buffered and
18
+ * flushed on open; malformed inbound frames are dropped.
19
+ */
20
+ declare function webSocketTransport(url: string, protocols?: string | string[]): MeshTransportFactory;
21
+ /**
22
+ * Connects directly to an in-process `createRelay` — no network. The backbone of full-loop
23
+ * tests, and handy for single-process demos or an SSR-side room.
24
+ */
25
+ declare function directTransport(relay: Relay, ctx: PrincipalCtx): MeshTransportFactory;
26
+
27
+ type MeshStatus = 'connecting' | 'live' | 'reconnecting' | 'ejected' | 'closed';
28
+ type MeshPeer = PresenceState;
29
+ type MeshSyncOptions = {
30
+ readonly room: string;
31
+ /** Opaque principal pseudonym — provided, never minted (op-protocol RFC §3). */
32
+ readonly writer: string;
33
+ readonly transport: MeshTransportFactory;
34
+ /** Per-path merge policies for rebase/convergence (`lww` default). */
35
+ readonly policies?: readonly MergePolicyEntry[];
36
+ /** Emit-side validation, symmetric with the relay's (tripwire honesty). */
37
+ readonly policy?: OpPolicy;
38
+ /** kind/claims of this principal, so a shared policy evaluates identically on both sides. */
39
+ readonly ctx?: Omit<PrincipalCtx, 'writer'>;
40
+ readonly policyVersion?: number;
41
+ /** Exponential backoff cap for reconnects (default 15s; base 500ms + jitter). */
42
+ readonly reconnect?: {
43
+ readonly maxDelayMs?: number;
44
+ };
45
+ readonly injector?: Injector;
46
+ /** Register with the nearest transition scope so (re)connection surfaces as `pending`. */
47
+ readonly register?: 'track' | 'suspend';
48
+ readonly onEject?: (reason: string) => void;
49
+ };
50
+ type MeshSyncRef = {
51
+ readonly status: Signal<MeshStatus>;
52
+ readonly peers: Signal<readonly MeshPeer[]>;
53
+ /** Publish this client's ephemeral presence payload (cursor, section, activity…). */
54
+ setPresence(data: unknown): void;
55
+ close(): void;
56
+ };
57
+ /**
58
+ * Replicates a signal store across clients through a relay room: local writes emit stamped
59
+ * envelopes, remote envelopes fold in convergently, reconnects resume via delta or snapshot
60
+ * with unacknowledged local writes rebased on top, and presence rides an ephemeral channel.
61
+ * A synced store reads exactly like a local one — connection state surfaces only through
62
+ * `status` and the transition scope (op-protocol RFC §10).
63
+ */
64
+ declare function meshSync<T extends object>(source: WritableSignal<T>, opt: MeshSyncOptions): MeshSyncRef;
65
+
66
+ /** A data channel as the P2P engine needs it; opens later, buffers nothing itself. */
67
+ type DataChannelLike = {
68
+ send(frame: string): void;
69
+ onMessage(cb: (frame: string) => void): () => void;
70
+ onOpen(cb: () => void): () => void;
71
+ onClose(cb: () => void): () => void;
72
+ close(): void;
73
+ };
74
+ /**
75
+ * Creates one peer link. `polite` assigns the perfect-negotiation role (derived
76
+ * deterministically from origin ordering); `sendSignal` routes offer/answer/ICE payloads
77
+ * through the relay; `signal` delivers the remote side's payloads.
78
+ */
79
+ type PeerConnector = (opt: {
80
+ readonly remote: string;
81
+ readonly polite: boolean;
82
+ readonly sendSignal: (data: unknown) => void;
83
+ }) => {
84
+ readonly channel: DataChannelLike;
85
+ signal(data: unknown): void;
86
+ close(): void;
87
+ };
88
+ /** WebRTC `PeerConnector` implementing the perfect-negotiation pattern. Browser-only. */
89
+ declare function rtcPeerConnector(config?: RTCConfiguration): PeerConnector;
90
+ type WebRtcMeshOptions = {
91
+ readonly room: string;
92
+ readonly writer: string;
93
+ /** Signaling path to the relay (ws or direct) — data flows peer-to-peer. */
94
+ readonly signaling: MeshTransportFactory;
95
+ /** Peer link factory; defaults to {@link rtcPeerConnector}. Injectable for tests. */
96
+ readonly connector?: PeerConnector;
97
+ readonly policies?: readonly MergePolicyEntry[];
98
+ readonly policyVersion?: number;
99
+ readonly injector?: Injector;
100
+ };
101
+ type WebRtcMeshRef = {
102
+ readonly status: Signal<'connecting' | 'live'>;
103
+ /** Origins with an OPEN data channel. */
104
+ readonly peers: Signal<readonly string[]>;
105
+ close(): void;
106
+ };
107
+ /**
108
+ * Peer-to-peer mesh sync (unsequenced topology, op-protocol RFC §4): the relay only signals
109
+ * and tracks membership; envelopes flow over WebRTC data channels and converge via the
110
+ * per-path register map. Catch-up is pairwise: on channel open both sides exchange
111
+ * watermarks; a side whose state is strictly covered hydrates from the other. Two peers that
112
+ * diverged while BOTH held state keep their convergent go-forward guarantees but do not
113
+ * retroactively merge history (same contract as the tab rung).
114
+ */
115
+ declare function webRtcMesh<T extends object>(source: WritableSignal<T>, opt: WebRtcMeshOptions): WebRtcMeshRef;
116
+
117
+ export { directTransport, meshSync, rtcPeerConnector, webRtcMesh, webSocketTransport };
118
+ export type { DataChannelLike, MeshPeer, MeshStatus, MeshSyncOptions, MeshSyncRef, MeshTransport, MeshTransportFactory, PeerConnector, WebRtcMeshOptions, WebRtcMeshRef };