@mmstack/mesh-protocol 0.1.0 → 0.1.1
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 +40 -2
- package/index.d.ts +31 -7
- package/index.js +14 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -139,8 +139,46 @@ export class MeshRoom {
|
|
|
139
139
|
}
|
|
140
140
|
```
|
|
141
141
|
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
## Persistence
|
|
143
|
+
|
|
144
|
+
Rooms live in memory. That covers dev and single-process deployments, and when the relay
|
|
145
|
+
restarts, the first client to rejoin seeds the room from its own local state, so nothing is
|
|
146
|
+
lost as long as somebody was online. For durability beyond that, the relay exposes a seam
|
|
147
|
+
rather than a storage engine, because the envelope already is the persistence record: an
|
|
148
|
+
event-sourced journal is just `snapshot + envelopes`, compacted by re-snapshotting.
|
|
149
|
+
|
|
150
|
+
`onCommit` fires after every envelope is sequenced and folded, with the envelope and the
|
|
151
|
+
room's current `{ seq, epoch, root }`. Append the envelope to your journal, and checkpoint the
|
|
152
|
+
root as often as you like. The relay never awaits it, so batching and backpressure belong to
|
|
153
|
+
your adapter:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
const relay = createRelay({
|
|
157
|
+
onCommit: (room, env, state) => {
|
|
158
|
+
journal.append(room, env); // your DB, KV, or DO storage
|
|
159
|
+
if (state.seq % 100 === 0) checkpoints.put(room, state);
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`relay.hydrate(room, snapshot)` restores a persisted room before clients join (relay boot, or
|
|
165
|
+
inside a Durable Object's `blockConcurrencyWhile`). It refuses once the room has state or
|
|
166
|
+
members, so your load can race a fast client without corrupting a live sequence space:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
const saved = await checkpoints.get(roomName);
|
|
170
|
+
if (saved) {
|
|
171
|
+
relay.hydrate(roomName, {
|
|
172
|
+
...saved, // seq, epoch, root
|
|
173
|
+
journal: await journal.tail(roomName, saved.seq),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Restoring the persisted `epoch` is what lets clients that were connected before the restart
|
|
179
|
+
keep their sequence watermark and catch up with a cheap `delta` answer. Omit it and they fall
|
|
180
|
+
back to a full snapshot, which is always safe. The optional journal tail is only there to make
|
|
181
|
+
those delta answers possible; the room is complete without it.
|
|
144
182
|
|
|
145
183
|
## WebRTC signaling
|
|
146
184
|
|
package/index.d.ts
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
//#region src/lib/wire.d.ts
|
|
2
|
-
/**
|
|
3
|
-
* Canonical wire types of the mmstack op protocol (op-protocol RFC §3/§6). Structurally
|
|
4
|
-
* identical to the L0 types in `@mmstack/primitives` — deliberately NOT imported from there,
|
|
5
|
-
* so this package stays zero-dependency and never drags Angular peers onto a server. The
|
|
6
|
-
* client package asserts mutual assignability at compile time.
|
|
7
|
-
*/
|
|
8
2
|
type Key = string | number;
|
|
9
3
|
type StoreOp = {
|
|
10
4
|
kind: 'set';
|
|
@@ -184,6 +178,30 @@ type RelayOptions = {
|
|
|
184
178
|
readonly journalLimit?: number;
|
|
185
179
|
readonly now?: () => number;
|
|
186
180
|
readonly onViolation?: (room: string, violation: PolicyViolation) => void;
|
|
181
|
+
/**
|
|
182
|
+
* The persistence egress: fired after an envelope is sequenced, folded into the room
|
|
183
|
+
* snapshot, and broadcast. The envelope is the persistence record (append it to a journal);
|
|
184
|
+
* `state` carries the folded root for throttled checkpoints. Called synchronously and never
|
|
185
|
+
* awaited: batch, debounce, and store at the adapter layer. Pair with {@link Relay.hydrate}.
|
|
186
|
+
*/
|
|
187
|
+
readonly onCommit?: (room: string, env: SeqEnvelope, state: RoomState) => void;
|
|
188
|
+
};
|
|
189
|
+
/** The room's durable state at a commit: what a checkpoint needs to capture. */
|
|
190
|
+
type RoomState = {
|
|
191
|
+
readonly seq: number;
|
|
192
|
+
readonly epoch: string;
|
|
193
|
+
readonly root: unknown;
|
|
194
|
+
};
|
|
195
|
+
/** A persisted room to restore via {@link Relay.hydrate}. */
|
|
196
|
+
type RoomSnapshot = {
|
|
197
|
+
readonly seq: number;
|
|
198
|
+
readonly root: unknown;
|
|
199
|
+
/**
|
|
200
|
+
* Restore the persisted epoch so clients reconnecting across the restart keep their seq
|
|
201
|
+
* watermark and get a `delta` answer; omit to mint a fresh one (they re-snapshot instead).
|
|
202
|
+
*/
|
|
203
|
+
readonly epoch?: string; /** Journal tail (ascending seq, entries at or below `seq`) enabling those delta answers. */
|
|
204
|
+
readonly journal?: readonly SeqEnvelope[];
|
|
187
205
|
};
|
|
188
206
|
type RelayConnection = {
|
|
189
207
|
receive(msg: ClientMsg): void;
|
|
@@ -197,6 +215,12 @@ type RoomInfo = {
|
|
|
197
215
|
type Relay = {
|
|
198
216
|
/** Attach an authenticated connection. `ctx.writer` is the trusted principal. */connect(socket: RelaySocket, ctx: PrincipalCtx): RelayConnection;
|
|
199
217
|
room(name: string): RoomInfo | undefined;
|
|
218
|
+
/**
|
|
219
|
+
* Restore a persisted room before clients join (relay boot, Durable Object wake). Refused
|
|
220
|
+
* (`false`) once the room has state or members: hydrating a live seq space would corrupt
|
|
221
|
+
* it. Load asynchronously at the adapter layer, then hydrate synchronously.
|
|
222
|
+
*/
|
|
223
|
+
hydrate(name: string, snapshot: RoomSnapshot): boolean;
|
|
200
224
|
};
|
|
201
225
|
/**
|
|
202
226
|
* The reference relay core (op-protocol RFC §6/§7): room-scoped sequencing, journal +
|
|
@@ -213,4 +237,4 @@ type Relay = {
|
|
|
213
237
|
*/
|
|
214
238
|
declare function createRelay(opt?: RelayOptions): Relay;
|
|
215
239
|
//#endregion
|
|
216
|
-
export { type ClientEnvMsg, type ClientMsg, type ClientPresenceMsg, type ClientSignalMsg, type EjectMsg, type HelloMsg, type Hlc, type Key, MESH_PROTO_VERSION, type MemberMsg, type OpEnvelope, type OpPolicy, type PathAclRule, type PolicyViolation, type PresenceState, type PrincipalCtx, type RejectMsg, type Relay, type RelayConnection, type RelayLimits, type RelayOptions, type RelaySocket, type RoomInfo, type SeqEnvelope, type ServerEnvMsg, type ServerMsg, type ServerPresenceMsg, type ServerSignalMsg, type StoreOp, type WelcomeMsg, applyWireOps, checkEnvelope, createRelay, pathPrefixAcl };
|
|
240
|
+
export { type ClientEnvMsg, type ClientMsg, type ClientPresenceMsg, type ClientSignalMsg, type EjectMsg, type HelloMsg, type Hlc, type Key, MESH_PROTO_VERSION, type MemberMsg, type OpEnvelope, type OpPolicy, type PathAclRule, type PolicyViolation, type PresenceState, type PrincipalCtx, type RejectMsg, type Relay, type RelayConnection, type RelayLimits, type RelayOptions, type RelaySocket, type RoomInfo, type RoomSnapshot, type RoomState, type SeqEnvelope, type ServerEnvMsg, type ServerMsg, type ServerPresenceMsg, type ServerSignalMsg, type StoreOp, type WelcomeMsg, applyWireOps, checkEnvelope, createRelay, pathPrefixAcl };
|
package/index.js
CHANGED
|
@@ -158,6 +158,15 @@ function createRelay(opt = {}) {
|
|
|
158
158
|
journal: room.journal.length
|
|
159
159
|
} : void 0;
|
|
160
160
|
},
|
|
161
|
+
hydrate: (name, snapshot) => {
|
|
162
|
+
const room = roomOf(name);
|
|
163
|
+
if (room.seq !== 0 || room.members.size > 0 || room.journal.length > 0) return false;
|
|
164
|
+
room.seq = snapshot.seq;
|
|
165
|
+
room.root = snapshot.root;
|
|
166
|
+
if (snapshot.epoch !== void 0) room.epoch = snapshot.epoch;
|
|
167
|
+
if (snapshot.journal) room.journal = snapshot.journal.filter((e) => e.seq <= snapshot.seq).sort((a, b) => a.seq - b.seq).slice(-journalLimit);
|
|
168
|
+
return true;
|
|
169
|
+
},
|
|
161
170
|
connect: (socket, ctx) => {
|
|
162
171
|
const joined = /* @__PURE__ */ new Map();
|
|
163
172
|
const disconnect = () => {
|
|
@@ -309,6 +318,11 @@ function createRelay(opt = {}) {
|
|
|
309
318
|
room: msg.room,
|
|
310
319
|
env: seqEnv
|
|
311
320
|
});
|
|
321
|
+
opt.onCommit?.(msg.room, seqEnv, {
|
|
322
|
+
seq: room.seq,
|
|
323
|
+
epoch: room.epoch,
|
|
324
|
+
root: room.root
|
|
325
|
+
});
|
|
312
326
|
}
|
|
313
327
|
};
|
|
314
328
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mmstack/mesh-protocol",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"module": "./index.js",
|
|
@@ -33,4 +33,4 @@
|
|
|
33
33
|
},
|
|
34
34
|
"homepage": "https://github.com/mihajm/mmstack/blob/master/packages/mesh/protocol",
|
|
35
35
|
"sideEffects": false
|
|
36
|
-
}
|
|
36
|
+
}
|