@hotmeshio/long-tail 0.25.4 → 0.26.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.
Files changed (139) hide show
  1. package/README.md +1 -0
  2. package/build/index.d.ts +1 -0
  3. package/build/index.js +8 -1
  4. package/build/lib/machines/index.d.ts +5 -0
  5. package/build/lib/machines/index.js +21 -0
  6. package/build/lib/machines/line.d.ts +44 -0
  7. package/build/lib/machines/line.js +114 -0
  8. package/build/lib/machines/protocol/codec.d.ts +92 -0
  9. package/build/lib/machines/protocol/codec.js +79 -0
  10. package/build/lib/machines/protocol/constants.d.ts +120 -0
  11. package/build/lib/machines/protocol/constants.js +97 -0
  12. package/build/lib/machines/protocol/contract.d.ts +19 -0
  13. package/build/lib/machines/protocol/contract.js +132 -0
  14. package/build/lib/machines/protocol/envelope.d.ts +14 -0
  15. package/build/lib/machines/protocol/envelope.js +43 -0
  16. package/build/lib/machines/protocol/index.d.ts +5 -0
  17. package/build/lib/machines/protocol/index.js +21 -0
  18. package/build/lib/machines/protocol/topics.d.ts +33 -0
  19. package/build/lib/machines/protocol/topics.js +79 -0
  20. package/build/lib/machines/site/mosquitto-conf.d.ts +27 -0
  21. package/build/lib/machines/site/mosquitto-conf.js +81 -0
  22. package/build/lib/machines/site/site-conf.d.ts +20 -0
  23. package/build/lib/machines/site/site-conf.js +55 -0
  24. package/build/lib/machines/transport/callback-feed.d.ts +18 -0
  25. package/build/lib/machines/transport/callback-feed.js +31 -0
  26. package/build/lib/machines/transport/delivery.d.ts +21 -0
  27. package/build/lib/machines/transport/delivery.js +42 -0
  28. package/build/lib/machines/transport/index.d.ts +11 -0
  29. package/build/lib/machines/transport/index.js +27 -0
  30. package/build/lib/machines/transport/memory-broker.d.ts +35 -0
  31. package/build/lib/machines/transport/memory-broker.js +145 -0
  32. package/build/lib/machines/transport/memory.d.ts +40 -0
  33. package/build/lib/machines/transport/memory.js +79 -0
  34. package/build/lib/machines/transport/mqtt-bus.d.ts +24 -0
  35. package/build/lib/machines/transport/mqtt-bus.js +43 -0
  36. package/build/lib/machines/transport/mqtt-link.d.ts +16 -0
  37. package/build/lib/machines/transport/mqtt-link.js +37 -0
  38. package/build/lib/machines/transport/mqtt-session.d.ts +34 -0
  39. package/build/lib/machines/transport/mqtt-session.js +127 -0
  40. package/build/lib/machines/transport/nats-bus.d.ts +42 -0
  41. package/build/lib/machines/transport/nats-bus.js +67 -0
  42. package/build/lib/machines/transport/nats-durable.d.ts +11 -0
  43. package/build/lib/machines/transport/nats-durable.js +28 -0
  44. package/build/lib/machines/transport/nats-feed.d.ts +26 -0
  45. package/build/lib/machines/transport/nats-feed.js +49 -0
  46. package/build/lib/machines/transport/serial.d.ts +21 -0
  47. package/build/lib/machines/transport/serial.js +54 -0
  48. package/build/lib/machines/transport/streams.d.ts +28 -0
  49. package/build/lib/machines/transport/streams.js +92 -0
  50. package/build/scripts/machine-line.d.ts +33 -0
  51. package/build/scripts/machine-line.js +301 -0
  52. package/build/services/machines/commands.d.ts +29 -0
  53. package/build/services/machines/commands.js +37 -0
  54. package/build/services/machines/config.d.ts +43 -0
  55. package/build/services/machines/config.js +84 -0
  56. package/build/services/machines/contracts.d.ts +193 -0
  57. package/build/services/machines/contracts.js +115 -0
  58. package/build/services/machines/forms.d.ts +55 -0
  59. package/build/services/machines/forms.js +37 -0
  60. package/build/services/machines/gateway/actions.d.ts +111 -0
  61. package/build/services/machines/gateway/actions.js +8 -0
  62. package/build/services/machines/gateway/address-book.d.ts +16 -0
  63. package/build/services/machines/gateway/address-book.js +24 -0
  64. package/build/services/machines/gateway/admission.d.ts +35 -0
  65. package/build/services/machines/gateway/admission.js +66 -0
  66. package/build/services/machines/gateway/api-escalations.d.ts +23 -0
  67. package/build/services/machines/gateway/api-escalations.js +101 -0
  68. package/build/services/machines/gateway/dispatcher.d.ts +63 -0
  69. package/build/services/machines/gateway/dispatcher.js +157 -0
  70. package/build/services/machines/gateway/escalations.d.ts +51 -0
  71. package/build/services/machines/gateway/escalations.js +40 -0
  72. package/build/services/machines/gateway/executor.d.ts +55 -0
  73. package/build/services/machines/gateway/executor.js +195 -0
  74. package/build/services/machines/gateway/gateway.d.ts +82 -0
  75. package/build/services/machines/gateway/gateway.js +247 -0
  76. package/build/services/machines/gateway/index.d.ts +10 -0
  77. package/build/services/machines/gateway/index.js +26 -0
  78. package/build/services/machines/gateway/jobs.d.ts +16 -0
  79. package/build/services/machines/gateway/jobs.js +24 -0
  80. package/build/services/machines/gateway/log.d.ts +41 -0
  81. package/build/services/machines/gateway/log.js +42 -0
  82. package/build/services/machines/gateway/lt-http.d.ts +51 -0
  83. package/build/services/machines/gateway/lt-http.js +120 -0
  84. package/build/services/machines/gateway/pacing.d.ts +29 -0
  85. package/build/services/machines/gateway/pacing.js +134 -0
  86. package/build/services/machines/gateway/plan-escalation.d.ts +19 -0
  87. package/build/services/machines/gateway/plan-escalation.js +201 -0
  88. package/build/services/machines/gateway/plan-machine.d.ts +16 -0
  89. package/build/services/machines/gateway/plan-machine.js +242 -0
  90. package/build/services/machines/gateway/plan-shared.d.ts +18 -0
  91. package/build/services/machines/gateway/plan-shared.js +36 -0
  92. package/build/services/machines/gateway/provision.d.ts +22 -0
  93. package/build/services/machines/gateway/provision.js +79 -0
  94. package/build/services/machines/gateway/receipts.d.ts +33 -0
  95. package/build/services/machines/gateway/receipts.js +65 -0
  96. package/build/services/machines/gateway/reconcile.d.ts +33 -0
  97. package/build/services/machines/gateway/reconcile.js +86 -0
  98. package/build/services/machines/gateway/registration.d.ts +38 -0
  99. package/build/services/machines/gateway/registration.js +121 -0
  100. package/build/services/machines/gateway/reply-waiter.d.ts +24 -0
  101. package/build/services/machines/gateway/reply-waiter.js +65 -0
  102. package/build/services/machines/gateway/state.d.ts +125 -0
  103. package/build/services/machines/gateway/state.js +228 -0
  104. package/build/services/machines/gateway/trace.d.ts +16 -0
  105. package/build/services/machines/gateway/trace.js +43 -0
  106. package/build/services/machines/identities.d.ts +25 -0
  107. package/build/services/machines/identities.js +77 -0
  108. package/build/services/machines/index.d.ts +11 -0
  109. package/build/services/machines/index.js +27 -0
  110. package/build/services/machines/leadership.d.ts +22 -0
  111. package/build/services/machines/leadership.js +75 -0
  112. package/build/services/machines/roles.d.ts +13 -0
  113. package/build/services/machines/roles.js +62 -0
  114. package/build/services/machines/rows.d.ts +30 -0
  115. package/build/services/machines/rows.js +56 -0
  116. package/build/services/machines/select.d.ts +18 -0
  117. package/build/services/machines/select.js +63 -0
  118. package/build/services/machines/surface.d.ts +33 -0
  119. package/build/services/machines/surface.js +98 -0
  120. package/build/services/machines/testing/fake-long-tail.d.ts +78 -0
  121. package/build/services/machines/testing/fake-long-tail.js +155 -0
  122. package/build/services/machines/testing/index.d.ts +3 -0
  123. package/build/services/machines/testing/index.js +19 -0
  124. package/build/services/machines/testing/recording-log.d.ts +14 -0
  125. package/build/services/machines/testing/recording-log.js +22 -0
  126. package/build/services/machines/testing/wait.d.ts +3 -0
  127. package/build/services/machines/testing/wait.js +17 -0
  128. package/build/services/mcp/db-server/schemas.d.ts +2 -2
  129. package/build/start/index.js +13 -0
  130. package/build/system/mcp-servers/admin/schemas.d.ts +221 -221
  131. package/build/system/mcp-servers/db-query/schemas.d.ts +2 -2
  132. package/build/system/mcp-servers/human-queue-schemas.d.ts +6 -6
  133. package/build/tsconfig.tsbuildinfo +1 -1
  134. package/build/types/index.d.ts +2 -1
  135. package/build/types/machines.d.ts +166 -0
  136. package/build/types/machines.js +7 -0
  137. package/build/types/startup.d.ts +23 -0
  138. package/docs/machines.md +293 -0
  139. package/package.json +11 -1
