@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.
@@ -0,0 +1,258 @@
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/CommandEngine.ts
21
+ var CommandEngine_exports = {};
22
+ __export(CommandEngine_exports, {
23
+ CommandEngine: () => CommandEngine
24
+ });
25
+ module.exports = __toCommonJS(CommandEngine_exports);
26
+
27
+ // src/domain/roomId.ts
28
+ var ROOM_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
29
+
30
+ // src/domain/roomCreate.ts
31
+ var roomCreateSchema = {
32
+ type: { type: "string", required: true, min: 1 },
33
+ roomId: { type: "string", pattern: ROOM_ID_PATTERN.source },
34
+ placement: { type: "object" }
35
+ };
36
+
37
+ // src/domain/errors.ts
38
+ var import_node = require("@toolcase/node");
39
+ var CODE_TO_STATUS = {
40
+ VALIDATION: 400,
41
+ UNAUTHORIZED: 401,
42
+ INSTANCE_NOT_FOUND: 404,
43
+ ROOM_NOT_FOUND: 404,
44
+ NO_CANDIDATE: 409,
45
+ ROOM_EXISTS: 409,
46
+ INSTANCE_DRAINING: 409,
47
+ PAYLOAD_TOO_LARGE: 413,
48
+ INSTANCE_BUSY: 429,
49
+ AUTH_THROTTLED: 429,
50
+ SSE_LIMIT: 429,
51
+ COMMAND_FAILED: 502,
52
+ INSTANCE_DISCONNECTED: 502,
53
+ COMMAND_TIMEOUT: 504
54
+ };
55
+ var FleetError = class extends import_node.EndpointError {
56
+ constructor(code, message) {
57
+ super(CODE_TO_STATUS[code], code, message);
58
+ this.name = "FleetError";
59
+ }
60
+ };
61
+
62
+ // src/wire/topics.ts
63
+ var MAX_INFLIGHT_COMMANDS = 32;
64
+ var Topics = {
65
+ /** orch → agent: assigns id + heartbeat (poll cadence) on join; followed by the first poll. */
66
+ hello: "fleet/hello",
67
+ /** orch → agent: state poll. Carries `knownHash` (dedup) + the last recorded `status` (echo). */
68
+ poll: "fleet/poll",
69
+ /** agent → orch: poll reply. Full snapshot when the hash differs from `knownHash`, hash-only otherwise. */
70
+ state: "fleet/state",
71
+ /** orch → agent: command push. */
72
+ cmd: "fleet/cmd",
73
+ /** agent → orch: command result. */
74
+ ack: "fleet/ack"
75
+ };
76
+
77
+ // src/wire/serializer.ts
78
+ var import_node_module = require("module");
79
+ var Type = {
80
+ Label: "Label",
81
+ SyncRoom: "SyncRoom",
82
+ Capacity: "Capacity",
83
+ AckRoom: "AckRoom",
84
+ Hello: "Hello",
85
+ Poll: "Poll",
86
+ State: "State",
87
+ Cmd: "Cmd",
88
+ Ack: "Ack"
89
+ };
90
+ var TOPIC_TYPE = {
91
+ [Topics.hello]: Type.Hello,
92
+ [Topics.poll]: Type.Poll,
93
+ [Topics.state]: Type.State,
94
+ [Topics.cmd]: Type.Cmd,
95
+ [Topics.ack]: Type.Ack
96
+ };
97
+
98
+ // src/util/errors.ts
99
+ function describe(error) {
100
+ return error instanceof Error ? error.message : String(error);
101
+ }
102
+
103
+ // src/orchestrator/CommandEngine.ts
104
+ var CommandEngine = class {
105
+ constructor(scheduler, reservations, commandTimeoutMs) {
106
+ this.scheduler = scheduler;
107
+ this.reservations = reservations;
108
+ this.commandTimeoutMs = commandTimeoutMs;
109
+ }
110
+ scheduler;
111
+ reservations;
112
+ commandTimeoutMs;
113
+ /** Pending commands keyed by instance id, then by `cmdId`. */
114
+ pending = /* @__PURE__ */ new Map();
115
+ cmdSeq = 0;
116
+ /** Monotonic command id (`cmd_N`) — connection-agnostic, unique per orchestrator. */
117
+ nextCmdId() {
118
+ return `cmd_${++this.cmdSeq}`;
119
+ }
120
+ /** How many commands are currently in flight for an instance. */
121
+ inFlight(instanceId) {
122
+ return this.pending.get(instanceId)?.size ?? 0;
123
+ }
124
+ /**
125
+ * Push a `fleet/cmd` and return a promise that resolves on its `fleet/ack`
126
+ * (rejects on `COMMAND_FAILED`), or rejects on timeout (`COMMAND_TIMEOUT`) /
127
+ * disconnect (`INSTANCE_DISCONNECTED`). Caps in-flight commands per instance at
128
+ * {@link MAX_INFLIGHT_COMMANDS} → `INSTANCE_BUSY` rather than queueing unbounded
129
+ * promises behind a slow agent (§7). Reservations (create only) ride on the
130
+ * pending entry and are released on every settle path.
131
+ */
132
+ send(link, cmd, reservation = null, roomIdReservation = null) {
133
+ const map = this.mapFor(link.instanceId);
134
+ if (map.size >= MAX_INFLIGHT_COMMANDS) {
135
+ if (reservation !== null) {
136
+ this.reservations.release(reservation);
137
+ }
138
+ if (roomIdReservation !== null) {
139
+ this.reservations.releaseRoomId(roomIdReservation);
140
+ }
141
+ return Promise.reject(new FleetError(
142
+ "INSTANCE_BUSY",
143
+ `instance ${link.instanceId} has ${map.size} commands in flight (max ${MAX_INFLIGHT_COMMANDS})`
144
+ ));
145
+ }
146
+ return new Promise((resolve, reject) => {
147
+ const timer = this.scheduler.setTimeout(() => {
148
+ this.settle(link.instanceId, cmd.cmdId, (pending) => {
149
+ this.holdOrRelease(pending);
150
+ pending.reject(new FleetError(
151
+ "COMMAND_TIMEOUT",
152
+ `command ${cmd.cmdId} (${cmd.op}) timed out after ${this.commandTimeoutMs}ms`
153
+ ));
154
+ });
155
+ }, this.commandTimeoutMs);
156
+ map.set(cmd.cmdId, { resolve, reject, timer, reservation, roomIdReservation });
157
+ try {
158
+ link.send(Topics.cmd, cmd);
159
+ } catch (error) {
160
+ this.settle(link.instanceId, cmd.cmdId, (pending) => {
161
+ this.releaseReservations(pending);
162
+ pending.reject(new FleetError(
163
+ "INSTANCE_DISCONNECTED",
164
+ `failed to send command ${cmd.cmdId} (${cmd.op}) to instance ${link.instanceId}: ${describe(error)}`
165
+ ));
166
+ });
167
+ }
168
+ });
169
+ }
170
+ /**
171
+ * Resolve/reject the originating promise for an inbound `fleet/ack`. Returns
172
+ * `false` when no such pending exists (a late ack after a timeout, or an unknown
173
+ * cmd) so the caller can log-and-drop — never a double-resolve (§14).
174
+ */
175
+ ack(instanceId, ack) {
176
+ return this.settle(instanceId, ack.cmdId, (pending) => {
177
+ if (ack.ok) {
178
+ this.holdOrRelease(pending);
179
+ pending.resolve(ack);
180
+ } else {
181
+ this.releaseReservations(pending);
182
+ pending.reject(ack.exists === true ? new FleetError("ROOM_EXISTS", ack.error ?? "room id already exists") : new FleetError("COMMAND_FAILED", ack.error ?? "agent reported command failure"));
183
+ }
184
+ });
185
+ }
186
+ /**
187
+ * Reject every in-flight command for a disconnected/evicted instance immediately
188
+ * with `INSTANCE_DISCONNECTED` — callers never wait out `commandTimeoutMs` for an
189
+ * instance the orchestrator already knows is gone (§7).
190
+ */
191
+ rejectAll(instanceId, reason) {
192
+ const map = this.pending.get(instanceId);
193
+ if (map === void 0) {
194
+ return;
195
+ }
196
+ for (const cmdId of [...map.keys()]) {
197
+ this.settle(instanceId, cmdId, (pending) => {
198
+ this.releaseReservations(pending);
199
+ pending.reject(new FleetError("INSTANCE_DISCONNECTED", `instance ${instanceId} disconnected (${reason})`));
200
+ });
201
+ }
202
+ this.pending.delete(instanceId);
203
+ }
204
+ /**
205
+ * Settle exactly one pending command: delete it and clear its timer, then run
206
+ * `action` (which disposes the reservations — release or {@link holdOrRelease} —
207
+ * and resolves/rejects). Returns `false` when no such pending exists (already
208
+ * settled) — the single guard against double-resolve from a timeout-then-late-ack
209
+ * or disconnect-then-ack race (§14). Reservation disposition moved into the per-path
210
+ * `action` callbacks (task 003): ack-OK / timeout hold until visible, every other
211
+ * path releases.
212
+ */
213
+ settle(instanceId, cmdId, action) {
214
+ const map = this.pending.get(instanceId);
215
+ const pending = map?.get(cmdId);
216
+ if (map === void 0 || pending === void 0) {
217
+ return false;
218
+ }
219
+ map.delete(cmdId);
220
+ this.scheduler.clearTimeout(pending.timer);
221
+ action(pending);
222
+ return true;
223
+ }
224
+ /**
225
+ * Hold a create's reservations until its room is visible (task 003) — used on
226
+ * ack-OK and timeout. A create carries BOTH a capacity and a room-id reservation;
227
+ * any other command (destroy/drain/undrain) carries neither, so this degrades to a
228
+ * release of whatever (if anything) is present.
229
+ */
230
+ holdOrRelease(pending) {
231
+ if (pending.reservation !== null && pending.roomIdReservation !== null) {
232
+ this.reservations.holdUntilVisible(pending.roomIdReservation, pending.reservation);
233
+ } else {
234
+ this.releaseReservations(pending);
235
+ }
236
+ }
237
+ /** Release a settled command's reservations immediately (failure / disconnect / busy). */
238
+ releaseReservations(pending) {
239
+ if (pending.reservation !== null) {
240
+ this.reservations.release(pending.reservation);
241
+ }
242
+ if (pending.roomIdReservation !== null) {
243
+ this.reservations.releaseRoomId(pending.roomIdReservation);
244
+ }
245
+ }
246
+ mapFor(instanceId) {
247
+ let map = this.pending.get(instanceId);
248
+ if (map === void 0) {
249
+ map = /* @__PURE__ */ new Map();
250
+ this.pending.set(instanceId, map);
251
+ }
252
+ return map;
253
+ }
254
+ };
255
+ // Annotate the CommonJS export names for ESM import in node:
256
+ 0 && (module.exports = {
257
+ CommandEngine
258
+ });
@@ -0,0 +1,90 @@
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/EventReconciler.ts
21
+ var EventReconciler_exports = {};
22
+ __export(EventReconciler_exports, {
23
+ EventReconciler: () => EventReconciler
24
+ });
25
+ module.exports = __toCommonJS(EventReconciler_exports);
26
+ var EventReconciler = class {
27
+ constructor(state, emit) {
28
+ this.state = state;
29
+ this.emit = emit;
30
+ }
31
+ state;
32
+ emit;
33
+ knownInstanceIds = /* @__PURE__ */ new Set();
34
+ knownRooms = /* @__PURE__ */ new Map();
35
+ lastStatsHash = "";
36
+ /**
37
+ * Diff the read model and emit the derived events: `instance:join` for a new
38
+ * instance, `room:create`/`room:destroy` for room churn, and `sync` whenever the
39
+ * semantic `stateHash` changes. `instance:leave` is emitted by
40
+ * {@link instanceRemoved}, not here.
41
+ */
42
+ reconcile() {
43
+ const instances = this.state.instances;
44
+ const currentInstanceIds = /* @__PURE__ */ new Set();
45
+ const currentRoomIds = /* @__PURE__ */ new Set();
46
+ for (const instance of instances) {
47
+ currentInstanceIds.add(instance.id);
48
+ if (!this.knownInstanceIds.has(instance.id)) {
49
+ this.knownInstanceIds.add(instance.id);
50
+ this.emit("instance:join", instance);
51
+ }
52
+ for (const room of instance.rooms) {
53
+ currentRoomIds.add(room.id);
54
+ if (!this.knownRooms.has(room.id)) {
55
+ this.knownRooms.set(room.id, room);
56
+ this.emit("room:create", room);
57
+ }
58
+ }
59
+ }
60
+ for (const [roomId, room] of [...this.knownRooms]) {
61
+ if (!currentRoomIds.has(roomId)) {
62
+ this.knownRooms.delete(roomId);
63
+ this.emit("room:destroy", room);
64
+ }
65
+ }
66
+ for (const id of [...this.knownInstanceIds]) {
67
+ if (!currentInstanceIds.has(id)) {
68
+ this.knownInstanceIds.delete(id);
69
+ }
70
+ }
71
+ const stats = this.state.stats;
72
+ if (stats.stateHash !== this.lastStatsHash) {
73
+ this.lastStatsHash = stats.stateHash;
74
+ this.emit("sync", stats);
75
+ }
76
+ }
77
+ /**
78
+ * An instance was removed from the read model (socket close or eviction): forget
79
+ * it and emit `instance:leave`. The caller follows with a {@link reconcile} so the
80
+ * vanished instance's rooms surface as `room:destroy` and the `sync` fires.
81
+ */
82
+ instanceRemoved(removed) {
83
+ this.knownInstanceIds.delete(removed.id);
84
+ this.emit("instance:leave", removed);
85
+ }
86
+ };
87
+ // Annotate the CommonJS export names for ESM import in node:
88
+ 0 && (module.exports = {
89
+ EventReconciler
90
+ });