@hotmeshio/long-tail 0.25.3 → 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 (145) 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/routes/oauth-server/authorize.js +4 -4
  51. package/build/scripts/machine-line.d.ts +33 -0
  52. package/build/scripts/machine-line.js +301 -0
  53. package/build/services/auth/oauth-server/authorization.d.ts +5 -0
  54. package/build/services/auth/oauth-server/authorization.js +8 -0
  55. package/build/services/auth/oauth-server/metadata.d.ts +1 -1
  56. package/build/services/auth/oauth-server/metadata.js +2 -1
  57. package/build/services/machines/commands.d.ts +29 -0
  58. package/build/services/machines/commands.js +37 -0
  59. package/build/services/machines/config.d.ts +43 -0
  60. package/build/services/machines/config.js +84 -0
  61. package/build/services/machines/contracts.d.ts +193 -0
  62. package/build/services/machines/contracts.js +115 -0
  63. package/build/services/machines/forms.d.ts +55 -0
  64. package/build/services/machines/forms.js +37 -0
  65. package/build/services/machines/gateway/actions.d.ts +111 -0
  66. package/build/services/machines/gateway/actions.js +8 -0
  67. package/build/services/machines/gateway/address-book.d.ts +16 -0
  68. package/build/services/machines/gateway/address-book.js +24 -0
  69. package/build/services/machines/gateway/admission.d.ts +35 -0
  70. package/build/services/machines/gateway/admission.js +66 -0
  71. package/build/services/machines/gateway/api-escalations.d.ts +23 -0
  72. package/build/services/machines/gateway/api-escalations.js +101 -0
  73. package/build/services/machines/gateway/dispatcher.d.ts +63 -0
  74. package/build/services/machines/gateway/dispatcher.js +157 -0
  75. package/build/services/machines/gateway/escalations.d.ts +51 -0
  76. package/build/services/machines/gateway/escalations.js +40 -0
  77. package/build/services/machines/gateway/executor.d.ts +55 -0
  78. package/build/services/machines/gateway/executor.js +195 -0
  79. package/build/services/machines/gateway/gateway.d.ts +82 -0
  80. package/build/services/machines/gateway/gateway.js +247 -0
  81. package/build/services/machines/gateway/index.d.ts +10 -0
  82. package/build/services/machines/gateway/index.js +26 -0
  83. package/build/services/machines/gateway/jobs.d.ts +16 -0
  84. package/build/services/machines/gateway/jobs.js +24 -0
  85. package/build/services/machines/gateway/log.d.ts +41 -0
  86. package/build/services/machines/gateway/log.js +42 -0
  87. package/build/services/machines/gateway/lt-http.d.ts +51 -0
  88. package/build/services/machines/gateway/lt-http.js +120 -0
  89. package/build/services/machines/gateway/pacing.d.ts +29 -0
  90. package/build/services/machines/gateway/pacing.js +134 -0
  91. package/build/services/machines/gateway/plan-escalation.d.ts +19 -0
  92. package/build/services/machines/gateway/plan-escalation.js +201 -0
  93. package/build/services/machines/gateway/plan-machine.d.ts +16 -0
  94. package/build/services/machines/gateway/plan-machine.js +242 -0
  95. package/build/services/machines/gateway/plan-shared.d.ts +18 -0
  96. package/build/services/machines/gateway/plan-shared.js +36 -0
  97. package/build/services/machines/gateway/provision.d.ts +22 -0
  98. package/build/services/machines/gateway/provision.js +79 -0
  99. package/build/services/machines/gateway/receipts.d.ts +33 -0
  100. package/build/services/machines/gateway/receipts.js +65 -0
  101. package/build/services/machines/gateway/reconcile.d.ts +33 -0
  102. package/build/services/machines/gateway/reconcile.js +86 -0
  103. package/build/services/machines/gateway/registration.d.ts +38 -0
  104. package/build/services/machines/gateway/registration.js +121 -0
  105. package/build/services/machines/gateway/reply-waiter.d.ts +24 -0
  106. package/build/services/machines/gateway/reply-waiter.js +65 -0
  107. package/build/services/machines/gateway/state.d.ts +125 -0
  108. package/build/services/machines/gateway/state.js +228 -0
  109. package/build/services/machines/gateway/trace.d.ts +16 -0
  110. package/build/services/machines/gateway/trace.js +43 -0
  111. package/build/services/machines/identities.d.ts +25 -0
  112. package/build/services/machines/identities.js +77 -0
  113. package/build/services/machines/index.d.ts +11 -0
  114. package/build/services/machines/index.js +27 -0
  115. package/build/services/machines/leadership.d.ts +22 -0
  116. package/build/services/machines/leadership.js +75 -0
  117. package/build/services/machines/roles.d.ts +13 -0
  118. package/build/services/machines/roles.js +62 -0
  119. package/build/services/machines/rows.d.ts +30 -0
  120. package/build/services/machines/rows.js +56 -0
  121. package/build/services/machines/select.d.ts +18 -0
  122. package/build/services/machines/select.js +63 -0
  123. package/build/services/machines/surface.d.ts +33 -0
  124. package/build/services/machines/surface.js +98 -0
  125. package/build/services/machines/testing/fake-long-tail.d.ts +78 -0
  126. package/build/services/machines/testing/fake-long-tail.js +155 -0
  127. package/build/services/machines/testing/index.d.ts +3 -0
  128. package/build/services/machines/testing/index.js +19 -0
  129. package/build/services/machines/testing/recording-log.d.ts +14 -0
  130. package/build/services/machines/testing/recording-log.js +22 -0
  131. package/build/services/machines/testing/wait.d.ts +3 -0
  132. package/build/services/machines/testing/wait.js +17 -0
  133. package/build/services/mcp/db-server/schemas.d.ts +2 -2
  134. package/build/start/index.js +13 -0
  135. package/build/system/mcp-servers/admin/schemas.d.ts +221 -221
  136. package/build/system/mcp-servers/db-query/schemas.d.ts +2 -2
  137. package/build/system/mcp-servers/human-queue-schemas.d.ts +6 -6
  138. package/build/tsconfig.tsbuildinfo +1 -1
  139. package/build/types/index.d.ts +2 -1
  140. package/build/types/machines.d.ts +166 -0
  141. package/build/types/machines.js +7 -0
  142. package/build/types/startup.d.ts +23 -0
  143. package/docs/machines.md +293 -0
  144. package/docs/mcp.md +6 -1
  145. package/package.json +11 -1