@@ -0,0 +1,195 @@
1
+ "use strict";
2
+ /**
3
+ * Runs the planner's actions: escalation verbs through LineEscalations,
4
+ * dispatches and commands through the Dispatcher, alarm and machine rows,
5
+ * and the offline debounce. A verb that fails for real throws, so the message that
6
+ * caused it is delivered again.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.Executor = void 0;
10
+ const contracts_1 = require("../contracts");
11
+ const line_1 = require("../../../lib/machines/line");
12
+ const protocol_1 = require("../../../lib/machines/protocol");
13
+ const actions_1 = require("./actions");
14
+ const escalations_1 = require("./escalations");
15
+ const log_1 = require("./log");
16
+ const dispatcher_1 = require("./dispatcher");
17
+ const plan_shared_1 = require("./plan-shared");
18
+ const NOT_FOUND = 404;
19
+ const CONFLICT = 409;
20
+ class Executor {
21
+ constructor(d) {
22
+ this.d = d;
23
+ this.inflight = new Set();
24
+ this.quietPending = new Set();
25
+ this.timers = new Set();
26
+ this.dispatcher = new dispatcher_1.Dispatcher(d);
27
+ }
28
+ /** Dispatches and commands run detached so one slow reply never holds up the feed. */
29
+ async apply(actions) {
30
+ for (const action of actions) {
31
+ if (action.kind === 'dispatch')
32
+ this.track(this.dispatcher.dispatch(action));
33
+ else if (action.kind === 'command')
34
+ this.track(this.dispatcher.command(action));
35
+ else if (action.kind === 'control')
36
+ this.track(this.dispatcher.control(action));
37
+ else
38
+ await this.run(action);
39
+ }
40
+ }
41
+ /** Resolves when every detached dispatch has finished. */
42
+ async idle() {
43
+ while (this.inflight.size)
44
+ await Promise.all([...this.inflight]);
45
+ }
46
+ stop() {
47
+ for (const t of this.timers)
48
+ clearTimeout(t);
49
+ this.timers.clear();
50
+ this.d.replies.cancelAll();
51
+ }
52
+ track(work) {
53
+ const tracked = work.catch((err) => this.d.log.error(log_1.GATEWAY_LOG.VERB_FAILED, { error: String(err?.message ?? err) }))
54
+ .finally(() => this.inflight.delete(tracked));
55
+ this.inflight.add(tracked);
56
+ }
57
+ async run(a) {
58
+ const { lt, state, replies } = this.d;
59
+ switch (a.kind) {
60
+ case 'claim':
61
+ return this.check(a, await lt.claim(a.as, a.rowId));
62
+ case 'accumulate':
63
+ return this.accumulate(a);
64
+ case 'resolve':
65
+ this.check(a, await lt.resolve(a.as, a.rowId, a.payload));
66
+ state.close(a.rowId);
67
+ return;
68
+ case 'createAlarmRow':
69
+ return this.createAlarmRow(a);
70
+ case 'createRow': {
71
+ const { id } = await lt.create(actions_1.CONTROLLER, a.row);
72
+ this.d.log.info(log_1.GATEWAY_LOG.VIOLATION_ROW, { row: id, type: a.row.type });
73
+ return;
74
+ }
75
+ case 'watchOffline':
76
+ return this.watchOffline(a.machineId);
77
+ case 'settleReply':
78
+ if (!replies.settle({ cmdId: a.cmdId, status: a.status, reason: a.reason }))
79
+ await this.lateReply(a);
80
+ return;
81
+ case 'settleSnapshot':
82
+ replies.settleSnapshot(a.machineId, new Set(a.jobIds));
83
+ return;
84
+ case 'bindAddress':
85
+ await this.d.addresses.bind(a.machineId, a.address);
86
+ state.bindAddress(a.machineId, a.address);
87
+ this.d.log.info(log_1.GATEWAY_LOG.ADDRESS_BOUND, { machine: a.machineId, address: a.address, ...(a.by ? { approvedBy: a.by } : {}) });
88
+ return;
89
+ case 'refuseBinding':
90
+ this.d.log.warn(log_1.GATEWAY_LOG.ADDRESS_REFUSED, { machine: a.machineId, address: a.address, reason: a.reason });
91
+ }
92
+ }
93
+ /** A dispatch's second reply: ready, or a failure to prepare that ends the handoff. */
94
+ async lateReply(a) {
95
+ const row = this.d.state.rowForDispatch(a.cmdId);
96
+ if (!row?.toMachine)
97
+ return;
98
+ const context = { row: row.id, machine: row.toMachine, cmdId: a.cmdId, reason: a.reason };
99
+ if (a.status === protocol_1.REPLY_STATUS.COMPLETED)
100
+ return this.d.log.info(log_1.GATEWAY_LOG.DISPATCH_READY, context);
101
+ if (a.status !== protocol_1.REPLY_STATUS.FAILED)
102
+ return;
103
+ this.d.log.warn(log_1.GATEWAY_LOG.DISPATCH_FAILED, context);
104
+ await this.apply((0, plan_shared_1.failHandoff)(this.d.state, row, row.toMachine, {
105
+ received: false, errorCode: contracts_1.HANDOFF_ERROR.DISPATCH_FAILED, detail: `${row.toMachine} could not prepare the job: ${a.reason}`,
106
+ }));
107
+ }
108
+ /** Add the item; when the row then holds every key the action waits for, resolve it. */
109
+ async accumulate(a) {
110
+ const { lt, state } = this.d;
111
+ const result = await lt.accumulate(a.as, a.rowId, a.itemKey, a.payload, a.metadata);
112
+ this.check(a, result);
113
+ if (!a.complete || result.outcome === escalations_1.VERB_OUTCOME.INVALID)
114
+ return;
115
+ const held = new Set(result.itemKeys ?? await lt.itemKeys(a.rowId));
116
+ if (!a.complete.keys.every((k) => held.has(k)))
117
+ return;
118
+ if (a.complete.then)
119
+ return this.apply(a.complete.then);
120
+ this.check(a, await lt.resolve(a.as, a.rowId, a.complete.payload ?? {}));
121
+ state.close(a.rowId);
122
+ }
123
+ check(a, result) {
124
+ const rowId = a.rowId;
125
+ if (result.outcome === escalations_1.VERB_OUTCOME.INVALID) {
126
+ this.d.log.error(log_1.GATEWAY_LOG.VERB_INVALID, { action: a.kind, row: rowId, detail: result.detail });
127
+ }
128
+ // Refused on a row the gateway holds open: outside the actor's roles (404) or claimed by someone else (409).
129
+ const open = rowId ? this.d.state.get(rowId) : undefined;
130
+ if (open && (result.status === NOT_FOUND || result.status === CONFLICT)) {
131
+ const event = result.status === NOT_FOUND ? log_1.GATEWAY_LOG.ROW_NOT_VISIBLE : log_1.GATEWAY_LOG.ROW_CLAIMED_ELSEWHERE;
132
+ this.d.log.warn(event, { action: a.kind, row: rowId, as: a.as, role: open.role, detail: result.detail });
133
+ }
134
+ }
135
+ async createAlarmRow(a) {
136
+ const { id } = await this.d.lt.create(actions_1.CONTROLLER, { ...a.row, role: (0, line_1.operatorRole)(this.d.line) });
137
+ this.d.log.info(log_1.GATEWAY_LOG.ALARM_ROW, { row: id, machine: a.machineId, alarm: a.alarmId });
138
+ const track = this.d.state.alarms.get(this.d.state.alarmKey(a.machineId, a.alarmId));
139
+ const raisedAt = a.row.metadata?.raisedAt;
140
+ if (track && (!track.raisedAt || !raisedAt || track.raisedAt === raisedAt)) {
141
+ track.rowId = id;
142
+ return;
143
+ }
144
+ // Cleared at the machine, or raised again, while the row was being created.
145
+ const note = track ? `${a.machineId} raised ${a.alarmId} again; this one ended` : `cleared at ${a.machineId}`;
146
+ await this.d.lt.resolve(actions_1.CONTROLLER, id, { decision: contracts_1.DECISION.CLEARED, note });
147
+ }
148
+ /** Raise a quiet row for each connected machine silent longer than `quietMs`. */
149
+ checkQuiet(machineIds, quietMs) {
150
+ const now = Date.now();
151
+ for (const machineId of machineIds) {
152
+ const link = this.d.state.link(machineId);
153
+ if (!link.online || link.quietRowId || this.quietPending.has(machineId) || now - link.lastSeen < quietMs)
154
+ continue;
155
+ this.quietPending.add(machineId);
156
+ this.track(this.raiseQuiet(machineId, now - link.lastSeen).finally(() => this.quietPending.delete(machineId)));
157
+ }
158
+ }
159
+ async raiseQuiet(machineId, silentMs) {
160
+ const { id } = await this.d.lt.create(actions_1.CONTROLLER, {
161
+ type: contracts_1.ROW_TYPE.MACHINE_QUIET, role: (0, line_1.operatorRole)(this.d.line), priority: 2,
162
+ description: `${machineId} is connected but has sent nothing for ${Math.round(silentMs / 1000)} s`,
163
+ metadata: { siteId: this.d.line.siteId, lineId: this.d.line.lineId, machineId, silentMs },
164
+ });
165
+ this.d.log.info(log_1.GATEWAY_LOG.QUIET_ROW, { row: id, machine: machineId });
166
+ this.d.state.link(machineId).quietRowId = id;
167
+ }
168
+ watchOffline(machineId) {
169
+ const timer = setTimeout(() => {
170
+ this.timers.delete(timer);
171
+ this.track(this.raiseOffline(machineId));
172
+ }, this.d.offlineDebounceMs);
173
+ this.timers.add(timer);
174
+ }
175
+ async raiseOffline(machineId) {
176
+ const link = this.d.state.links.get(machineId);
177
+ if (!link)
178
+ return;
179
+ link.watching = false;
180
+ if (link.online || link.rowId)
181
+ return;
182
+ const since = new Date(link.since).toISOString();
183
+ const { id } = await this.d.lt.create(actions_1.CONTROLLER, {
184
+ type: contracts_1.ROW_TYPE.MACHINE_OFFLINE, role: (0, line_1.operatorRole)(this.d.line), priority: 2,
185
+ description: `${machineId} has been offline since ${since}`,
186
+ metadata: { siteId: this.d.line.siteId, lineId: this.d.line.lineId, machineId, offlineSince: since },
187
+ });
188
+ this.d.log.info(log_1.GATEWAY_LOG.OFFLINE_ROW, { row: id, machine: machineId });
189
+ if (!link.online)
190
+ link.rowId = id;
191
+ else
192
+ await this.d.lt.resolve(actions_1.CONTROLLER, id, { decision: contracts_1.DECISION.RESTORED, note: `${machineId} back online` });
193
+ }
194
+ }
195
+ exports.Executor = Executor;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * The machine gateway: the seam between machines and escalation rows. It
3
+ * reads escalation events (a new handoff row means "dispatch"), reads every
4
+ * machine message on the line (a receipt means "resolve"), and performs the
5
+ * verbs a person would, as the machine. Neither side knows about the other.
6
+ */
7
+ import { type HandoffRowAdapter } from '../rows';
8
+ import { type AddressBook } from './address-book';
9
+ import type { LineDef } from '../../../lib/machines/line';
10
+ import { type LineCodec } from '../../../lib/machines/protocol';
11
+ import { type EscalationFeed, type MachineBus } from '../../../lib/machines/transport';
12
+ import type { LineEscalations } from './escalations';
13
+ import { type GatewayLog } from './log';
14
+ import { GatewayState } from './state';
15
+ export interface GatewaySettings {
16
+ topicRoot: string;
17
+ replyTimeoutMs: number;
18
+ dispatchAttempts: number;
19
+ offlineDebounceMs: number;
20
+ /** How long a dispatch stays valid at the machine (expiresAt). */
21
+ dispatchTtlMs: number;
22
+ /** A connected machine silent this long gets a quiet row; 0 turns the check off. */
23
+ quietMs: number;
24
+ /** How long confirmed message ids gather before a receipt goes out (codecs with receipts). */
25
+ receiptDelayMs?: number;
26
+ }
27
+ export interface GatewayDeps {
28
+ bus: MachineBus;
29
+ feed: EscalationFeed;
30
+ lt: LineEscalations;
31
+ line: LineDef;
32
+ settings: GatewaySettings;
33
+ log?: GatewayLog;
34
+ /** Reads a host's rows into handoffs; the standard handoff facets by default. */
35
+ rows?: HandoffRowAdapter;
36
+ /** The machines' wire protocol; the line protocol under `settings.topicRoot` by default. */
37
+ codec?: LineCodec;
38
+ /** Where a line that learns addresses keeps them; in memory by default. */
39
+ addresses?: AddressBook;
40
+ }
41
+ export declare class MachineGateway {
42
+ private readonly deps;
43
+ readonly state: GatewayState;
44
+ private readonly rows;
45
+ private readonly codec;
46
+ private readonly executor;
47
+ private readonly receipts;
48
+ private readonly log;
49
+ private readonly addresses;
50
+ private readonly admission;
51
+ private quietTimer?;
52
+ constructor(deps: GatewayDeps);
53
+ start(): Promise<void>;
54
+ stop(): Promise<void>;
55
+ /** Resolves when no dispatch is in flight (tests). */
56
+ idle(): Promise<void>;
57
+ /**
58
+ * On taking the line: open handoff and command rows (a dispatch or command
59
+ * interrupted by a restart is sent again with the same cmdId), the gateway's
60
+ * own alarm, offline and quiet rows, then the machines' retained state to
61
+ * catch what changed while no gateway was listening.
62
+ */
63
+ private reconcile;
64
+ /** Send waiting handoffs to machines that have room (pacing.ts). */
65
+ private pace;
66
+ private reconcileCommands;
67
+ private reconcileHandoffs;
68
+ private resync;
69
+ private onEscalation;
70
+ /**
71
+ * An event only says which row to look at. The row is read back as the
72
+ * controller, so a row on a role the line does not hold, or an event that
73
+ * disagrees with the stored row, does nothing.
74
+ */
75
+ private verify;
76
+ private onMachine;
77
+ /** A message the codec cannot read is not retried: it is logged and has no effect. */
78
+ private decode;
79
+ /** Done with the message: confirm each id its codec asked to confirm. */
80
+ private confirm;
81
+ private applyMachine;
82
+ }
@@ -0,0 +1,247 @@
1
+ "use strict";
2
+ /**
3
+ * The machine gateway: the seam between machines and escalation rows. It
4
+ * reads escalation events (a new handoff row means "dispatch"), reads every
5
+ * machine message on the line (a receipt means "resolve"), and performs the
6
+ * verbs a person would, as the machine. Neither side knows about the other.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.MachineGateway = void 0;
10
+ const rows_1 = require("../rows");
11
+ const contracts_1 = require("../contracts");
12
+ const actions_1 = require("./actions");
13
+ const reconcile_1 = require("./reconcile");
14
+ const address_book_1 = require("./address-book");
15
+ const admission_1 = require("./admission");
16
+ const protocol_1 = require("../../../lib/machines/protocol");
17
+ const transport_1 = require("../../../lib/machines/transport");
18
+ const reply_waiter_1 = require("./reply-waiter");
19
+ const executor_1 = require("./executor");
20
+ const log_1 = require("./log");
21
+ const plan_escalation_1 = require("./plan-escalation");
22
+ const pacing_1 = require("./pacing");
23
+ const plan_machine_1 = require("./plan-machine");
24
+ const state_1 = require("./state");
25
+ const receipts_1 = require("./receipts");
26
+ class MachineGateway {
27
+ constructor(deps) {
28
+ this.deps = deps;
29
+ this.state = new state_1.GatewayState();
30
+ this.log = deps.log ?? log_1.consoleLog;
31
+ this.rows = deps.rows ?? rows_1.standardRows;
32
+ this.codec = deps.codec ?? (0, protocol_1.genericCodec)({ root: deps.settings.topicRoot, line: deps.line });
33
+ this.state.unitsPerDispatch = this.codec.unitsPerDispatch;
34
+ this.state.dispatchOnArrival = this.codec.dispatchOn === protocol_1.DISPATCH_ON.ARRIVAL;
35
+ this.addresses = deps.addresses ?? new address_book_1.MemoryAddressBook();
36
+ this.executor = new executor_1.Executor({
37
+ addresses: this.addresses,
38
+ bus: deps.bus, lt: deps.lt, replies: new reply_waiter_1.ReplyWaiter(), state: this.state, line: deps.line,
39
+ codec: this.codec, replyTimeoutMs: deps.settings.replyTimeoutMs,
40
+ dispatchAttempts: deps.settings.dispatchAttempts, offlineDebounceMs: deps.settings.offlineDebounceMs, log: this.log,
41
+ });
42
+ this.admission = { state: this.state, line: deps.line, executor: this.executor, log: this.log, pace: () => this.pace() };
43
+ this.receipts = new receipts_1.ReceiptBatcher({ bus: deps.bus, codec: this.codec, delayMs: deps.settings.receiptDelayMs ?? DEFAULT_RECEIPT_DELAY_MS, log: this.log });
44
+ }
45
+ async start() {
46
+ await this.deps.bus.connect();
47
+ await this.deps.feed.connect();
48
+ await this.reconcile();
49
+ await this.deps.feed.consume((event) => this.onEscalation(event));
50
+ await this.deps.bus.consume((msg) => this.onMachine(msg));
51
+ const { quietMs } = this.deps.settings;
52
+ if (quietMs > 0) {
53
+ const machines = this.deps.line.machines.map((m) => m.machineId);
54
+ this.quietTimer = setInterval(() => this.executor.checkQuiet(machines, quietMs), Math.max(10, Math.floor(quietMs / 4)));
55
+ }
56
+ this.log.info(log_1.GATEWAY_LOG.STARTED, { site: this.deps.line.siteId, line: this.deps.line.lineId });
57
+ }
58
+ async stop() {
59
+ if (this.quietTimer)
60
+ clearInterval(this.quietTimer);
61
+ this.executor.stop();
62
+ await this.receipts.flush();
63
+ await this.deps.feed.close();
64
+ await this.deps.bus.close();
65
+ }
66
+ /** Resolves when no dispatch is in flight (tests). */
67
+ idle() {
68
+ return this.executor.idle();
69
+ }
70
+ /**
71
+ * On taking the line: open handoff and command rows (a dispatch or command
72
+ * interrupted by a restart is sent again with the same cmdId), the gateway's
73
+ * own alarm, offline and quiet rows, then the machines' retained state to
74
+ * catch what changed while no gateway was listening.
75
+ */
76
+ async reconcile() {
77
+ // Handoffs wait until the whole line is read back, so pacing sees every job before it sends one.
78
+ this.state.holdDispatch = true;
79
+ try {
80
+ const addresses = await (0, reconcile_1.restoreAddresses)(this.state, this.addresses, this.deps.line);
81
+ const handoffs = await this.reconcileHandoffs();
82
+ await this.executor.apply(await (0, reconcile_1.arrivalsOnRows)(this.state, this.deps.lt, this.deps.settings.dispatchTtlMs));
83
+ const commands = await this.reconcileCommands();
84
+ const restored = await (0, reconcile_1.restoreLineRows)(this.state, this.deps.lt, this.deps.line);
85
+ await this.resync();
86
+ this.log.info(log_1.GATEWAY_LOG.RECONCILED, { handoffs, commands, addresses, ...restored });
87
+ }
88
+ finally {
89
+ this.state.holdDispatch = false;
90
+ }
91
+ await this.pace();
92
+ }
93
+ /** Send waiting handoffs to machines that have room (pacing.ts). */
94
+ async pace() {
95
+ await this.executor.apply((0, pacing_1.planPacing)(this.state, this.deps.line, this.deps.settings.dispatchTtlMs));
96
+ }
97
+ async reconcileCommands() {
98
+ let commands = 0;
99
+ for (const data of await this.deps.lt.listOpen({ type: contracts_1.ROW_TYPE.MACHINE_COMMAND })) {
100
+ const event = { type: 'reconcile', escalationId: data.id, role: data.role, verb: 'created', data };
101
+ const actions = (0, plan_escalation_1.planEscalationEvent)(this.state, this.deps.line, event, this.deps.settings.dispatchTtlMs, this.rows);
102
+ if (actions.length)
103
+ commands++;
104
+ await this.executor.apply(actions);
105
+ }
106
+ return commands;
107
+ }
108
+ async reconcileHandoffs() {
109
+ let handoffs = 0;
110
+ for (const query of this.rows.openRows(this.deps.line)) {
111
+ for (const data of await this.deps.lt.listOpen(query)) {
112
+ const event = { type: 'reconcile', escalationId: data.id, role: data.role, verb: 'created', data };
113
+ const actions = (0, plan_escalation_1.planEscalationEvent)(this.state, this.deps.line, event, this.deps.settings.dispatchTtlMs, this.rows);
114
+ if (this.state.get(data.id))
115
+ handoffs++;
116
+ await this.executor.apply(actions);
117
+ }
118
+ }
119
+ return handoffs;
120
+ }
121
+ async resync() {
122
+ const { line } = this.deps;
123
+ const active = new Set();
124
+ for (const msg of await this.deps.bus.retained((0, protocol_1.lineFilter)(this.codec.root, line.siteId, line.lineId))) {
125
+ for (const raw of this.decode(msg.topic, msg.payload, true)) {
126
+ if (!raw.parsed)
127
+ continue;
128
+ const seen = await (0, admission_1.admitSender)(this.admission, msg.topic, raw);
129
+ if (!('parsed' in seen) || !seen.parsed)
130
+ continue;
131
+ if (seen.parsed.kind === protocol_1.MESSAGE_KIND.ALARM && seen.body?.body?.active)
132
+ active.add(this.state.alarmKey(seen.parsed.ident.machineId, seen.parsed.key));
133
+ await this.applyMachine(msg.topic, seen.parsed, seen.body);
134
+ }
135
+ }
136
+ for (const gone of (0, reconcile_1.alarmsClearedMeanwhile)(this.state, active)) {
137
+ this.state.alarms.delete(gone.key);
138
+ await this.executor.apply([{ kind: 'resolve', as: actions_1.CONTROLLER, rowId: gone.rowId, payload: { decision: contracts_1.DECISION.CLEARED, note: `cleared at ${gone.machineId} while the gateway was away` } }]);
139
+ }
140
+ }
141
+ async onEscalation(event) {
142
+ try {
143
+ const verified = await this.verify(event);
144
+ if (verified)
145
+ await this.executor.apply((0, plan_escalation_1.planEscalationEvent)(this.state, this.deps.line, verified, this.deps.settings.dispatchTtlMs, this.rows));
146
+ await this.pace();
147
+ return transport_1.DISPOSITION.ACK;
148
+ }
149
+ catch (err) {
150
+ this.log.error(log_1.GATEWAY_LOG.VERB_FAILED, { event: event.type, error: err.message });
151
+ return transport_1.DISPOSITION.NAK;
152
+ }
153
+ }
154
+ /**
155
+ * An event only says which row to look at. The row is read back as the
156
+ * controller, so a row on a role the line does not hold, or an event that
157
+ * disagrees with the stored row, does nothing.
158
+ */
159
+ async verify(event) {
160
+ if (!(0, plan_escalation_1.concernsLine)(this.state, this.deps.line, event, this.rows))
161
+ return null;
162
+ const row = await this.deps.lt.get(event.escalationId);
163
+ if (!row || !(0, plan_escalation_1.statusFits)(event.verb, row.status))
164
+ return null;
165
+ return { ...event, role: row.role, data: row };
166
+ }
167
+ async onMachine(msg) {
168
+ const decoded = this.decode(msg.topic, msg.payload, msg.retained);
169
+ const acted = [];
170
+ let outcome = transport_1.DISPOSITION.TERM;
171
+ let failed = false;
172
+ for (const raw of decoded) {
173
+ if (!raw.parsed)
174
+ continue;
175
+ const seen = await (0, admission_1.admitSender)(this.admission, msg.topic, raw);
176
+ if (!('parsed' in seen)) {
177
+ failed ||= seen.failed;
178
+ outcome = worse(outcome, seen.disposition);
179
+ continue;
180
+ }
181
+ acted.push(seen);
182
+ if (seen.violations.length && msg.deliveries === 1) {
183
+ this.log.warn(log_1.GATEWAY_LOG.CONTRACT_VIOLATION, { topic: msg.topic, violations: seen.violations });
184
+ }
185
+ const applied = seen.violations.length && this.codec.strict
186
+ ? await (0, admission_1.holdViolation)(this.admission, msg, seen)
187
+ : await this.applyMachine(msg.topic, seen.parsed, seen.body);
188
+ failed ||= applied.failed;
189
+ outcome = worse(outcome, applied.disposition);
190
+ }
191
+ // No row has claimed it after this many tries, so it is not racing its row: stop. A failed verb always retries.
192
+ if (outcome === transport_1.DISPOSITION.NAK && !failed && msg.deliveries >= MAX_DELIVERIES) {
193
+ this.log.warn(log_1.GATEWAY_LOG.UNMATCHED, { topic: msg.topic, deliveries: msg.deliveries });
194
+ outcome = transport_1.DISPOSITION.TERM;
195
+ }
196
+ if (outcome !== transport_1.DISPOSITION.NAK)
197
+ this.confirm(acted);
198
+ return outcome;
199
+ }
200
+ /** A message the codec cannot read is not retried: it is logged and has no effect. */
201
+ decode(topic, payload, retained) {
202
+ try {
203
+ return this.codec.decode(topic, payload, retained);
204
+ }
205
+ catch (err) {
206
+ this.log.warn(log_1.GATEWAY_LOG.UNREADABLE, { topic, error: err.message });
207
+ return [];
208
+ }
209
+ }
210
+ /** Done with the message: confirm each id its codec asked to confirm. */
211
+ confirm(decoded) {
212
+ const seen = new Set();
213
+ for (const { parsed, receiptId } of decoded) {
214
+ if (!parsed || !receiptId || seen.has(receiptId))
215
+ continue;
216
+ seen.add(receiptId);
217
+ this.receipts.add(parsed.ident, receiptId);
218
+ }
219
+ }
220
+ async applyMachine(topic, parsed, body) {
221
+ let plan;
222
+ try {
223
+ plan = (0, plan_machine_1.planMachineMessage)(this.state, parsed, body, this.deps.settings.dispatchTtlMs);
224
+ }
225
+ catch (err) {
226
+ this.log.warn(log_1.GATEWAY_LOG.UNREADABLE, { topic, error: err.message });
227
+ return { disposition: transport_1.DISPOSITION.TERM, failed: false };
228
+ }
229
+ try {
230
+ await this.executor.apply(plan.actions);
231
+ await this.pace();
232
+ return { disposition: plan.disposition, failed: false };
233
+ }
234
+ catch (err) {
235
+ this.log.error(log_1.GATEWAY_LOG.VERB_FAILED, { topic, error: err.message });
236
+ return { disposition: transport_1.DISPOSITION.NAK, failed: true };
237
+ }
238
+ }
239
+ }
240
+ exports.MachineGateway = MachineGateway;
241
+ const MAX_DELIVERIES = 30;
242
+ const DEFAULT_RECEIPT_DELAY_MS = 250;
243
+ const RANK = { [transport_1.DISPOSITION.TERM]: 0, [transport_1.DISPOSITION.ACK]: 1, [transport_1.DISPOSITION.NAK]: 2 };
244
+ /** One wire message may decode to several; any that must be retried retries the message. */
245
+ function worse(a, b) {
246
+ return RANK[b] > RANK[a] ? b : a;
247
+ }
@@ -0,0 +1,10 @@
1
+ export * from './actions';
2
+ export * from './escalations';
3
+ export * from './gateway';
4
+ export * from './log';
5
+ export * from './lt-http';
6
+ export * from './api-escalations';
7
+ export * from './provision';
8
+ export * from './state';
9
+ export * from './address-book';
10
+ export * from './registration';
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./actions"), exports);
18
+ __exportStar(require("./escalations"), exports);
19
+ __exportStar(require("./gateway"), exports);
20
+ __exportStar(require("./log"), exports);
21
+ __exportStar(require("./lt-http"), exports);
22
+ __exportStar(require("./api-escalations"), exports);
23
+ __exportStar(require("./provision"), exports);
24
+ __exportStar(require("./state"), exports);
25
+ __exportStar(require("./address-book"), exports);
26
+ __exportStar(require("./registration"), exports);
@@ -0,0 +1,16 @@
1
+ /**
2
+ * How a handoff's units become machine jobs. A codec that takes one unit per
3
+ * dispatch gets one job per unit, each with its own job id and command id;
4
+ * otherwise the units travel as one job. The split is deterministic, so a
5
+ * cancel or a re-dispatch names the same jobs the dispatch did.
6
+ */
7
+ import type { HandoffUnit } from '../contracts';
8
+ export interface DispatchJob {
9
+ jobId: string;
10
+ units: HandoffUnit[];
11
+ /** Appended to command ids so each job's reply is told apart; empty when unsplit. */
12
+ suffix: string;
13
+ }
14
+ /** Whether a machine's job id is `jobId` or one of the jobs it was split into, whichever units a row names. */
15
+ export declare function jobOwns(jobId: string, reported: string): boolean;
16
+ export declare function dispatchJobs(jobId: string, units: HandoffUnit[], perDispatch: number | null): DispatchJob[];
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ /**
3
+ * How a handoff's units become machine jobs. A codec that takes one unit per
4
+ * dispatch gets one job per unit, each with its own job id and command id;
5
+ * otherwise the units travel as one job. The split is deterministic, so a
6
+ * cancel or a re-dispatch names the same jobs the dispatch did.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.jobOwns = jobOwns;
10
+ exports.dispatchJobs = dispatchJobs;
11
+ /** Whether a machine's job id is `jobId` or one of the jobs it was split into, whichever units a row names. */
12
+ function jobOwns(jobId, reported) {
13
+ return reported === jobId || reported.startsWith(`${jobId}#`);
14
+ }
15
+ function dispatchJobs(jobId, units, perDispatch) {
16
+ if (!perDispatch || units.length <= perDispatch)
17
+ return [{ jobId, units, suffix: '' }];
18
+ const jobs = [];
19
+ for (let i = 0; i < units.length; i += perDispatch) {
20
+ const n = jobs.length + 1;
21
+ jobs.push({ jobId: `${jobId}#${n}`, units: units.slice(i, i + perDispatch), suffix: `#${n}` });
22
+ }
23
+ return jobs;
24
+ }
@@ -0,0 +1,41 @@
1
+ export declare const GATEWAY_LOG: {
2
+ readonly STARTED: "machine_gateway.started";
3
+ readonly RECONCILED: "machine_gateway.reconciled";
4
+ readonly DISPATCHED: "machine_gateway.dispatch.accepted";
5
+ readonly DISPATCH_REFUSED: "machine_gateway.dispatch.refused";
6
+ readonly DISPATCH_READY: "machine_gateway.dispatch.ready";
7
+ readonly DISPATCH_FAILED: "machine_gateway.dispatch.failed";
8
+ readonly DISPATCH_RECONCILED: "machine_gateway.dispatch.reconciled";
9
+ readonly VERB_INVALID: "machine_gateway.verb.invalid";
10
+ readonly VERB_FAILED: "machine_gateway.verb.failed";
11
+ readonly ROW_NOT_VISIBLE: "machine_gateway.verb.row_not_visible";
12
+ readonly ROW_CLAIMED_ELSEWHERE: "machine_gateway.verb.row_claimed_elsewhere";
13
+ readonly UNMATCHED: "machine_gateway.message.unmatched";
14
+ readonly UNREADABLE: "machine_gateway.message.unreadable";
15
+ readonly CONTRACT_VIOLATION: "machine_gateway.contract.violation";
16
+ readonly VIOLATION_ROW: "machine_gateway.contract.row_created";
17
+ readonly ADDRESS_BOUND: "machine_gateway.address.bound";
18
+ readonly ADDRESS_CONTESTED: "machine_gateway.address.contested";
19
+ readonly ADDRESS_UNKNOWN: "machine_gateway.address.unknown";
20
+ readonly ADDRESS_REFUSED: "machine_gateway.address.refused";
21
+ readonly RECEIPT_FAILED: "machine_gateway.receipt.failed";
22
+ readonly ALARM_ROW: "machine_gateway.alarm.row_created";
23
+ readonly OFFLINE_ROW: "machine_gateway.offline.row_created";
24
+ readonly QUIET_ROW: "machine_gateway.quiet.row_created";
25
+ readonly COMMANDED: "machine_gateway.command.accepted";
26
+ readonly COMMAND_REFUSED: "machine_gateway.command.refused";
27
+ readonly COMMAND_UNANSWERED: "machine_gateway.command.unanswered";
28
+ readonly COMMAND_UNSUPPORTED: "machine_gateway.command.unsupported";
29
+ readonly LEADING: "machine_gateway.line.leading";
30
+ readonly STANDBY: "machine_gateway.line.standby";
31
+ readonly LEADERSHIP_LOST: "machine_gateway.line.leadership_lost";
32
+ readonly START_FAILED: "machine_gateway.line.start_failed";
33
+ };
34
+ export type GatewayLogEvent = typeof GATEWAY_LOG[keyof typeof GATEWAY_LOG];
35
+ export interface GatewayLog {
36
+ info(event: GatewayLogEvent, context?: Record<string, unknown>): void;
37
+ warn(event: GatewayLogEvent, context?: Record<string, unknown>): void;
38
+ error(event: GatewayLogEvent, context?: Record<string, unknown>): void;
39
+ }
40
+ export declare const consoleLog: GatewayLog;
41
+ export declare const silentLog: GatewayLog;