@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 CHANGED
@@ -139,8 +139,46 @@ export class MeshRoom {
139
139
  }
140
140
  ```
141
141
 
142
- Durable storage (journal persistence across restarts) rides the same seam and lands with a
143
- later release. In-memory rooms cover dev and single-process deployments today.
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.0",
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
+ }