@@ -0,0 +1,242 @@
1
+ "use strict";
2
+ /**
3
+ * Machine messages to gateway actions: the escalation verbs a person would
4
+ * perform, performed for the machine. Each unit's arrival and the sender's
5
+ * finished record accumulate on the handoff row, and the last expected item
6
+ * resolves it. A failure on either side resolves it with an error. Alarms that
7
+ * stop work and machines that go dark or quiet become rows for people. A
8
+ * restarted machine gets back the dispatches it lost. Everything else stays
9
+ * in the stream.
10
+ *
11
+ * A message whose row does not exist yet is nak'd and arrives again; one whose
12
+ * row already closed is acked, so a replay never resolves anything twice.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.planMachineMessage = planMachineMessage;
16
+ const contracts_1 = require("../contracts");
17
+ const protocol_1 = require("../../../lib/machines/protocol");
18
+ const transport_1 = require("../../../lib/machines/transport");
19
+ const escalation_1 = require("../../../types/escalation");
20
+ const actions_1 = require("./actions");
21
+ const pacing_1 = require("./pacing");
22
+ const plan_shared_1 = require("./plan-shared");
23
+ const jobs_1 = require("./jobs");
24
+ const trace_1 = require("./trace");
25
+ const state_1 = require("./state");
26
+ const ack = (actions = []) => ({ actions, disposition: transport_1.DISPOSITION.ACK });
27
+ const nak = { actions: [], disposition: transport_1.DISPOSITION.NAK };
28
+ function planMachineMessage(state, parsed, env, dispatchTtlMs) {
29
+ const m = parsed.ident.machineId;
30
+ const b = env?.body ?? {};
31
+ const plan = planByKind(state, m, parsed, env, b, dispatchTtlMs);
32
+ return { ...plan, actions: [...heard(state, m), ...plan.actions] };
33
+ }
34
+ function planByKind(state, m, parsed, env, b, ttl) {
35
+ switch (parsed.kind) {
36
+ case protocol_1.MESSAGE_KIND.REPLY:
37
+ return ack([{ kind: 'settleReply', cmdId: b.cmdId, status: b.status, reason: b.reason }]);
38
+ case protocol_1.MESSAGE_KIND.JOB:
39
+ return planJob(state, m, b, env?.at, ttl);
40
+ case protocol_1.MESSAGE_KIND.ALARM:
41
+ return planAlarm(state, parsed, parsed.key ?? b.alarmId, env);
42
+ case protocol_1.MESSAGE_KIND.PRESENCE:
43
+ return planPresence(state, m, b);
44
+ case protocol_1.MESSAGE_KIND.SNAPSHOT:
45
+ return planSnapshot(state, m, b, ttl);
46
+ default:
47
+ return ack();
48
+ }
49
+ }
50
+ /** Any message proves a quiet machine is talking again. */
51
+ function heard(state, m) {
52
+ const link = state.link(m);
53
+ link.lastSeen = Date.now();
54
+ if (!link.quietRowId)
55
+ return [];
56
+ const rowId = link.quietRowId;
57
+ link.quietRowId = undefined;
58
+ return [{ kind: 'resolve', as: actions_1.CONTROLLER, rowId, payload: { decision: contracts_1.DECISION.RESTORED, note: `${m} is talking again` } }];
59
+ }
60
+ /** Ack when the matching row already closed (a replay or a cancel); otherwise wait for the row. */
61
+ function missing(state, unitId, pred) {
62
+ return state.closedFor(unitId, pred) ? ack() : nak;
63
+ }
64
+ /** Whether a machine's job id belongs to `jobId`; a row for some of a job's units still owns it. */
65
+ function ownsJob(jobId, reported) {
66
+ if (!jobId || typeof reported !== 'string')
67
+ return true;
68
+ return (0, jobs_1.jobOwns)(jobId, reported);
69
+ }
70
+ /** Keep the receiver's arrival event for the unit, to go out with its dispatch. */
71
+ function withArrival(row, unitId, b) {
72
+ row.arrivals = { ...row.arrivals, [unitId]: b };
73
+ return row;
74
+ }
75
+ function planJob(state, m, b, at, ttl) {
76
+ const unitId = b.unitId;
77
+ // An event from an earlier attempt's job never touches a later attempt's row.
78
+ const toHere = (r) => r.toMachine === m && ownsJob(r.jobId, b.jobId);
79
+ const fromHere = (r) => r.fromMachine === m && ownsJob(r.senderJobId, b.jobId);
80
+ if (b.event === protocol_1.JOB_EVENT.ARRIVED) {
81
+ const row = state.openFor(unitId, toHere);
82
+ if (!row)
83
+ return missing(state, unitId, toHere);
84
+ const expected = row.units.find((u) => u.unitId === unitId)?.carrierId;
85
+ if (expected && b.carrierId && expected !== b.carrierId) {
86
+ return ack((0, plan_shared_1.failHandoff)(state, row, m, {
87
+ received: false, unitId, errorCode: contracts_1.HANDOFF_ERROR.CARRIER_MISMATCH, detail: `${m} read carrier ${b.carrierId}, expected ${expected}`,
88
+ }));
89
+ }
90
+ // The unit is at the receiver now, and off the sender.
91
+ const job = (0, jobs_1.dispatchJobs)(row.jobId, row.units, state.unitsPerDispatch).find((j) => j.units.some((u) => u.unitId === unitId));
92
+ if (job)
93
+ state.hold(m, unitId, job.jobId);
94
+ const early = row.fromMachine ? releasedEarly(state, row, unitId) : [];
95
+ if (row.fromMachine)
96
+ state.letGo(row.fromMachine, unitId);
97
+ const units = row.units.map((u) => u.unitId);
98
+ const complete = row.dispatch === state_1.DISPATCH_STATE.ARRIVING
99
+ ? { keys: units.map(contracts_1.itemKeys.arrived), then: [(0, pacing_1.dispatchAction)(withArrival(row, unitId, b), m, ttl)] }
100
+ : { keys: units.map(contracts_1.itemKeys.arrived), payload: { received: true, receiver: m, units } };
101
+ return ack([...early, {
102
+ kind: 'accumulate', as: m, rowId: row.id, itemKey: contracts_1.itemKeys.arrived(unitId),
103
+ payload: {
104
+ unitId, carrierId: b.carrierId ?? null, from: b.peer ?? null, at: at ?? new Date().toISOString(),
105
+ ...(row.dispatch === state_1.DISPATCH_STATE.ARRIVING ? { arrival: b } : {}),
106
+ },
107
+ complete,
108
+ }]);
109
+ }
110
+ if (b.event === protocol_1.JOB_EVENT.FINISHED) {
111
+ const row = state.openFor(unitId, fromHere);
112
+ if (!row) {
113
+ if (!state.closedFor(unitId, fromHere))
114
+ state.rememberFinish(m, unitId, b);
115
+ return missing(state, unitId, fromHere);
116
+ }
117
+ state.takeFinish(m, unitId);
118
+ const released = releasedAction(row, m, b);
119
+ if (b.result === protocol_1.JOB_RESULT.OK)
120
+ return ack([released]);
121
+ return ack([released, ...(0, plan_shared_1.failHandoff)(state, row, m, {
122
+ received: false, unitId, errorCode: contracts_1.HANDOFF_ERROR.SENDER_REJECTED, reason: b.reason ?? null, detail: `${m} finished ${unitId}: ${b.result}${b.reason ? ` (${b.reason})` : ''}`,
123
+ })]);
124
+ }
125
+ if (b.event === protocol_1.JOB_EVENT.TRANSFER_FAILED) {
126
+ const either = (r) => toHere(r) || fromHere(r);
127
+ const row = state.openFor(unitId, either);
128
+ if (!row)
129
+ return missing(state, unitId, either);
130
+ return ack((0, plan_shared_1.failHandoff)(state, row, m, { received: false, unitId, errorCode: b.failure, detail: `${m} reported ${b.failure}` }));
131
+ }
132
+ if (b.event === protocol_1.JOB_EVENT.RELEASED) {
133
+ state.letGo(m, unitId);
134
+ return ack();
135
+ }
136
+ if (b.event === protocol_1.JOB_EVENT.WITHDRAWN) {
137
+ state.letGo(m, unitId);
138
+ const row = state.openFor(unitId, toHere);
139
+ if (!row)
140
+ return ack();
141
+ return ack((0, plan_shared_1.failHandoff)(state, row, m, { received: false, unitId, errorCode: contracts_1.HANDOFF_ERROR.WITHDRAWN, detail: `${m} gave up ${b.jobId}` }));
142
+ }
143
+ return ack();
144
+ }
145
+ function releasedAction(row, m, b) {
146
+ row.trace = (0, trace_1.mergeTrace)(row.trace, [(0, trace_1.traceEntry)(m, b)]);
147
+ return { kind: 'accumulate', as: m, rowId: row.id, itemKey: contracts_1.itemKeys.released(b.unitId), payload: b, metadata: (0, trace_1.traceFacets)(row.trace) };
148
+ }
149
+ /** The sender's finished record that came before the row, written ahead of the arrival that may resolve it. */
150
+ function releasedEarly(state, row, unitId) {
151
+ const b = state.takeFinish(row.fromMachine, unitId);
152
+ if (!b || b.result !== protocol_1.JOB_RESULT.OK || !ownsJob(row.senderJobId, b.jobId))
153
+ return [];
154
+ return [releasedAction(row, row.fromMachine, b)];
155
+ }
156
+ function planAlarm(state, parsed, alarmId, env) {
157
+ const { siteId, lineId, machineId: m } = parsed.ident;
158
+ const key = state.alarmKey(m, alarmId);
159
+ const b = env?.body ?? {};
160
+ if (!env || b.active === false) {
161
+ const track = state.alarms.get(key);
162
+ state.alarms.delete(key);
163
+ if (!track?.rowId || !track.open)
164
+ return ack();
165
+ return ack([{
166
+ kind: 'resolve', as: actions_1.CONTROLLER, rowId: track.rowId,
167
+ payload: { decision: contracts_1.DECISION.CLEARED, clearedBy: b.clearedBy ?? null, note: `cleared at ${m}${b.clearedBy ? ` by ${b.clearedBy}` : ''}` },
168
+ }]);
169
+ }
170
+ if (!protocol_1.STOPPING_LEVELS.includes(b.level))
171
+ return ack();
172
+ const known = state.alarms.get(key);
173
+ if (known && (!known.raisedAt || !b.raisedAt || known.raisedAt === b.raisedAt))
174
+ return ack();
175
+ const superseded = known?.rowId && known.open
176
+ ? [{ kind: 'resolve', as: actions_1.CONTROLLER, rowId: known.rowId, payload: { decision: contracts_1.DECISION.CLEARED, note: `${m} raised ${alarmId} again; this one ended` } }]
177
+ : [];
178
+ state.alarms.set(key, { machineId: m, alarmId, raisedAt: b.raisedAt, open: true });
179
+ return ack([...superseded, {
180
+ kind: 'createAlarmRow', machineId: m, alarmId,
181
+ row: {
182
+ type: contracts_1.ROW_TYPE.ALARM, subtype: b.level, role: contracts_1.LINE_ROLE.OPERATOR, priority: b.level === protocol_1.LEVEL.EMERGENCY ? 1 : 2,
183
+ description: `${m}: ${b.text}`,
184
+ metadata: {
185
+ siteId, lineId, machineId: m, alarmId, code: b.code, level: b.level, raisedAt: b.raisedAt,
186
+ zone: b.zone ?? null, autoClears: b.autoClears ?? false, action: b.action ?? null, jobId: b.jobId ?? null,
187
+ },
188
+ ...catalogEnvelope(b.catalog),
189
+ },
190
+ }]);
191
+ }
192
+ /** An alarm naming the catalog edition its code comes from carries that edition as a lookup, so the row reads its meaning. */
193
+ function catalogEnvelope(catalog) {
194
+ const ref = catalog;
195
+ if (!ref || typeof ref.domain !== 'string' || typeof ref.key !== 'string')
196
+ return {};
197
+ return { envelope: JSON.stringify({ [escalation_1.ESCALATION_ENVELOPE_KEYS.LOOKUPS]: [ref] }) };
198
+ }
199
+ function planPresence(state, m, b) {
200
+ const link = state.link(m);
201
+ if (b.online !== true) {
202
+ if (link.online)
203
+ link.since = Date.now();
204
+ link.online = false;
205
+ if (link.rowId || link.watching)
206
+ return ack();
207
+ link.watching = true;
208
+ return ack([{ kind: 'watchOffline', machineId: m }]);
209
+ }
210
+ if (b.software)
211
+ link.software = b.software;
212
+ const wasOffline = !link.online;
213
+ link.online = true;
214
+ link.watching = false;
215
+ const actions = [];
216
+ // Back from a drop: ask for the full state rather than wait for the next heartbeat.
217
+ if (wasOffline)
218
+ actions.push({ kind: 'command', machineId: m, op: protocol_1.OP.SNAPSHOT, cmdId: `${m}:resync:${link.since}`, args: {} });
219
+ if (link.rowId) {
220
+ actions.push({ kind: 'resolve', as: actions_1.CONTROLLER, rowId: link.rowId, payload: { decision: contracts_1.DECISION.RESTORED, note: `${m} back online` } });
221
+ link.rowId = undefined;
222
+ }
223
+ return ack(actions);
224
+ }
225
+ /** A new boot id means the machine restarted and lost its queue: send its open dispatches again. */
226
+ function planSnapshot(state, m, b, ttl) {
227
+ const link = state.link(m);
228
+ if (b.software)
229
+ link.software = b.software;
230
+ const jobIds = Array.isArray(b.jobs) ? b.jobs.map((j) => j?.jobId).filter((id) => typeof id === 'string') : [];
231
+ if (Array.isArray(b.jobs))
232
+ state.keepHeld(m, new Set(jobIds));
233
+ const answer = { kind: 'settleSnapshot', machineId: m, jobIds };
234
+ const previous = link.bootId;
235
+ link.bootId = b.bootId;
236
+ if (!previous || previous === b.bootId)
237
+ return ack([answer]);
238
+ const held = new Set(jobIds);
239
+ const missing = (r) => (0, jobs_1.dispatchJobs)(r.jobId, r.units, state.unitsPerDispatch).some((j) => !held.has(j.jobId));
240
+ const lost = state.open().filter((r) => r.toMachine === m && r.dispatch === state_1.DISPATCH_STATE.ACCEPTED && missing(r));
241
+ return ack([answer, ...lost.map((r) => (0, pacing_1.dispatchAction)(r, m, ttl))]);
242
+ }
@@ -0,0 +1,18 @@
1
+ import { type HandoffResolution } from '../contracts';
2
+ import type { Action, Actor } from './actions';
3
+ import { type DispatchJob } from './jobs';
4
+ import { type GatewayState, type HandoffRow } from './state';
5
+ /** A cancel for each job a handoff's units became at `machineId`, or for those `keep` leaves out. */
6
+ export declare function cancelJobs(state: GatewayState, row: HandoffRow, machineId: string, jobId: string, tag?: string, keep?: (job: DispatchJob) => boolean): Action[];
7
+ /**
8
+ * Withdraw a dispatch that may still sit in the receiver's queue. A job some
9
+ * of whose units already reached the receiver is under way: it stays, and
10
+ * what happens to it is the workflow's next decision (a machine-command row).
11
+ */
12
+ export declare function cancelDispatch(state: GatewayState, row: HandoffRow): Action[];
13
+ /**
14
+ * End a handoff without a receipt: resolve it with the error, then withdraw the
15
+ * dispatch. A row the receiver has claimed is resolved by the receiver (only the
16
+ * claimant may resolve a claimed row); `reportedBy` names who saw the failure.
17
+ */
18
+ export declare function failHandoff(state: GatewayState, row: HandoffRow, reporter: Actor, resolution: HandoffResolution): Action[];
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cancelJobs = cancelJobs;
4
+ exports.cancelDispatch = cancelDispatch;
5
+ exports.failHandoff = failHandoff;
6
+ const contracts_1 = require("../contracts");
7
+ const protocol_1 = require("../../../lib/machines/protocol");
8
+ const jobs_1 = require("./jobs");
9
+ const state_1 = require("./state");
10
+ /** A cancel for each job a handoff's units became at `machineId`, or for those `keep` leaves out. */
11
+ function cancelJobs(state, row, machineId, jobId, tag = '', keep = () => false) {
12
+ return (0, jobs_1.dispatchJobs)(jobId, row.units, state.unitsPerDispatch).filter((job) => !keep(job)).map((job) => ({
13
+ kind: 'command', machineId, op: protocol_1.OP.CANCEL, cmdId: `${(0, contracts_1.cancelCommandId)(row.id)}${tag}${job.suffix}`, args: { jobId: job.jobId },
14
+ }));
15
+ }
16
+ /**
17
+ * Withdraw a dispatch that may still sit in the receiver's queue. A job some
18
+ * of whose units already reached the receiver is under way: it stays, and
19
+ * what happens to it is the workflow's next decision (a machine-command row).
20
+ */
21
+ function cancelDispatch(state, row) {
22
+ const to = row.toMachine;
23
+ if (!to || row.dispatch === state_1.DISPATCH_STATE.NONE || row.dispatch === state_1.DISPATCH_STATE.REFUSED)
24
+ return [];
25
+ return cancelJobs(state, row, to, row.jobId, '', (job) => job.units.some((u) => state.heldJob(to, u.unitId) === job.jobId));
26
+ }
27
+ /**
28
+ * End a handoff without a receipt: resolve it with the error, then withdraw the
29
+ * dispatch. A row the receiver has claimed is resolved by the receiver (only the
30
+ * claimant may resolve a claimed row); `reportedBy` names who saw the failure.
31
+ */
32
+ function failHandoff(state, row, reporter, resolution) {
33
+ const claimant = row.dispatch === state_1.DISPATCH_STATE.ACCEPTED && row.toMachine ? row.toMachine : reporter;
34
+ const payload = claimant === reporter ? { ...resolution } : { ...resolution, reportedBy: reporter };
35
+ return [{ kind: 'resolve', as: claimant, rowId: row.id, payload }, ...cancelDispatch(state, row)];
36
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Identities. Each machine acts in Long Tail as its own bot account, so the
3
+ * rows it claims and resolves name the machine beside the people who worked
4
+ * the unit; the gateway's own bot (the controller) raises operator rows. At
5
+ * boot the gateway logs in, makes sure every bot exists with its roles, and
6
+ * mints a fresh session key per bot, revoking the previous one.
7
+ */
8
+ import { type LineDef } from '../../../lib/machines/line';
9
+ import { type Actor } from './actions';
10
+ import type { LtHttp } from './lt-http';
11
+ export declare const SESSION_KEY_NAME = "machine-gateway-session";
12
+ export interface Identity {
13
+ actor: Actor;
14
+ botName: string;
15
+ roles: string[];
16
+ }
17
+ /** The bots a line needs: one per machine, plus the controller. */
18
+ export declare function lineIdentities(line: LineDef): Identity[];
19
+ /** A builder token: LT_GATEWAY_TOKEN when set, otherwise a username/password login. */
20
+ export declare function adminToken(http: LtHttp, env: Record<string, string | undefined>): Promise<string>;
21
+ /** Ensure every bot and mint its session key; returns actor → bearer token. */
22
+ export declare function provisionIdentities(http: LtHttp, admin: string, line: LineDef): Promise<Map<Actor, string>>;
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ /**
3
+ * Identities. Each machine acts in Long Tail as its own bot account, so the
4
+ * rows it claims and resolves name the machine beside the people who worked
5
+ * the unit; the gateway's own bot (the controller) raises operator rows. At
6
+ * boot the gateway logs in, makes sure every bot exists with its roles, and
7
+ * mints a fresh session key per bot, revoking the previous one.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.SESSION_KEY_NAME = void 0;
11
+ exports.lineIdentities = lineIdentities;
12
+ exports.adminToken = adminToken;
13
+ exports.provisionIdentities = provisionIdentities;
14
+ const protocol_1 = require("../../../lib/machines/protocol");
15
+ const line_1 = require("../../../lib/machines/line");
16
+ const actions_1 = require("./actions");
17
+ exports.SESSION_KEY_NAME = 'machine-gateway-session';
18
+ /** The bots a line needs: one per machine, plus the controller. */
19
+ function lineIdentities(line) {
20
+ const stations = (0, line_1.stationRoles)(line);
21
+ const machines = line.machines.map((m) => ({
22
+ actor: m.machineId,
23
+ botName: `machine-${(0, protocol_1.clientIdOf)((0, line_1.identOf)(line, m.machineId))}`,
24
+ roles: [...stations, ...(0, line_1.receiverRoles)(line)],
25
+ }));
26
+ const controller = {
27
+ actor: actions_1.CONTROLLER,
28
+ botName: (0, line_1.controllerId)(line),
29
+ roles: [...stations, ...(0, line_1.receiverRoles)(line), (0, line_1.operatorRole)(line)],
30
+ };
31
+ return [controller, ...machines];
32
+ }
33
+ /** A builder token: LT_GATEWAY_TOKEN when set, otherwise a username/password login. */
34
+ async function adminToken(http, env) {
35
+ if (env.LT_GATEWAY_TOKEN)
36
+ return env.LT_GATEWAY_TOKEN;
37
+ const username = env.LT_GATEWAY_USERNAME;
38
+ const password = env.LT_GATEWAY_PASSWORD;
39
+ if (!username || !password)
40
+ throw new Error('the gateway needs LT_GATEWAY_TOKEN, or LT_GATEWAY_USERNAME and LT_GATEWAY_PASSWORD');
41
+ const { token } = await http.request('POST', '/auth/login', null, { username, password });
42
+ return token;
43
+ }
44
+ async function ensureBot(http, admin, identity) {
45
+ const page = await http.request('GET', `/bot-accounts?search=${encodeURIComponent(identity.botName)}&limit=50`, admin);
46
+ const found = page.bots.find((b) => b.external_id === identity.botName);
47
+ if (found) {
48
+ const { roles } = await http.request('GET', `/bot-accounts/${found.id}/roles`, admin);
49
+ const held = new Set(roles.map((r) => r.role));
50
+ for (const role of identity.roles.filter((r) => !held.has(r))) {
51
+ await http.request('POST', `/bot-accounts/${found.id}/roles`, admin, { role, type: 'member' });
52
+ }
53
+ return found.id;
54
+ }
55
+ const bot = await http.request('POST', '/bot-accounts', admin, {
56
+ name: identity.botName,
57
+ display_name: identity.actor === actions_1.CONTROLLER ? 'Line controller' : identity.actor,
58
+ description: identity.actor === actions_1.CONTROLLER ? 'Machine gateway: raises line-operator rows' : `Machine ${identity.actor} on the line`,
59
+ roles: identity.roles.map((role) => ({ role, type: 'member' })),
60
+ });
61
+ return bot.id;
62
+ }
63
+ async function mintSessionKey(http, admin, botId) {
64
+ const { keys } = await http.request('GET', `/bot-accounts/${botId}/api-keys`, admin);
65
+ for (const key of keys.filter((k) => k.name === exports.SESSION_KEY_NAME)) {
66
+ await http.request('DELETE', `/bot-accounts/${botId}/api-keys/${key.id}`, admin);
67
+ }
68
+ const { rawKey } = await http.request('POST', `/bot-accounts/${botId}/api-keys`, admin, { name: exports.SESSION_KEY_NAME });
69
+ return rawKey;
70
+ }
71
+ /** Ensure every bot and mint its session key; returns actor → bearer token. */
72
+ async function provisionIdentities(http, admin, line) {
73
+ const tokens = new Map();
74
+ for (const identity of lineIdentities(line)) {
75
+ const botId = await ensureBot(http, admin, identity);
76
+ tokens.set(identity.actor, await mintSessionKey(http, admin, botId));
77
+ }
78
+ return tokens;
79
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Application receipts. A machine that keeps an outbox until the line
3
+ * controller confirms each event gets that confirmation here: once the
4
+ * gateway is done with a message, its id joins a batch for that machine, and
5
+ * the batch goes out through the codec after a short delay or when full. A
6
+ * replayed event is confirmed again, so a machine that missed a receipt stops
7
+ * resending.
8
+ */
9
+ import { type LineCodec, type MachineIdent } from '../../../lib/machines/protocol';
10
+ import type { MachineBus } from '../../../lib/machines/transport';
11
+ import { type GatewayLog } from './log';
12
+ /** Most message ids one receipt carries. */
13
+ export declare const MAX_RECEIPT_IDS = 100;
14
+ export interface ReceiptDeps {
15
+ bus: MachineBus;
16
+ codec: Pick<LineCodec, 'receipt'>;
17
+ /** How long ids gather before a receipt goes out. */
18
+ delayMs: number;
19
+ log: GatewayLog;
20
+ }
21
+ export declare class ReceiptBatcher {
22
+ private readonly d;
23
+ private batches;
24
+ private sending;
25
+ constructor(d: ReceiptDeps);
26
+ /** Whether the codec sends receipts at all. */
27
+ get enabled(): boolean;
28
+ add(ident: MachineIdent, messageId: string): void;
29
+ /** Send every batch now and wait for the sends. */
30
+ flush(): Promise<void>;
31
+ private send;
32
+ private publish;
33
+ }
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ /**
3
+ * Application receipts. A machine that keeps an outbox until the line
4
+ * controller confirms each event gets that confirmation here: once the
5
+ * gateway is done with a message, its id joins a batch for that machine, and
6
+ * the batch goes out through the codec after a short delay or when full. A
7
+ * replayed event is confirmed again, so a machine that missed a receipt stops
8
+ * resending.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.ReceiptBatcher = exports.MAX_RECEIPT_IDS = void 0;
12
+ const protocol_1 = require("../../../lib/machines/protocol");
13
+ const log_1 = require("./log");
14
+ /** Most message ids one receipt carries. */
15
+ exports.MAX_RECEIPT_IDS = 100;
16
+ class ReceiptBatcher {
17
+ constructor(d) {
18
+ this.d = d;
19
+ this.batches = new Map();
20
+ this.sending = new Set();
21
+ }
22
+ /** Whether the codec sends receipts at all. */
23
+ get enabled() {
24
+ return !!this.d.codec.receipt;
25
+ }
26
+ add(ident, messageId) {
27
+ if (!this.enabled)
28
+ return;
29
+ const key = (0, protocol_1.addressOf)(ident);
30
+ let batch = this.batches.get(key);
31
+ if (!batch) {
32
+ batch = { ident, ids: new Set() };
33
+ this.batches.set(key, batch);
34
+ }
35
+ batch.ids.add(messageId);
36
+ if (batch.ids.size >= exports.MAX_RECEIPT_IDS)
37
+ this.send(key);
38
+ else
39
+ batch.timer ??= setTimeout(() => this.send(key), this.d.delayMs);
40
+ }
41
+ /** Send every batch now and wait for the sends. */
42
+ async flush() {
43
+ for (const key of [...this.batches.keys()])
44
+ this.send(key);
45
+ await Promise.all([...this.sending]);
46
+ }
47
+ send(key) {
48
+ const batch = this.batches.get(key);
49
+ if (!batch)
50
+ return;
51
+ this.batches.delete(key);
52
+ if (batch.timer)
53
+ clearTimeout(batch.timer);
54
+ const ids = [...batch.ids];
55
+ const work = this.publish(batch.ident, ids)
56
+ .catch((err) => this.d.log.warn(log_1.GATEWAY_LOG.RECEIPT_FAILED, { machine: batch.ident.machineId, ids: ids.length, error: String(err?.message ?? err) }))
57
+ .finally(() => this.sending.delete(work));
58
+ this.sending.add(work);
59
+ }
60
+ async publish(ident, ids) {
61
+ for (const wire of this.d.codec.receipt(ident, ids))
62
+ await this.d.bus.publish(wire.topic, wire.payload);
63
+ }
64
+ }
65
+ exports.ReceiptBatcher = ReceiptBatcher;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * What a gateway restores when it starts leading a line, read back from Long
3
+ * Tail: the alarm, offline and quiet rows it raised before, so a clear or a
4
+ * return that comes later still resolves them; and the units each open
5
+ * handoff saw arrive, so the receiver holds them again.
6
+ */
7
+ import { type LineDef } from '../../../lib/machines/line';
8
+ import type { AddressBook } from './address-book';
9
+ import type { Action } from './actions';
10
+ import type { LineEscalations } from './escalations';
11
+ import { type GatewayState } from './state';
12
+ export interface RestoredRows {
13
+ alarms: number;
14
+ offline: number;
15
+ quiet: number;
16
+ registrations: number;
17
+ }
18
+ export declare function restoreLineRows(state: GatewayState, lt: LineEscalations, line: LineDef): Promise<RestoredRows>;
19
+ /** The addresses the line learned before, for the stations that still learn theirs. */
20
+ export declare function restoreAddresses(state: GatewayState, book: AddressBook, line: LineDef): Promise<number>;
21
+ /** Alarm rows whose alarm the machine no longer holds: it cleared while no gateway was listening. */
22
+ export declare function alarmsClearedMeanwhile(state: GatewayState, active: Set<string>): {
23
+ key: string;
24
+ rowId: string;
25
+ machineId: string;
26
+ }[];
27
+ /**
28
+ * Units that arrived at an open handoff's receiver, read from the row's items:
29
+ * the receiver holds them again, so a timeout leaves their job alone. A row
30
+ * whose units all arrived completes; one dispatched on arrival is sent with the
31
+ * arrivals it holds.
32
+ */
33
+ export declare function arrivalsOnRows(state: GatewayState, lt: LineEscalations, dispatchTtlMs: number): Promise<Action[]>;
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ /**
3
+ * What a gateway restores when it starts leading a line, read back from Long
4
+ * Tail: the alarm, offline and quiet rows it raised before, so a clear or a
5
+ * return that comes later still resolves them; and the units each open
6
+ * handoff saw arrive, so the receiver holds them again.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.restoreLineRows = restoreLineRows;
10
+ exports.restoreAddresses = restoreAddresses;
11
+ exports.alarmsClearedMeanwhile = alarmsClearedMeanwhile;
12
+ exports.arrivalsOnRows = arrivalsOnRows;
13
+ const contracts_1 = require("../contracts");
14
+ const line_1 = require("../../../lib/machines/line");
15
+ const jobs_1 = require("./jobs");
16
+ const pacing_1 = require("./pacing");
17
+ const state_1 = require("./state");
18
+ async function restoreLineRows(state, lt, line) {
19
+ const role = (0, line_1.operatorRole)(line);
20
+ const mine = (row) => row.metadata?.siteId === line.siteId && row.metadata?.lineId === line.lineId && row.metadata?.machineId;
21
+ const [alarms, offline, quiet, registrations] = await Promise.all([contracts_1.ROW_TYPE.ALARM, contracts_1.ROW_TYPE.MACHINE_OFFLINE, contracts_1.ROW_TYPE.MACHINE_QUIET, contracts_1.ROW_TYPE.MACHINE_REGISTRATION]
22
+ .map(async (type) => (await lt.listOpen({ type, role })).filter(mine)));
23
+ for (const row of alarms) {
24
+ const { machineId, alarmId, raisedAt } = row.metadata;
25
+ state.alarms.set(state.alarmKey(machineId, alarmId), { machineId, alarmId, raisedAt, rowId: row.id, open: true });
26
+ }
27
+ for (const row of offline) {
28
+ const link = state.link(row.metadata.machineId);
29
+ link.online = false;
30
+ link.rowId = row.id;
31
+ }
32
+ for (const row of quiet)
33
+ state.link(row.metadata.machineId).quietRowId = row.id;
34
+ for (const row of registrations)
35
+ state.raiseClaim(row.metadata.address, row.metadata.machineId);
36
+ return { alarms: alarms.length, offline: offline.length, quiet: quiet.length, registrations: registrations.length };
37
+ }
38
+ /** The addresses the line learned before, for the stations that still learn theirs. */
39
+ async function restoreAddresses(state, book, line) {
40
+ let restored = 0;
41
+ for (const [machineId, address] of await book.load()) {
42
+ if (!(0, line_1.learnsAddress)(line, machineId))
43
+ continue;
44
+ state.bindAddress(machineId, address);
45
+ restored++;
46
+ }
47
+ return restored;
48
+ }
49
+ /** Alarm rows whose alarm the machine no longer holds: it cleared while no gateway was listening. */
50
+ function alarmsClearedMeanwhile(state, active) {
51
+ return [...state.alarms.entries()]
52
+ .filter(([key, t]) => t.open && t.rowId && !active.has(key))
53
+ .map(([key, t]) => ({ key, rowId: t.rowId, machineId: t.machineId }));
54
+ }
55
+ /**
56
+ * Units that arrived at an open handoff's receiver, read from the row's items:
57
+ * the receiver holds them again, so a timeout leaves their job alone. A row
58
+ * whose units all arrived completes; one dispatched on arrival is sent with the
59
+ * arrivals it holds.
60
+ */
61
+ async function arrivalsOnRows(state, lt, dispatchTtlMs) {
62
+ const actions = [];
63
+ for (const row of state.open()) {
64
+ const to = row.toMachine;
65
+ if (!to || (row.dispatch !== state_1.DISPATCH_STATE.ARRIVING && row.dispatch !== state_1.DISPATCH_STATE.ACCEPTED))
66
+ continue;
67
+ const items = await lt.items(row.id);
68
+ const units = row.units.map((u) => u.unitId);
69
+ const { arrived, missing } = (0, contracts_1.handoffProgress)(units, items);
70
+ for (const job of (0, jobs_1.dispatchJobs)(row.jobId, row.units, state.unitsPerDispatch)) {
71
+ for (const u of job.units)
72
+ if (arrived.includes(u.unitId))
73
+ state.hold(to, u.unitId, job.jobId);
74
+ }
75
+ if (missing.length)
76
+ continue;
77
+ if (row.dispatch === state_1.DISPATCH_STATE.ACCEPTED) {
78
+ actions.push({ kind: 'resolve', as: to, rowId: row.id, payload: { received: true, receiver: to, units } });
79
+ continue;
80
+ }
81
+ const byUnit = new Map(items.filter((i) => (0, contracts_1.arrivedUnit)(i.itemKey)).map((i) => [i.payload.unitId, i.payload]));
82
+ row.arrivals = Object.fromEntries(units.map((u) => [u, byUnit.get(u)?.arrival ?? { unitId: u, carrierId: byUnit.get(u)?.carrierId ?? null }]));
83
+ actions.push((0, pacing_1.dispatchAction)(row, to, dispatchTtlMs));
84
+ }
85
+ return actions;
86
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Learned addresses. On a line that learns them, a station is bound to the
3
+ * address that first claims it. A claim that disagrees with a binding (a
4
+ * station already bound to another address, or an address bound to another
5
+ * station) is a person's decision: one registration row per claim, and the
6
+ * claimant's messages have no effect until it is approved. Lines that declare
7
+ * their addresses pass through unchanged.
8
+ */
9
+ import { type LineDef } from '../../../lib/machines/line';
10
+ import { type Inspection, type MachineIdent } from '../../../lib/machines/protocol';
11
+ import type { EscalationEvent } from '../../../lib/machines/transport';
12
+ import type { Action, CreateRowInput } from './actions';
13
+ import type { GatewayState } from './state';
14
+ export declare const SENDER: {
15
+ readonly KNOWN: "known";
16
+ readonly BOUND: "bound";
17
+ readonly CONTESTED: "contested";
18
+ readonly UNKNOWN: "unknown";
19
+ };
20
+ export type SenderStatus = typeof SENDER[keyof typeof SENDER];
21
+ export interface Sender {
22
+ status: SenderStatus;
23
+ address: string;
24
+ /** The station the message is from (known, bound) or claims to be (contested). */
25
+ machineId?: string;
26
+ /** The inspection with the station as its machine id; set when the message may be acted on. */
27
+ seen?: Inspection;
28
+ }
29
+ /** Who sent a message: a known station, one bound just now, a contested claim, or no station at all. */
30
+ export declare function resolveSender(state: GatewayState, line: LineDef, seen: Inspection): Sender;
31
+ /** A station's identity on the wire, with the address the line learned for it. */
32
+ export declare function identFor(state: GatewayState, line: LineDef, machineId: string): MachineIdent;
33
+ /** Whether the gateway knows where to send to a station: a declared address, or one it learned. */
34
+ export declare function reachable(state: GatewayState, line: LineDef, machineId: string): boolean;
35
+ /** The row asking a person whether `address` is `machineId`. */
36
+ export declare function registrationRow(state: GatewayState, line: LineDef, address: string, machineId: string): CreateRowInput;
37
+ /** A person decided a registration row: approval binds the station to the claimant; anything else leaves the binding. */
38
+ export declare function planRegistrationRow(state: GatewayState, line: LineDef, event: EscalationEvent): Action[];