@rivalis/fleet 8.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/lib/Poller.js ADDED
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/orchestrator/Poller.ts
21
+ var Poller_exports = {};
22
+ __export(Poller_exports, {
23
+ Poller: () => Poller
24
+ });
25
+ module.exports = __toCommonJS(Poller_exports);
26
+ var FORCE_FULL_EVERY_POLLS = 12;
27
+ var Poller = class {
28
+ constructor(scheduler, intervalMs, callbacks) {
29
+ this.scheduler = scheduler;
30
+ this.intervalMs = intervalMs;
31
+ this.callbacks = callbacks;
32
+ }
33
+ scheduler;
34
+ intervalMs;
35
+ callbacks;
36
+ entries = /* @__PURE__ */ new Map();
37
+ reqSeq = 0;
38
+ /** True while the instance is being polled (started and not yet forgotten). */
39
+ has(instanceId) {
40
+ return this.entries.has(instanceId);
41
+ }
42
+ /** Begin polling an instance: send the first poll now, then one every `intervalMs`. */
43
+ start(instanceId) {
44
+ this.entries.set(instanceId, { timer: null, outstandingReqId: null, missed: 0, pollCount: 0 });
45
+ this.poll(instanceId);
46
+ this.schedule(instanceId);
47
+ }
48
+ /**
49
+ * Consume the outstanding poll's reply (§7 enforcement). Returns `true` when
50
+ * `reqId` matches the in-flight poll (resets the missed counter); `false` when it
51
+ * matches no outstanding poll — an unsolicited / duplicate / post-settle
52
+ * `fleet/state`, which the caller turns into a kick.
53
+ */
54
+ reply(instanceId, reqId) {
55
+ const entry = this.entries.get(instanceId);
56
+ if (entry === void 0 || entry.outstandingReqId === null || entry.outstandingReqId !== reqId) {
57
+ return false;
58
+ }
59
+ entry.outstandingReqId = null;
60
+ entry.missed = 0;
61
+ return true;
62
+ }
63
+ /** Stop polling an instance and cancel its timer (teardown). Idempotent. */
64
+ forget(instanceId) {
65
+ const entry = this.entries.get(instanceId);
66
+ if (entry !== void 0) {
67
+ this.scheduler.clearTimeout(entry.timer);
68
+ this.entries.delete(instanceId);
69
+ }
70
+ }
71
+ schedule(instanceId) {
72
+ const entry = this.entries.get(instanceId);
73
+ if (entry === void 0) {
74
+ return;
75
+ }
76
+ entry.timer = this.scheduler.setTimeout(() => this.tick(instanceId), this.intervalMs);
77
+ }
78
+ tick(instanceId) {
79
+ const entry = this.entries.get(instanceId);
80
+ if (entry === void 0) {
81
+ return;
82
+ }
83
+ if (entry.outstandingReqId !== null) {
84
+ entry.missed += 1;
85
+ if (entry.missed === 2) {
86
+ this.callbacks.onStale(instanceId);
87
+ }
88
+ if (entry.missed >= 3) {
89
+ this.callbacks.onEvict(instanceId);
90
+ return;
91
+ }
92
+ this.schedule(instanceId);
93
+ return;
94
+ }
95
+ this.poll(instanceId);
96
+ this.schedule(instanceId);
97
+ }
98
+ poll(instanceId) {
99
+ const entry = this.entries.get(instanceId);
100
+ if (entry === void 0) {
101
+ return;
102
+ }
103
+ const reqId = `poll_${++this.reqSeq}`;
104
+ const forceFull = entry.pollCount % FORCE_FULL_EVERY_POLLS === 0;
105
+ entry.pollCount += 1;
106
+ entry.outstandingReqId = reqId;
107
+ this.callbacks.sendPoll(instanceId, reqId, forceFull);
108
+ }
109
+ };
110
+ // Annotate the CommonJS export names for ESM import in node:
111
+ 0 && (module.exports = {
112
+ Poller
113
+ });
@@ -0,0 +1,471 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/agent/Snapshot.ts
21
+ var Snapshot_exports = {};
22
+ __export(Snapshot_exports, {
23
+ MAX_SNAPSHOT_BYTES: () => MAX_SNAPSHOT_BYTES,
24
+ Snapshot: () => Snapshot
25
+ });
26
+ module.exports = __toCommonJS(Snapshot_exports);
27
+ var import_node_crypto2 = require("crypto");
28
+
29
+ // src/util/canonical.ts
30
+ var import_node_crypto = require("crypto");
31
+ function canonicalize(value) {
32
+ return encode(value);
33
+ }
34
+ function encode(value) {
35
+ if (value === null) {
36
+ return "null";
37
+ }
38
+ const type = typeof value;
39
+ if (type === "string") {
40
+ return JSON.stringify(value);
41
+ }
42
+ if (type === "number") {
43
+ return Number.isFinite(value) ? String(value) : "null";
44
+ }
45
+ if (type === "boolean") {
46
+ return value ? "true" : "false";
47
+ }
48
+ if (type === "bigint") {
49
+ return value.toString();
50
+ }
51
+ if (Array.isArray(value)) {
52
+ const items = value.map((item) => encodeArrayItem(item));
53
+ return "[" + items.join(",") + "]";
54
+ }
55
+ if (type === "object") {
56
+ const obj = value;
57
+ const keys = Object.keys(obj).sort();
58
+ const parts = [];
59
+ for (const key of keys) {
60
+ const child = obj[key];
61
+ if (isSkippable(child)) {
62
+ continue;
63
+ }
64
+ parts.push(JSON.stringify(key) + ":" + encode(child));
65
+ }
66
+ return "{" + parts.join(",") + "}";
67
+ }
68
+ return "null";
69
+ }
70
+ function encodeArrayItem(item) {
71
+ return isSkippable(item) ? "null" : encode(item);
72
+ }
73
+ function isSkippable(value) {
74
+ const type = typeof value;
75
+ return value === void 0 || type === "function" || type === "symbol";
76
+ }
77
+ function hash64(value) {
78
+ const digest = (0, import_node_crypto.createHash)("sha256").update(canonicalize(value)).digest();
79
+ return digest.subarray(0, 8).toString("hex");
80
+ }
81
+
82
+ // src/util/logger.ts
83
+ var NOOP_LOGGER = {
84
+ error() {
85
+ },
86
+ warning() {
87
+ },
88
+ info() {
89
+ },
90
+ debug() {
91
+ },
92
+ verbose() {
93
+ },
94
+ log() {
95
+ }
96
+ };
97
+
98
+ // src/util/packageVersion.ts
99
+ var import_node_module = require("module");
100
+ var import_meta = {};
101
+ function packageVersion() {
102
+ try {
103
+ const metaUrl = import_meta.url;
104
+ const req = metaUrl ? (0, import_node_module.createRequire)(metaUrl) : require;
105
+ const pkg = req("../package.json");
106
+ return pkg.version ?? "0.0.0";
107
+ } catch {
108
+ return "0.0.0";
109
+ }
110
+ }
111
+
112
+ // src/wire/topics.ts
113
+ var PROTOCOL_VERSION = 3;
114
+ var Topics = {
115
+ /** orch → agent: assigns id + heartbeat (poll cadence) on join; followed by the first poll. */
116
+ hello: "fleet/hello",
117
+ /** orch → agent: state poll. Carries `knownHash` (dedup) + the last recorded `status` (echo). */
118
+ poll: "fleet/poll",
119
+ /** agent → orch: poll reply. Full snapshot when the hash differs from `knownHash`, hash-only otherwise. */
120
+ state: "fleet/state",
121
+ /** orch → agent: command push. */
122
+ cmd: "fleet/cmd",
123
+ /** agent → orch: command result. */
124
+ ack: "fleet/ack"
125
+ };
126
+
127
+ // src/wire/serializer.ts
128
+ var import_node_module2 = require("module");
129
+ var import_meta2 = {};
130
+ var WIRE_MAJOR = PROTOCOL_VERSION;
131
+ var WIRE_MINOR = 0;
132
+ var HEADER_BYTES = 2;
133
+ var Type = {
134
+ Label: "Label",
135
+ SyncRoom: "SyncRoom",
136
+ Capacity: "Capacity",
137
+ AckRoom: "AckRoom",
138
+ Hello: "Hello",
139
+ Poll: "Poll",
140
+ State: "State",
141
+ Cmd: "Cmd",
142
+ Ack: "Ack"
143
+ };
144
+ var TOPIC_TYPE = {
145
+ [Topics.hello]: Type.Hello,
146
+ [Topics.poll]: Type.Poll,
147
+ [Topics.state]: Type.State,
148
+ [Topics.cmd]: Type.Cmd,
149
+ [Topics.ack]: Type.Ack
150
+ };
151
+ var serializer = null;
152
+ function getSerializer() {
153
+ if (serializer !== null) {
154
+ return serializer;
155
+ }
156
+ const metaUrl = import_meta2.url;
157
+ const req = metaUrl ? (0, import_node_module2.createRequire)(metaUrl) : require;
158
+ const mod = req("@toolcase/serializer");
159
+ const Serializer = mod.Serializer ?? mod.default;
160
+ const F = Serializer.FieldType;
161
+ const s = new Serializer("fleet");
162
+ s.define(Type.Label, [
163
+ { key: "key", type: F.STRING, rule: "optional" },
164
+ { key: "value", type: F.STRING, rule: "optional" }
165
+ ]);
166
+ s.define(Type.SyncRoom, [
167
+ { key: "id", type: F.STRING, rule: "optional" },
168
+ { key: "type", type: F.STRING, rule: "optional" },
169
+ { key: "connections", type: F.UINT32, rule: "optional" },
170
+ { key: "origin", type: F.STRING, rule: "optional" }
171
+ ]);
172
+ s.define(Type.Capacity, [
173
+ // null = unlimited (§6). Absent on the wire ⇒ null; an explicit 0 ⇒ 0.
174
+ { key: "maxConnections", type: F.INT32, rule: "optional", default: null },
175
+ { key: "maxRooms", type: F.INT32, rule: "optional", default: null }
176
+ ]);
177
+ s.define(Type.AckRoom, [
178
+ { key: "id", type: F.STRING, rule: "optional" },
179
+ { key: "type", type: F.STRING, rule: "optional" }
180
+ ]);
181
+ s.define(Type.Hello, [
182
+ { key: "instanceId", type: F.STRING, rule: "optional" },
183
+ { key: "protocolVersion", type: F.UINT32, rule: "optional" },
184
+ { key: "heartbeatMs", type: F.UINT32, rule: "optional" }
185
+ ]);
186
+ s.define(Type.Poll, [
187
+ { key: "reqId", type: F.STRING, rule: "optional" },
188
+ // Absent ⇒ null (no prior state / forced full, subsumes the old fleet/resync).
189
+ { key: "knownHash", type: F.STRING, rule: "optional" },
190
+ { key: "status", type: F.STRING, rule: "optional" }
191
+ ]);
192
+ s.define(Type.State, [
193
+ { key: "reqId", type: F.STRING, rule: "optional" },
194
+ // full=false is a hash-only liveness reply: the snapshot fields below are
195
+ // omitted on the wire (preserving the old sync/ping dedup, orch-initiated).
196
+ { key: "full", type: F.BOOL, rule: "optional" },
197
+ { key: "seq", type: F.UINT32, rule: "optional" },
198
+ { key: "hash", type: F.STRING, rule: "optional" },
199
+ { key: "name", type: F.STRING, rule: "optional" },
200
+ { key: "processUid", type: F.STRING, rule: "optional" },
201
+ { key: "agentVersion", type: F.STRING, rule: "optional" },
202
+ { key: "protocolVersion", type: F.UINT32, rule: "optional" },
203
+ { key: "endpointUrl", type: F.STRING, rule: "optional" },
204
+ { key: "labels", type: Type.Label, rule: "repeated" },
205
+ { key: "capacity", type: Type.Capacity, rule: "optional" },
206
+ { key: "autoCreate", type: F.BOOL, rule: "optional" },
207
+ { key: "roomTypes", type: F.STRING, rule: "repeated" },
208
+ { key: "rooms", type: Type.SyncRoom, rule: "repeated" },
209
+ { key: "status", type: F.STRING, rule: "optional" }
210
+ ]);
211
+ s.define(Type.Cmd, [
212
+ { key: "cmdId", type: F.STRING, rule: "optional" },
213
+ { key: "op", type: F.STRING, rule: "optional" },
214
+ { key: "roomId", type: F.STRING, rule: "optional" },
215
+ { key: "roomType", type: F.STRING, rule: "optional" }
216
+ ]);
217
+ s.define(Type.Ack, [
218
+ { key: "cmdId", type: F.STRING, rule: "optional" },
219
+ { key: "ok", type: F.BOOL, rule: "optional" },
220
+ { key: "error", type: F.STRING, rule: "optional" },
221
+ { key: "alreadyGone", type: F.BOOL, rule: "optional" },
222
+ { key: "room", type: Type.AckRoom, rule: "optional" },
223
+ // APPEND-ONLY (task 003): the room-already-exists signal must stay LAST so
224
+ // existing tags are unmoved (see the append-only tag rule in the file header).
225
+ { key: "exists", type: F.BOOL, rule: "optional" }
226
+ ]);
227
+ serializer = s;
228
+ return s;
229
+ }
230
+ function labelsToList(labels) {
231
+ return Object.entries(labels ?? {}).map(([key, value]) => ({ key, value }));
232
+ }
233
+ function capacityToMessage(capacity) {
234
+ return {
235
+ maxConnections: capacity?.maxConnections ?? null,
236
+ maxRooms: capacity?.maxRooms ?? null
237
+ };
238
+ }
239
+ function stateToMessage(p) {
240
+ if (!p.full) {
241
+ return { reqId: p.reqId, full: false, seq: p.seq, hash: p.hash };
242
+ }
243
+ return {
244
+ reqId: p.reqId,
245
+ full: true,
246
+ seq: p.seq,
247
+ hash: p.hash,
248
+ name: p.name,
249
+ processUid: p.processUid,
250
+ agentVersion: p.agentVersion,
251
+ protocolVersion: p.protocolVersion,
252
+ endpointUrl: p.endpointUrl,
253
+ labels: labelsToList(p.labels),
254
+ capacity: capacityToMessage(p.capacity),
255
+ autoCreate: p.autoCreate,
256
+ roomTypes: p.roomTypes ?? [],
257
+ rooms: (p.rooms ?? []).map((r) => ({
258
+ id: r.id,
259
+ type: r.type,
260
+ connections: r.connections,
261
+ origin: r.origin
262
+ })),
263
+ status: p.status
264
+ };
265
+ }
266
+ function toMessage(topic, payload) {
267
+ switch (topic) {
268
+ case Topics.state:
269
+ return stateToMessage(payload);
270
+ case Topics.poll: {
271
+ const p = payload;
272
+ const msg = { reqId: p.reqId, status: p.status };
273
+ if (p.knownHash !== null && p.knownHash !== void 0) {
274
+ msg.knownHash = p.knownHash;
275
+ }
276
+ return msg;
277
+ }
278
+ default:
279
+ return payload;
280
+ }
281
+ }
282
+ function encodeFrame(topic, payload) {
283
+ const type = TOPIC_TYPE[topic];
284
+ if (type === void 0) {
285
+ throw new Error(`fleet wire: no message type for topic=${topic}`);
286
+ }
287
+ const body = getSerializer().encode(type, toMessage(topic, payload));
288
+ const frame = new Uint8Array(HEADER_BYTES + body.length);
289
+ frame[0] = WIRE_MAJOR;
290
+ frame[1] = WIRE_MINOR;
291
+ frame.set(body, HEADER_BYTES);
292
+ return frame;
293
+ }
294
+
295
+ // src/agent/Snapshot.ts
296
+ var MAX_SNAPSHOT_BYTES = 4 * 1024 * 1024;
297
+ var WARN_RATIO = 0.5;
298
+ var ERROR_RATIO = 0.9;
299
+ var MIN_CORE_VERSION = "6.1.0";
300
+ function generateProcessUid() {
301
+ return "p_" + (0, import_node_crypto2.randomBytes)(12).toString("hex");
302
+ }
303
+ var Snapshot = class {
304
+ /** Stable per-process id (§6) — constant across reconnects. */
305
+ processUid;
306
+ rivalis;
307
+ logger;
308
+ name;
309
+ endpointUrl;
310
+ labels;
311
+ capacity;
312
+ autoCreate;
313
+ agentVersion;
314
+ protocolVersion;
315
+ /** Room ids created in response to `fleet/cmd` → stamped `origin: 'fleet'`. */
316
+ fleetOrigins = /* @__PURE__ */ new Set();
317
+ /** Agent owns `status` (§7); flipped via `setStatus`. */
318
+ statusValue;
319
+ /** Per-connection monotonic frame counter — defensive hardening only (§7). */
320
+ seq = 0;
321
+ constructor(rivalis, options, logger) {
322
+ this.assertCoreSupport(rivalis);
323
+ this.rivalis = rivalis;
324
+ this.logger = logger ?? rivalis.logging?.getLogger?.("fleet:agent") ?? NOOP_LOGGER;
325
+ this.name = options.name;
326
+ this.endpointUrl = options.endpointUrl;
327
+ this.labels = options.labels ?? {};
328
+ this.capacity = {
329
+ maxConnections: options.capacity?.maxConnections ?? null,
330
+ maxRooms: options.capacity?.maxRooms ?? null
331
+ };
332
+ this.autoCreate = options.autoCreate ?? true;
333
+ this.agentVersion = options.agentVersion ?? packageVersion();
334
+ this.protocolVersion = options.protocolVersion ?? PROTOCOL_VERSION;
335
+ this.processUid = options.processUid ?? generateProcessUid();
336
+ this.statusValue = options.status ?? "active";
337
+ }
338
+ get status() {
339
+ return this.statusValue;
340
+ }
341
+ /** Flip the agent-owned status (§7). The next snapshot carries the new value. */
342
+ setStatus(status) {
343
+ this.statusValue = status;
344
+ }
345
+ /** Stamp a room as fleet-created (`origin: 'fleet'`). Called on a `fleet/cmd` create. */
346
+ markFleetOrigin(roomId) {
347
+ this.fleetOrigins.add(roomId);
348
+ }
349
+ /** Drop provenance for a destroyed room so a future id reuse is not mis-stamped. */
350
+ forgetRoom(roomId) {
351
+ this.fleetOrigins.delete(roomId);
352
+ }
353
+ /**
354
+ * New connection (reconnect): reset the `seq` counter. The reconnect assigns a
355
+ * fresh `instanceId`, so the orchestrator holds no prior hash and its first poll
356
+ * carries `knownHash: null` → the next reply is always a full snapshot (§7).
357
+ */
358
+ resetConnection() {
359
+ this.seq = 0;
360
+ }
361
+ /**
362
+ * Rebuild the full semantic snapshot from live core state and hash it. Pure:
363
+ * no `seq`, no size guard, no dedup-state mutation — used for hash inspection
364
+ * and as the basis for {@link pollReply}.
365
+ */
366
+ rebuild() {
367
+ const content = this.buildContent();
368
+ return { content, hash: hash64(content) };
369
+ }
370
+ /**
371
+ * Build a `fleet/state` reply to an orchestrator `fleet/poll` (§7, task 011).
372
+ * The orchestrator drives the dedup: a FULL snapshot when the rebuilt hash
373
+ * differs from the poll's `knownHash` (or `knownHash` is null — no prior state /
374
+ * forced full), a hash-only reply otherwise. Always advances `seq`.
375
+ */
376
+ pollReply(reqId, knownHash) {
377
+ const { content, hash } = this.rebuild();
378
+ const seq = this.nextSeq();
379
+ if (knownHash !== null && hash === knownHash) {
380
+ const payload2 = { reqId, full: false, seq, hash, ...content };
381
+ return { kind: "state", full: false, hash, encodedBytes: 0, payload: payload2 };
382
+ }
383
+ const payload = { reqId, full: true, seq, hash, ...content };
384
+ const encodedBytes = encodeFrame(Topics.state, payload).length;
385
+ this.checkSize(encodedBytes, content.rooms.length);
386
+ return { kind: "state", full: true, hash, encodedBytes, payload };
387
+ }
388
+ nextSeq() {
389
+ this.seq += 1;
390
+ return this.seq;
391
+ }
392
+ buildContent() {
393
+ const manager = this.rivalis.rooms;
394
+ const roomTypes = [...manager.definitions()].sort();
395
+ const rooms = [];
396
+ for (const id of manager.keys()) {
397
+ const room = manager.get(id);
398
+ if (room === null) {
399
+ continue;
400
+ }
401
+ if (typeof room.type !== "string") {
402
+ throw new Error(this.coreSupportError(`room id=(${id}) has no string \`type\``));
403
+ }
404
+ rooms.push({
405
+ id,
406
+ type: room.type,
407
+ connections: room.actorCount,
408
+ origin: this.fleetOrigins.has(id) ? "fleet" : "local"
409
+ });
410
+ }
411
+ rooms.sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
412
+ return {
413
+ name: this.name,
414
+ processUid: this.processUid,
415
+ agentVersion: this.agentVersion,
416
+ protocolVersion: this.protocolVersion,
417
+ endpointUrl: this.endpointUrl,
418
+ labels: this.labels,
419
+ capacity: this.capacity,
420
+ autoCreate: this.autoCreate,
421
+ roomTypes,
422
+ rooms,
423
+ status: this.statusValue
424
+ };
425
+ }
426
+ checkSize(bytes, roomCount) {
427
+ const pct = Math.round(bytes / MAX_SNAPSHOT_BYTES * 100);
428
+ if (bytes >= MAX_SNAPSHOT_BYTES * ERROR_RATIO) {
429
+ this.logger.error(
430
+ `fleet snapshot at ${pct}% of the 4 MiB transport frame limit (${bytes} bytes, ${roomCount} rooms). An oversized snapshot is terminated by the transport, which causes a permanent reconnect loop. Remediation: host fewer rooms per instance, raise the orchestrator's WSTransport.maxPayload, or split the fleet across more instances (chunked sync is roadmap \xA716).`
431
+ );
432
+ } else if (bytes >= MAX_SNAPSHOT_BYTES * WARN_RATIO) {
433
+ this.logger.warning(
434
+ `fleet snapshot at ${pct}% of the 4 MiB transport frame limit (${bytes} bytes, ${roomCount} rooms) \u2014 approaching the size guard.`
435
+ );
436
+ }
437
+ }
438
+ /**
439
+ * Feature-detect the §4 core additions and throw an actionable, version-naming
440
+ * error when they are absent — a clean failure at startup instead of
441
+ * `undefined` types in snapshots at runtime. `Room.type` can only be checked
442
+ * against rooms that already exist; with zero rooms the `definitions()` gate
443
+ * is the primary guard (and `buildContent` re-checks each room defensively).
444
+ */
445
+ assertCoreSupport(rivalis) {
446
+ const manager = rivalis?.rooms;
447
+ if (manager === void 0 || manager === null) {
448
+ throw new Error(this.coreSupportError("rivalis.rooms is not available"));
449
+ }
450
+ if (typeof manager.definitions !== "function") {
451
+ throw new Error(this.coreSupportError("rivalis.rooms.definitions() is not available"));
452
+ }
453
+ if (typeof manager.keys !== "function" || typeof manager.get !== "function") {
454
+ throw new Error(this.coreSupportError("rivalis.rooms.keys()/get() are not available"));
455
+ }
456
+ for (const id of manager.keys()) {
457
+ const room = manager.get(id);
458
+ if (room !== null && typeof room.type !== "string") {
459
+ throw new Error(this.coreSupportError(`Room.type is not available (room id=(${id}) has no string \`type\`)`));
460
+ }
461
+ }
462
+ }
463
+ coreSupportError(detail) {
464
+ return `@rivalis/fleet requires @rivalis/core >= ${MIN_CORE_VERSION}: ${detail}. Upgrade @rivalis/core to >= ${MIN_CORE_VERSION} (the \xA74 additions: Room.type, RoomManager.definitions()).`;
465
+ }
466
+ };
467
+ // Annotate the CommonJS export names for ESM import in node:
468
+ 0 && (module.exports = {
469
+ MAX_SNAPSHOT_BYTES,
470
+ Snapshot
471
+ });
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/util/canonical.ts
21
+ var canonical_exports = {};
22
+ __export(canonical_exports, {
23
+ canonicalize: () => canonicalize,
24
+ hash64: () => hash64
25
+ });
26
+ module.exports = __toCommonJS(canonical_exports);
27
+ var import_node_crypto = require("crypto");
28
+ function canonicalize(value) {
29
+ return encode(value);
30
+ }
31
+ function encode(value) {
32
+ if (value === null) {
33
+ return "null";
34
+ }
35
+ const type = typeof value;
36
+ if (type === "string") {
37
+ return JSON.stringify(value);
38
+ }
39
+ if (type === "number") {
40
+ return Number.isFinite(value) ? String(value) : "null";
41
+ }
42
+ if (type === "boolean") {
43
+ return value ? "true" : "false";
44
+ }
45
+ if (type === "bigint") {
46
+ return value.toString();
47
+ }
48
+ if (Array.isArray(value)) {
49
+ const items = value.map((item) => encodeArrayItem(item));
50
+ return "[" + items.join(",") + "]";
51
+ }
52
+ if (type === "object") {
53
+ const obj = value;
54
+ const keys = Object.keys(obj).sort();
55
+ const parts = [];
56
+ for (const key of keys) {
57
+ const child = obj[key];
58
+ if (isSkippable(child)) {
59
+ continue;
60
+ }
61
+ parts.push(JSON.stringify(key) + ":" + encode(child));
62
+ }
63
+ return "{" + parts.join(",") + "}";
64
+ }
65
+ return "null";
66
+ }
67
+ function encodeArrayItem(item) {
68
+ return isSkippable(item) ? "null" : encode(item);
69
+ }
70
+ function isSkippable(value) {
71
+ const type = typeof value;
72
+ return value === void 0 || type === "function" || type === "symbol";
73
+ }
74
+ function hash64(value) {
75
+ const digest = (0, import_node_crypto.createHash)("sha256").update(canonicalize(value)).digest();
76
+ return digest.subarray(0, 8).toString("hex");
77
+ }
78
+ // Annotate the CommonJS export names for ESM import in node:
79
+ 0 && (module.exports = {
80
+ canonicalize,
81
+ hash64
82
+ });