@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 +124 -0
- package/fesm2022/mmstack-mesh.mjs +570 -0
- package/fesm2022/mmstack-mesh.mjs.map +1 -0
- package/package.json +42 -0
- package/types/mmstack-mesh.d.ts +118 -0
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":";;;;AAkEA,MAAM,iBAAiB,GAAG,GAAG;AAE7B;;;;;;AAMG;AACG,SAAU,QAAQ,CACtB,MAAyB,EACzB,GAAoB,EAAA;IAEpB,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC;AACjD,IAAA,MAAM,MAAM,GAAG,MAAM,CAAa,YAAY,6EAAC;IAC/C,MAAM,OAAO,GAAG,MAAM,CAAgC,IAAI,GAAG,EAAE,8EAAC;AAChE,IAAA,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,GAAG,OAAO,EAAE,CAAC,MAAM,EAAE,CAAC,4EAAC;AACrD,IAAA,MAAM,aAAa,GAAG,GAAG,CAAC,aAAa,IAAI,CAAC;AAE5C,IAAA,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE;QAC1B,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,QAAQ,EAAE,GAAG,CAAC,QAAQ;QACtB,aAAa;QACb,QAAQ;AACT,KAAA,CAAC;AAEF,IAAA,MAAM,OAAO,GAAG,IAAI,GAAG,EAAsB;IAC7C,IAAI,YAAY,GAAG,CAAC;IACpB,IAAI,OAAO,GAAG,CAAC;AACf,IAAA,IAAI,KAAyB;AAC7B,IAAA,IAAI,YAAqB;IACzB,IAAI,WAAW,GAAG,KAAK;IACvB,IAAI,SAAS,GAAyB,IAAI;IAC1C,IAAI,QAAQ,GAAG,CAAC;AAChB,IAAA,IAAI,cAAyD;IAC7D,IAAI,MAAM,GAAmB,EAAE;IAC/B,IAAI,MAAM,GAAG,KAAK;AAElB,IAAA,MAAM,QAAQ,GAAG,CAAC,KAA2B,EAAE,MAAe,KAAU;QACtE,MAAM,GAAG,IAAI;QACb,IAAI,cAAc,KAAK,SAAS;YAAE,YAAY,CAAC,cAAc,CAAC;QAC9D,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;AAAE,YAAA,KAAK,EAAE;AAC7C,QAAA,UAAU,EAAE;QACZ,OAAO,CAAC,KAAK,EAAE;QACf,SAAS,EAAE,KAAK,EAAE;QAClB,SAAS,GAAG,IAAI;AAChB,QAAA,OAAO,CAAC,GAAG,CAAC,IAAI,GAAG,EAAE,CAAC;AACtB,QAAA,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC;QACjB,IAAI,MAAM,KAAK,SAAS;AAAE,YAAA,GAAG,CAAC,OAAO,GAAG,MAAM,CAAC;AACjD,IAAA,CAAC;AAED,IAAA,MAAM,OAAO,GAAG,CAAC,GAAe,KAAU;AACxC,QAAA,SAAS,EAAE,IAAI,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC;AACpD,IAAA,CAAC;IAED,MAAM,YAAY,GAAG,MAAW;AAC9B,QAAA,KAAK,MAAM,GAAG,IAAI,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC,EAAE;YAC7E,OAAO,CAAC,GAAG,CAAC;QACd;AACF,IAAA,CAAC;AAED,IAAA,MAAM,MAAM,GAAG,CAAC,GAAc,KAAU;AACtC,QAAA,IAAI,GAAG,CAAC,IAAI,KAAK,GAAG,CAAC,IAAI;YAAE;AAC3B,QAAA,QAAQ,GAAG,CAAC,CAAC;YACX,KAAK,SAAS,EAAE;gBACd,IAAI,KAAK,KAAK,SAAS,IAAI,GAAG,CAAC,KAAK,KAAK,KAAK;oBAAE,OAAO,GAAG,CAAC;AAC3D,gBAAA,KAAK,GAAG,GAAG,CAAC,KAAK;gBACjB,OAAO,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;AACzD,gBAAA,IAAI,GAAG,CAAC,IAAI,KAAK,OAAO,EAAE;AACxB,oBAAA,KAAK,MAAM,GAAG,IAAI,GAAG,CAAC,IAAI;wBAAE,WAAW,CAAC,GAAG,CAAC;gBAC9C;AAAO,qBAAA,IAAI,GAAG,CAAC,IAAI,KAAK,UAAU,EAAE;AAClC,oBAAA,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAS,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,GAAG,YAAY,EAAE,CAAC;AAC5D,oBAAA,OAAO,GAAG,GAAG,CAAC,GAAG;gBACnB;qBAAO,IAAI,GAAG,CAAC,GAAG,KAAK,CAAC,IAAI,OAAO,KAAK,CAAC,EAAE;oBACzC,IAAI,CAAC,IAAI,EAAE;gBACb;gBACA,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,GAAG,CAAC;gBACpC,QAAQ,GAAG,CAAC;AACZ,gBAAA,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC;AAClB,gBAAA,YAAY,EAAE;AACd,gBAAA,IAAI,WAAW;AAAE,oBAAA,SAAS,EAAE,IAAI,CAAC,EAAE,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC;gBACvF;YACF;AACA,YAAA,KAAK,KAAK;AACR,gBAAA,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC;gBACpB;YACF,KAAK,UAAU,EAAE;gBACf,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,OAAO,EAAE,CAAC;gBAC/B,IAAI,GAAG,CAAC,IAAI;oBAAE,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC;;AACrC,oBAAA,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,IAAI,CAAC;AACxC,gBAAA,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC;gBACjB;YACF;AACA,YAAA,KAAK,OAAO;AACV,gBAAA,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,CAAC,MAAM;AAAE,oBAAA,QAAQ,CAAC,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC;gBAC9D;AACF,YAAA,KAAK,QAAQ;AACX,gBAAA,QAAQ,CAAC,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC;gBAC/B;;AAEN,IAAA,CAAC;AAED,IAAA,MAAM,WAAW,GAAG,CAAC,GAAgB,KAAU;QAC7C,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,GAAG,CAAC;QACpC,IAAI,GAAG,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM,EAAE;AAC9B,YAAA,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC;YAC3B,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,YAAY,EAAE,GAAG,CAAC,OAAO,CAAC;YAClD;QACF;AACA,QAAA,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC;AACnB,IAAA,CAAC;IAED,MAAM,OAAO,GAAG,MAAW;AACzB,QAAA,IAAI,MAAM;YAAE;QACZ,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;AAAE,YAAA,KAAK,EAAE;AAC7C,QAAA,MAAM,CAAC,GAAG,GAAG,CAAC,SAAS,EAAE;QACzB,SAAS,GAAG,CAAC;AACb,QAAA,MAAM,GAAG;AACP,YAAA,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC;AACnB,YAAA,CAAC,CAAC,OAAO,CAAC,MAAK;AACb,gBAAA,IAAI,MAAM,IAAI,SAAS,KAAK,CAAC;oBAAE;gBAC/B,SAAS,GAAG,IAAI;AAChB,gBAAA,MAAM,CAAC,GAAG,CAAC,cAAc,CAAC;gBAC1B,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CACpB,GAAG,CAAC,SAAS,EAAE,UAAU,IAAI,MAAM,EACnC,iBAAiB,GAAG,CAAC,IAAI,QAAQ,EAAE,CACpC;AACD,gBAAA,cAAc,GAAG,UAAU,CAAC,MAAK;oBAC/B,cAAc,GAAG,SAAS;AAC1B,oBAAA,OAAO,EAAE;gBACX,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,GAAG,CAAC;AACjC,YAAA,CAAC,CAAC;SACH;QACD,CAAC,CAAC,IAAI,CAAC;AACL,YAAA,CAAC,EAAE,OAAO;YACV,IAAI,EAAE,GAAG,CAAC,IAAI;YACd,MAAM,EAAE,IAAI,CAAC,MAAM;AACnB,YAAA,KAAK,EAAE,kBAAkB;YACzB,aAAa;YACb,GAAG,EAAE,OAAO,GAAG,CAAC,GAAG,OAAO,GAAG,SAAS;AACvC,SAAA,CAAC;AACJ,IAAA,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,GAAG,KAAI;QACxC,MAAM,SAAS,GAAG,aAAa,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,EAAE,GAAG,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC;QACpF,IAAI,SAAS,EAAE;YACb,IAAI,SAAS,EAAE,EAAE;AACf,gBAAA,OAAO,CAAC,IAAI,CAAC,iEAAiE,EAAE,SAAS,CAAC;YAC5F;YACA;QACF;QACA,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC;QAC7B,IAAI,MAAM,EAAE,KAAK,MAAM;YAAE,OAAO,CAAC,GAAG,CAAC;AACvC,IAAA,CAAC,CAAC;AAEF,IAAA,IAAI,GAAG,CAAC,QAAQ,EAAE;AAChB,QAAA,MAAM,UAAU,GAAiB;AAC/B,YAAA,MAAM,EAAE,QAAQ,CAAC,MAAK;AACpB,gBAAA,MAAM,CAAC,GAAG,MAAM,EAAE;gBAClB,OAAO,CAAC,KAAK;AACX,sBAAE;sBACA,CAAC,KAAK;AACN,0BAAE;0BACA,CAAC,KAAK;AACN,8BAAE;8BACA,UAAU;AACpB,YAAA,CAAC,CAAC;AACF,YAAA,SAAS,EAAE,QAAQ,CACjB,MAAM,MAAM,EAAE,KAAK,YAAY,IAAI,MAAM,EAAE,KAAK,cAAc,CAC/D;AACD,YAAA,QAAQ,EAAE,MAAM,IAAI;SACrB;QACD,qBAAqB,CAAC,QAAQ,EAAE,MAC9B,gBAAgB,CAAC,UAAU,EAAE,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC,CACvE;IACH;IAEA,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,SAAS,CAAC,MAAK;AACtC,QAAA,IAAI,CAAC,MAAM;YAAE,QAAQ,CAAC,QAAQ,CAAC;AAC/B,QAAA,UAAU,EAAE;QACZ,IAAI,CAAC,OAAO,EAAE;AAChB,IAAA,CAAC,CAAC;AAEF,IAAA,OAAO,EAAE;IAET,OAAO;AACL,QAAA,MAAM,EAAE,MAAM,CAAC,UAAU,EAAE;QAC3B,KAAK;AACL,QAAA,WAAW,EAAE,CAAC,IAAI,KAAI;YACpB,YAAY,GAAG,IAAI;YACnB,WAAW,GAAG,IAAI;AAClB,YAAA,IAAI,MAAM,EAAE,KAAK,MAAM,EAAE;AACvB,gBAAA,SAAS,EAAE,IAAI,CAAC,EAAE,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC;YAC1D;QACF,CAAC;QACD,KAAK,EAAE,MAAK;AACV,YAAA,IAAI,CAAC,MAAM;gBAAE,QAAQ,CAAC,QAAQ,CAAC;AAC/B,YAAA,UAAU,EAAE;YACZ,IAAI,CAAC,OAAO,EAAE;QAChB,CAAC;KACF;AACH;;ACtPA;;;AAGG;AACG,SAAU,kBAAkB,CAChC,GAAW,EACX,SAA6B,EAAA;AAE7B,IAAA,OAAO,MAAK;QACV,MAAM,EAAE,GAAG,IAAI,SAAS,CAAC,GAAG,EAAE,SAAS,CAAC;AACxC,QAAA,MAAM,UAAU,GAAG,IAAI,GAAG,EAA4B;AACtD,QAAA,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAc;QACtC,MAAM,OAAO,GAAa,EAAE;AAE5B,QAAA,EAAE,CAAC,MAAM,GAAG,MAAK;YACf,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;AAAE,gBAAA,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC;AACvD,QAAA,CAAC;AACD,QAAA,EAAE,CAAC,SAAS,GAAG,CAAC,KAAK,KAAI;AACvB,YAAA,IAAI,GAAc;AAClB,YAAA,IAAI;AACF,gBAAA,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAc;YACnD;AAAE,YAAA,MAAM;gBACN,IAAI,SAAS,EAAE,EAAE;AACf,oBAAA,OAAO,CAAC,IAAI,CAAC,gEAAgE,CAAC;gBAChF;gBACA;YACF;AACA,YAAA,KAAK,MAAM,EAAE,IAAI,CAAC,GAAG,UAAU,CAAC;gBAAE,EAAE,CAAC,GAAG,CAAC;AAC3C,QAAA,CAAC;AACD,QAAA,EAAE,CAAC,OAAO,GAAG,MAAK;AAChB,YAAA,KAAK,MAAM,EAAE,IAAI,CAAC,GAAG,QAAQ,CAAC;AAAE,gBAAA,EAAE,EAAE;AACtC,QAAA,CAAC;QAED,OAAO;AACL,YAAA,IAAI,EAAE,CAAC,GAAG,KAAI;gBACZ,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC;AACjC,gBAAA,IAAI,EAAE,CAAC,UAAU,KAAK,SAAS,CAAC,IAAI;AAAE,oBAAA,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC;AAC/C,qBAAA,IAAI,EAAE,CAAC,UAAU,KAAK,SAAS,CAAC,UAAU;AAAE,oBAAA,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC;YACtE,CAAC;AACD,YAAA,SAAS,EAAE,CAAC,EAAE,KAAI;AAChB,gBAAA,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;gBAClB,OAAO,MAAM,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;YACpC,CAAC;AACD,YAAA,OAAO,EAAE,CAAC,EAAE,KAAI;AACd,gBAAA,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;gBAChB,OAAO,MAAM,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;YAClC,CAAC;AACD,YAAA,KAAK,EAAE,MAAM,EAAE,CAAC,KAAK,EAAE;SACxB;AACH,IAAA,CAAC;AACH;AAEA;;;AAGG;AACG,SAAU,eAAe,CAC7B,KAAY,EACZ,GAAiB,EAAA;AAEjB,IAAA,OAAO,MAAK;AACV,QAAA,MAAM,UAAU,GAAG,IAAI,GAAG,EAA4B;AACtD,QAAA,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAc;QACtC,IAAI,IAAI,GAAG,IAAI;AAEf,QAAA,MAAM,UAAU,GAAG,KAAK,CAAC,OAAO,CAC9B;AACE,YAAA,IAAI,EAAE,CAAC,GAAG,KAAI;AACZ,gBAAA,IAAI,CAAC,IAAI;oBAAE;AACX,gBAAA,KAAK,MAAM,EAAE,IAAI,CAAC,GAAG,UAAU,CAAC;oBAAE,EAAE,CAAC,GAAG,CAAC;YAC3C,CAAC;YACD,KAAK,EAAE,MAAK;AACV,gBAAA,IAAI,CAAC,IAAI;oBAAE;gBACX,IAAI,GAAG,KAAK;gBACZ,UAAU,CAAC,UAAU,EAAE;AACvB,gBAAA,KAAK,MAAM,EAAE,IAAI,CAAC,GAAG,QAAQ,CAAC;AAAE,oBAAA,EAAE,EAAE;YACtC,CAAC;SACF,EACD,GAAG,CACJ;QAED,OAAO;AACL,YAAA,IAAI,EAAE,CAAC,GAAG,KAAI;AACZ,gBAAA,IAAI,IAAI;AAAE,oBAAA,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC;YACnC,CAAC;AACD,YAAA,SAAS,EAAE,CAAC,EAAE,KAAI;AAChB,gBAAA,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;gBAClB,OAAO,MAAM,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;YACpC,CAAC;AACD,YAAA,OAAO,EAAE,CAAC,EAAE,KAAI;AACd,gBAAA,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;gBAChB,OAAO,MAAM,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;YAClC,CAAC;YACD,KAAK,EAAE,MAAK;AACV,gBAAA,IAAI,CAAC,IAAI;oBAAE;gBACX,IAAI,GAAG,KAAK;gBACZ,UAAU,CAAC,UAAU,EAAE;AACvB,gBAAA,KAAK,MAAM,EAAE,IAAI,CAAC,GAAG,QAAQ,CAAC;AAAE,oBAAA,EAAE,EAAE;YACtC,CAAC;SACF;AACH,IAAA,CAAC;AACH;;ACjFA;AACM,SAAU,gBAAgB,CAAC,MAAyB,EAAA;AACxD,IAAA,OAAO,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,KAAI;AAChC,QAAA,MAAM,EAAE,GAAG,IAAI,iBAAiB,CAAC,MAAM,CAAC;AACxC,QAAA,MAAM,UAAU,GAAG,IAAI,GAAG,EAA2B;AACrD,QAAA,MAAM,OAAO,GAAG,IAAI,GAAG,EAAc;AACrC,QAAA,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAc;QACtC,MAAM,OAAO,GAAa,EAAE;QAC5B,IAAI,EAAE,GAA0B,IAAI;QACpC,IAAI,WAAW,GAAG,KAAK;QACvB,IAAI,WAAW,GAAG,KAAK;AAEvB,QAAA,MAAM,MAAM,GAAG,CAAC,OAAuB,KAAU;YAC/C,EAAE,GAAG,OAAO;AACZ,YAAA,OAAO,CAAC,SAAS,GAAG,CAAC,CAAC,KAAI;AACxB,gBAAA,KAAK,MAAM,EAAE,IAAI,CAAC,GAAG,UAAU,CAAC;oBAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;AACtD,YAAA,CAAC;AACD,YAAA,OAAO,CAAC,MAAM,GAAG,MAAK;gBACpB,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;AAAE,oBAAA,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC;AAC1D,gBAAA,KAAK,MAAM,EAAE,IAAI,CAAC,GAAG,OAAO,CAAC;AAAE,oBAAA,EAAE,EAAE;AACrC,YAAA,CAAC;AACD,YAAA,OAAO,CAAC,OAAO,GAAG,MAAK;AACrB,gBAAA,KAAK,MAAM,EAAE,IAAI,CAAC,GAAG,QAAQ,CAAC;AAAE,oBAAA,EAAE,EAAE;AACtC,YAAA,CAAC;AACH,QAAA,CAAC;AAED,QAAA,IAAI,CAAC,MAAM;YAAE,MAAM,CAAC,EAAE,CAAC,iBAAiB,CAAC,cAAc,CAAC,CAAC;;AACpD,YAAA,EAAE,CAAC,aAAa,GAAG,CAAC,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC;AAEhD,QAAA,EAAE,CAAC,cAAc,GAAG,CAAC,CAAC,KAAI;YACxB,IAAI,CAAC,CAAC,SAAS;AAAE,gBAAA,UAAU,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,SAAS,CAAC,MAAM,EAAE,EAAE,CAAC;AAC5D,QAAA,CAAC;AACD,QAAA,EAAE,CAAC,mBAAmB,GAAG,YAAW;AAClC,YAAA,IAAI;gBACF,WAAW,GAAG,IAAI;AAClB,gBAAA,MAAM,EAAE,CAAC,mBAAmB,EAAE;gBAC9B,UAAU,CAAC,EAAE,WAAW,EAAE,EAAE,CAAC,gBAAgB,EAAE,CAAC;YAClD;oBAAU;gBACR,WAAW,GAAG,KAAK;YACrB;AACF,QAAA,CAAC;QAED,OAAO;AACL,YAAA,OAAO,EAAE;AACP,gBAAA,IAAI,EAAE,CAAC,KAAK,KAAI;AACd,oBAAA,IAAI,EAAE,IAAI,EAAE,CAAC,UAAU,KAAK,MAAM;AAAE,wBAAA,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC;;AAC7C,wBAAA,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC;gBAC1B,CAAC;gBACD,SAAS,EAAE,CAAC,EAAE,MAAM,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,MAAM,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;gBACpE,MAAM,EAAE,CAAC,EAAE,MAAM,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;gBAC3D,OAAO,EAAE,CAAC,EAAE,MAAM,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,MAAM,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;AAC9D,gBAAA,KAAK,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE;AACzB,aAAA;AACD,YAAA,MAAM,EAAE,OAAO,IAAI,KAAI;gBACrB,MAAM,EAAE,WAAW,EAAE,GAAG,EAAE,IAAI,IAAI,IAAI,EAAE,CAGvC;gBACD,IAAI,WAAW,EAAE;AACf,oBAAA,MAAM,SAAS,GACb,WAAW,CAAC,IAAI,KAAK,OAAO;yBAC3B,WAAW,IAAI,EAAE,CAAC,cAAc,KAAK,QAAQ,CAAC;AACjD,oBAAA,WAAW,GAAG,CAAC,MAAM,IAAI,SAAS;AAClC,oBAAA,IAAI,WAAW;wBAAE;AACjB,oBAAA,MAAM,EAAE,CAAC,oBAAoB,CAAC,WAAW,CAAC;AAC1C,oBAAA,IAAI,WAAW,CAAC,IAAI,KAAK,OAAO,EAAE;AAChC,wBAAA,MAAM,EAAE,CAAC,mBAAmB,EAAE;wBAC9B,UAAU,CAAC,EAAE,WAAW,EAAE,EAAE,CAAC,gBAAgB,EAAE,CAAC;oBAClD;gBACF;qBAAO,IAAI,GAAG,EAAE;AACd,oBAAA,IAAI;AACF,wBAAA,MAAM,EAAE,CAAC,eAAe,CAAC,GAAG,CAAC;oBAC/B;oBAAE,OAAO,GAAG,EAAE;AACZ,wBAAA,IAAI,CAAC,WAAW;AAAE,4BAAA,MAAM,GAAG;oBAC7B;gBACF;YACF,CAAC;YACD,KAAK,EAAE,MAAK;gBACV,EAAE,EAAE,KAAK,EAAE;gBACX,EAAE,CAAC,KAAK,EAAE;YACZ,CAAC;SACF;AACH,IAAA,CAAC;AACH;AAiCA;;;;;;;AAOG;AACG,SAAU,UAAU,CACxB,MAAyB,EACzB,GAAsB,EAAA;IAEtB,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC;IACjD,MAAM,SAAS,GAAG,GAAG,CAAC,SAAS,IAAI,gBAAgB,EAAE;AACrD,IAAA,MAAM,MAAM,GAAG,MAAM,CAAwB,YAAY,6EAAC;IAC1D,MAAM,SAAS,GAAG,MAAM,CAAsB,IAAI,GAAG,EAAE,gFAAC;AACxD,IAAA,MAAM,KAAK,GAAG,IAAI,GAAG,EAAgB;IACrC,IAAI,eAAe,GAAmB,EAAE;IACxC,IAAI,SAAS,GAAyB,IAAI;AAC1C,IAAA,IAAI,cAAyD;IAC7D,IAAI,MAAM,GAAG,KAAK;AAElB,IAAA,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE;QAC1B,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,QAAQ,EAAE,GAAG,CAAC,QAAQ;QACtB,aAAa,EAAE,GAAG,CAAC,aAAa;QAChC,QAAQ;AACT,KAAA,CAAC;AAEF,IAAA,MAAM,OAAO,GAAG,CACd,IAA4B,EAC5B,MAA8B,KAClB,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAE3E,IAAA,MAAM,MAAM,GAAG,CAAC,IAAU,EAAE,GAAW,KAAU;AAC/C,QAAA,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;AAC7C,IAAA,CAAC;AAED,IAAA,MAAM,SAAS,GAAG,CAAC,GAAW,KAAU;QACtC,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,MAAM,EAAE,EAAE;YACjC,IAAI,IAAI,CAAC,IAAI;AAAE,gBAAA,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC;QAClC;AACF,IAAA,CAAC;AAED,IAAA,MAAM,QAAQ,GAAG,CAAC,MAAc,KAAU;QACxC,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC;AAC9B,QAAA,IAAI,CAAC,IAAI;YAAE;AACX,QAAA,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC;AACpB,QAAA,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM;AAAE,YAAA,KAAK,EAAE;AACxC,QAAA,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE;AACjB,QAAA,SAAS,CAAC,MAAM,CAAC,CAAC,GAAG,KAAI;AACvB,YAAA,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC;AACzB,YAAA,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC;AACnB,YAAA,OAAO,IAAI;AACb,QAAA,CAAC,CAAC;AACJ,IAAA,CAAC;AAED,IAAA,MAAM,UAAU,GAAG,CAAC,MAAc,KAAU;QAC1C,IAAI,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC;AAC5B,QAAA,IAAI,IAAI;AAAE,YAAA,OAAO,IAAI;QACrB,MAAM,IAAI,GAAG,SAAS,CAAC;AACrB,YAAA,MAAM,EAAE,MAAM;AACd,YAAA,MAAM,EAAE,IAAI,CAAC,MAAM,GAAG,MAAM;YAC5B,UAAU,EAAE,CAAC,IAAI,KACf,SAAS,EAAE,IAAI,CAAC,EAAE,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;AACrE,SAAA,CAAC;AACF,QAAA,IAAI,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE;AACxC,QAAA,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC;AACvB,QAAA,IAAI,CAAC,MAAM,CAAC,IAAI,CACd,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,MAAK;AACvB,YAAA,IAAK,CAAC,IAAI,GAAG,IAAI;AACjB,YAAA,SAAS,CAAC,MAAM,CAAC,CAAC,GAAG,KAAK,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;AACnD,YAAA,MAAM,CAAC,IAAK,EAAE,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,IAAI,CAAC,SAAS,EAAE,EAAE,CAAC;QACrD,CAAC,CAAC,EACF,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,KAAK,KAAI;AAC/B,YAAA,IAAI,GAAW;AACf,YAAA,IAAI;AACF,gBAAA,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAW;YACnC;AAAE,YAAA,MAAM;gBACN;YACF;AACA,YAAA,aAAa,CAAC,IAAK,EAAE,GAAG,CAAC;AAC3B,QAAA,CAAC,CAAC,EACF,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,QAAQ,CAAC,MAAM,CAAC,CAAC,CAC7C;AACD,QAAA,OAAO,IAAI;AACb,IAAA,CAAC;AAED,IAAA,MAAM,aAAa,GAAG,CAAC,IAAU,EAAE,GAAW,KAAU;AACtD,QAAA,QAAQ,GAAG,CAAC,CAAC;YACX,KAAK,OAAO,EAAE;AACZ,gBAAA,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE;gBAC5B,IAAI,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,GAAG,CAAC,EAAE,CAAC;oBAAE,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC;;oBACxD,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC;gBAC/D;YACF;YACA,KAAK,OAAO,EAAE;AACZ,gBAAA,IAAI,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,GAAG,CAAC,EAAE,CAAC,EAAE;oBACrC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAS,EAAE,GAAG,CAAC,EAAE,CAAC;gBACrC;gBACA;YACF;AACA,YAAA,KAAK,UAAU;gBACb;AACF,YAAA,KAAK,KAAK;AACR,gBAAA,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC;gBACrB;;AAEN,IAAA,CAAC;AAED,IAAA,MAAM,eAAe,GAAG,CAAC,GAAc,KAAU;AAC/C,QAAA,IAAI,GAAG,CAAC,IAAI,KAAK,GAAG,CAAC,IAAI;YAAE;AAC3B,QAAA,QAAQ,GAAG,CAAC,CAAC;AACX,YAAA,KAAK,SAAS;AACZ,gBAAA,KAAK,MAAM,MAAM,IAAI,GAAG,CAAC,OAAO;oBAAE,UAAU,CAAC,MAAM,CAAC;AACpD,gBAAA,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC;gBAClB;AACF,YAAA,KAAK,QAAQ;gBACX,IAAI,GAAG,CAAC,IAAI;AAAE,oBAAA,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC;;AAC7B,oBAAA,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC;gBAC3B;AACF,YAAA,KAAK,QAAQ;AACX,gBAAA,UAAU,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC;gBAC1C;AACF,YAAA,KAAK,QAAQ;AACX,gBAAA,KAAK,EAAE;gBACP;;AAEN,IAAA,CAAC;IAED,MAAM,gBAAgB,GAAG,MAAW;AAClC,QAAA,IAAI,MAAM;YAAE;QACZ,KAAK,MAAM,KAAK,IAAI,eAAe,CAAC,MAAM,CAAC,CAAC,CAAC;AAAE,YAAA,KAAK,EAAE;AACtD,QAAA,MAAM,CAAC,GAAG,GAAG,CAAC,SAAS,EAAE;QACzB,SAAS,GAAG,CAAC;AACb,QAAA,eAAe,GAAG;AAChB,YAAA,CAAC,CAAC,SAAS,CAAC,eAAe,CAAC;AAC5B,YAAA,CAAC,CAAC,OAAO,CAAC,MAAK;AACb,gBAAA,IAAI,MAAM,IAAI,SAAS,KAAK,CAAC;oBAAE;gBAC/B,SAAS,GAAG,IAAI;AAChB,gBAAA,cAAc,GAAG,UAAU,CAAC,gBAAgB,EAAE,IAAI,CAAC;AACrD,YAAA,CAAC,CAAC;SACH;QACD,CAAC,CAAC,IAAI,CAAC;AACL,YAAA,CAAC,EAAE,OAAO;YACV,IAAI,EAAE,GAAG,CAAC,IAAI;YACd,MAAM,EAAE,IAAI,CAAC,MAAM;AACnB,YAAA,KAAK,EAAE,kBAAkB;AACzB,YAAA,aAAa,EAAE,GAAG,CAAC,aAAa,IAAI,CAAC;AACtC,SAAA,CAAC;AACJ,IAAA,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,GAAG,KAAK,SAAS,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;IAExE,MAAM,KAAK,GAAG,MAAW;AACvB,QAAA,IAAI,MAAM;YAAE;QACZ,MAAM,GAAG,IAAI;QACb,IAAI,cAAc,KAAK,SAAS;YAAE,YAAY,CAAC,cAAc,CAAC;AAC9D,QAAA,UAAU,EAAE;QACZ,KAAK,MAAM,MAAM,IAAI,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;YAAE,QAAQ,CAAC,MAAM,CAAC;QACxD,KAAK,MAAM,KAAK,IAAI,eAAe,CAAC,MAAM,CAAC,CAAC,CAAC;AAAE,YAAA,KAAK,EAAE;QACtD,SAAS,EAAE,KAAK,EAAE;QAClB,SAAS,GAAG,IAAI;QAChB,IAAI,CAAC,OAAO,EAAE;AACd,QAAA,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC;AAC1B,IAAA,CAAC;IAED,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC;AACzC,IAAA,gBAAgB,EAAE;IAElB,OAAO;AACL,QAAA,MAAM,EAAE,MAAM,CAAC,UAAU,EAAE;QAC3B,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,SAAS,EAAE,CAAC,CAAC;QACvC,KAAK;KACN;AACH;;AC5UA;;AAEG;;;;"}
|
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 };
|