@hotmeshio/long-tail 0.25.4 → 0.27.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 (173) hide show
  1. package/README.md +1 -0
  2. package/build/index.d.ts +5 -0
  3. package/build/index.js +36 -7
  4. package/build/lib/events/nats.d.ts +11 -0
  5. package/build/lib/events/nats.js +33 -2
  6. package/build/lib/machines/index.d.ts +6 -0
  7. package/build/lib/machines/index.js +22 -0
  8. package/build/lib/machines/line.d.ts +44 -0
  9. package/build/lib/machines/line.js +114 -0
  10. package/build/lib/machines/protocol/codec.d.ts +108 -0
  11. package/build/lib/machines/protocol/codec.js +80 -0
  12. package/build/lib/machines/protocol/constants.d.ts +120 -0
  13. package/build/lib/machines/protocol/constants.js +97 -0
  14. package/build/lib/machines/protocol/contract.d.ts +19 -0
  15. package/build/lib/machines/protocol/contract.js +141 -0
  16. package/build/lib/machines/protocol/envelope.d.ts +14 -0
  17. package/build/lib/machines/protocol/envelope.js +43 -0
  18. package/build/lib/machines/protocol/index.d.ts +5 -0
  19. package/build/lib/machines/protocol/index.js +21 -0
  20. package/build/lib/machines/protocol/topics.d.ts +33 -0
  21. package/build/lib/machines/protocol/topics.js +79 -0
  22. package/build/lib/machines/site/edge-account.d.ts +11 -0
  23. package/build/lib/machines/site/edge-account.js +24 -0
  24. package/build/lib/machines/site/mosquitto-conf.d.ts +27 -0
  25. package/build/lib/machines/site/mosquitto-conf.js +81 -0
  26. package/build/lib/machines/site/site-conf.d.ts +23 -0
  27. package/build/lib/machines/site/site-conf.js +62 -0
  28. package/build/lib/machines/transport/callback-feed.d.ts +18 -0
  29. package/build/lib/machines/transport/callback-feed.js +31 -0
  30. package/build/lib/machines/transport/delivery.d.ts +20 -0
  31. package/build/lib/machines/transport/delivery.js +37 -0
  32. package/build/lib/machines/transport/index.d.ts +13 -0
  33. package/build/lib/machines/transport/index.js +29 -0
  34. package/build/lib/machines/transport/memory-broker.d.ts +35 -0
  35. package/build/lib/machines/transport/memory-broker.js +145 -0
  36. package/build/lib/machines/transport/memory.d.ts +40 -0
  37. package/build/lib/machines/transport/memory.js +79 -0
  38. package/build/lib/machines/transport/mqtt-bus.d.ts +24 -0
  39. package/build/lib/machines/transport/mqtt-bus.js +43 -0
  40. package/build/lib/machines/transport/mqtt-link.d.ts +16 -0
  41. package/build/lib/machines/transport/mqtt-link.js +37 -0
  42. package/build/lib/machines/transport/mqtt-session.d.ts +34 -0
  43. package/build/lib/machines/transport/mqtt-session.js +127 -0
  44. package/build/lib/machines/transport/nats-bus.d.ts +36 -0
  45. package/build/lib/machines/transport/nats-bus.js +47 -0
  46. package/build/lib/machines/transport/nats-feed.d.ts +26 -0
  47. package/build/lib/machines/transport/nats-feed.js +49 -0
  48. package/build/lib/machines/transport/pubsub-feed.d.ts +17 -0
  49. package/build/lib/machines/transport/pubsub-feed.js +39 -0
  50. package/build/lib/machines/transport/serial.d.ts +21 -0
  51. package/build/lib/machines/transport/serial.js +54 -0
  52. package/build/lib/machines/transport/stream-bus.d.ts +40 -0
  53. package/build/lib/machines/transport/stream-bus.js +57 -0
  54. package/build/lib/machines/transport/streams.d.ts +24 -0
  55. package/build/lib/machines/transport/streams.js +49 -0
  56. package/build/lib/messaging/config.d.ts +25 -0
  57. package/build/lib/messaging/config.js +55 -0
  58. package/build/lib/messaging/constants.d.ts +26 -0
  59. package/build/lib/messaging/constants.js +26 -0
  60. package/build/lib/messaging/index.d.ts +24 -0
  61. package/build/lib/messaging/index.js +65 -0
  62. package/build/lib/messaging/memory.d.ts +16 -0
  63. package/build/lib/messaging/memory.js +147 -0
  64. package/build/lib/messaging/nats/permissions.d.ts +13 -0
  65. package/build/lib/messaging/nats/permissions.js +22 -0
  66. package/build/lib/messaging/nats/provider.d.ts +30 -0
  67. package/build/lib/messaging/nats/provider.js +77 -0
  68. package/build/lib/messaging/nats/render-conf.d.ts +68 -0
  69. package/build/lib/messaging/nats/render-conf.js +58 -0
  70. package/build/lib/messaging/nats/streams.d.ts +26 -0
  71. package/build/lib/messaging/nats/streams.js +97 -0
  72. package/build/lib/messaging/subjects.d.ts +8 -0
  73. package/build/lib/messaging/subjects.js +25 -0
  74. package/build/scripts/bambu-boat.d.ts +17 -0
  75. package/build/scripts/bambu-boat.js +134 -0
  76. package/build/scripts/bambu-print.d.ts +18 -0
  77. package/build/scripts/bambu-print.js +114 -0
  78. package/build/scripts/machine-line.d.ts +33 -0
  79. package/build/scripts/machine-line.js +301 -0
  80. package/build/services/machines/commands.d.ts +29 -0
  81. package/build/services/machines/commands.js +37 -0
  82. package/build/services/machines/config.d.ts +49 -0
  83. package/build/services/machines/config.js +94 -0
  84. package/build/services/machines/contracts.d.ts +210 -0
  85. package/build/services/machines/contracts.js +124 -0
  86. package/build/services/machines/forms.d.ts +55 -0
  87. package/build/services/machines/forms.js +37 -0
  88. package/build/services/machines/gateway/actions.d.ts +111 -0
  89. package/build/services/machines/gateway/actions.js +8 -0
  90. package/build/services/machines/gateway/address-book.d.ts +16 -0
  91. package/build/services/machines/gateway/address-book.js +24 -0
  92. package/build/services/machines/gateway/admission.d.ts +35 -0
  93. package/build/services/machines/gateway/admission.js +66 -0
  94. package/build/services/machines/gateway/api-escalations.d.ts +23 -0
  95. package/build/services/machines/gateway/api-escalations.js +101 -0
  96. package/build/services/machines/gateway/dispatcher.d.ts +76 -0
  97. package/build/services/machines/gateway/dispatcher.js +197 -0
  98. package/build/services/machines/gateway/escalations.d.ts +51 -0
  99. package/build/services/machines/gateway/escalations.js +40 -0
  100. package/build/services/machines/gateway/executor.d.ts +60 -0
  101. package/build/services/machines/gateway/executor.js +221 -0
  102. package/build/services/machines/gateway/gateway.d.ts +88 -0
  103. package/build/services/machines/gateway/gateway.js +270 -0
  104. package/build/services/machines/gateway/index.d.ts +10 -0
  105. package/build/services/machines/gateway/index.js +26 -0
  106. package/build/services/machines/gateway/jobs.d.ts +16 -0
  107. package/build/services/machines/gateway/jobs.js +24 -0
  108. package/build/services/machines/gateway/lifecycle.d.ts +21 -0
  109. package/build/services/machines/gateway/lifecycle.js +89 -0
  110. package/build/services/machines/gateway/log.d.ts +41 -0
  111. package/build/services/machines/gateway/log.js +42 -0
  112. package/build/services/machines/gateway/lt-http.d.ts +51 -0
  113. package/build/services/machines/gateway/lt-http.js +120 -0
  114. package/build/services/machines/gateway/pacing.d.ts +43 -0
  115. package/build/services/machines/gateway/pacing.js +158 -0
  116. package/build/services/machines/gateway/plan-escalation.d.ts +19 -0
  117. package/build/services/machines/gateway/plan-escalation.js +210 -0
  118. package/build/services/machines/gateway/plan-machine.d.ts +17 -0
  119. package/build/services/machines/gateway/plan-machine.js +283 -0
  120. package/build/services/machines/gateway/plan-shared.d.ts +18 -0
  121. package/build/services/machines/gateway/plan-shared.js +36 -0
  122. package/build/services/machines/gateway/provision.d.ts +22 -0
  123. package/build/services/machines/gateway/provision.js +79 -0
  124. package/build/services/machines/gateway/receipts.d.ts +33 -0
  125. package/build/services/machines/gateway/receipts.js +65 -0
  126. package/build/services/machines/gateway/reconcile.d.ts +35 -0
  127. package/build/services/machines/gateway/reconcile.js +99 -0
  128. package/build/services/machines/gateway/registration.d.ts +38 -0
  129. package/build/services/machines/gateway/registration.js +121 -0
  130. package/build/services/machines/gateway/reply-waiter.d.ts +24 -0
  131. package/build/services/machines/gateway/reply-waiter.js +65 -0
  132. package/build/services/machines/gateway/state.d.ts +145 -0
  133. package/build/services/machines/gateway/state.js +241 -0
  134. package/build/services/machines/gateway/trace.d.ts +16 -0
  135. package/build/services/machines/gateway/trace.js +43 -0
  136. package/build/services/machines/identities.d.ts +25 -0
  137. package/build/services/machines/identities.js +77 -0
  138. package/build/services/machines/index.d.ts +11 -0
  139. package/build/services/machines/index.js +27 -0
  140. package/build/services/machines/leadership.d.ts +35 -0
  141. package/build/services/machines/leadership.js +100 -0
  142. package/build/services/machines/roles.d.ts +13 -0
  143. package/build/services/machines/roles.js +62 -0
  144. package/build/services/machines/rows.d.ts +30 -0
  145. package/build/services/machines/rows.js +71 -0
  146. package/build/services/machines/select.d.ts +25 -0
  147. package/build/services/machines/select.js +76 -0
  148. package/build/services/machines/surface.d.ts +33 -0
  149. package/build/services/machines/surface.js +104 -0
  150. package/build/services/machines/testing/fake-long-tail.d.ts +79 -0
  151. package/build/services/machines/testing/fake-long-tail.js +158 -0
  152. package/build/services/machines/testing/index.d.ts +3 -0
  153. package/build/services/machines/testing/index.js +19 -0
  154. package/build/services/machines/testing/recording-log.d.ts +14 -0
  155. package/build/services/machines/testing/recording-log.js +22 -0
  156. package/build/services/machines/testing/wait.d.ts +3 -0
  157. package/build/services/machines/testing/wait.js +17 -0
  158. package/build/services/mcp/db-server/schemas.d.ts +2 -2
  159. package/build/start/adapters.js +11 -2
  160. package/build/start/index.js +20 -1
  161. package/build/system/mcp-servers/admin/schemas.d.ts +221 -221
  162. package/build/system/mcp-servers/db-query/schemas.d.ts +2 -2
  163. package/build/system/mcp-servers/human-queue-schemas.d.ts +6 -6
  164. package/build/tsconfig.tsbuildinfo +1 -1
  165. package/build/types/index.d.ts +3 -1
  166. package/build/types/machines.d.ts +167 -0
  167. package/build/types/machines.js +7 -0
  168. package/build/types/messaging.d.ts +72 -0
  169. package/build/types/messaging.js +11 -0
  170. package/build/types/startup.d.ts +49 -1
  171. package/docs/events.md +50 -4
  172. package/docs/machines.md +322 -0
  173. package/package.json +13 -1
@@ -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,35 @@
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[]>;
34
+ /** With lifecycle notes on, the items each handoff out of a machine already holds, so a restart notes nothing twice. */
35
+ export declare function notesOnRows(state: GatewayState, lt: LineEscalations): Promise<void>;
@@ -0,0 +1,99 @@
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
+ exports.notesOnRows = notesOnRows;
14
+ const contracts_1 = require("../contracts");
15
+ const line_1 = require("../../../lib/machines/line");
16
+ const jobs_1 = require("./jobs");
17
+ const pacing_1 = require("./pacing");
18
+ const state_1 = require("./state");
19
+ async function restoreLineRows(state, lt, line) {
20
+ const role = (0, line_1.operatorRole)(line);
21
+ const mine = (row) => row.metadata?.siteId === line.siteId && row.metadata?.lineId === line.lineId && row.metadata?.machineId;
22
+ 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]
23
+ .map(async (type) => (await lt.listOpen({ type, role })).filter(mine)));
24
+ for (const row of alarms) {
25
+ const { machineId, alarmId, raisedAt } = row.metadata;
26
+ state.alarms.set(state.alarmKey(machineId, alarmId), { machineId, alarmId, raisedAt, rowId: row.id, open: true });
27
+ }
28
+ for (const row of offline) {
29
+ const link = state.link(row.metadata.machineId);
30
+ link.online = false;
31
+ link.rowId = row.id;
32
+ }
33
+ for (const row of quiet)
34
+ state.link(row.metadata.machineId).quietRowId = row.id;
35
+ for (const row of registrations)
36
+ state.raiseClaim(row.metadata.address, row.metadata.machineId);
37
+ return { alarms: alarms.length, offline: offline.length, quiet: quiet.length, registrations: registrations.length };
38
+ }
39
+ /** The addresses the line learned before, for the stations that still learn theirs. */
40
+ async function restoreAddresses(state, book, line) {
41
+ let restored = 0;
42
+ for (const [machineId, address] of await book.load()) {
43
+ if (!(0, line_1.learnsAddress)(line, machineId))
44
+ continue;
45
+ state.bindAddress(machineId, address);
46
+ restored++;
47
+ }
48
+ return restored;
49
+ }
50
+ /** Alarm rows whose alarm the machine no longer holds: it cleared while no gateway was listening. */
51
+ function alarmsClearedMeanwhile(state, active) {
52
+ return [...state.alarms.entries()]
53
+ .filter(([key, t]) => t.open && t.rowId && !active.has(key))
54
+ .map(([key, t]) => ({ key, rowId: t.rowId, machineId: t.machineId }));
55
+ }
56
+ /**
57
+ * Units that arrived at an open handoff's receiver, read from the row's items:
58
+ * the receiver holds them again, so a timeout leaves their job alone. A row
59
+ * whose units all arrived completes; one dispatched on arrival is sent with the
60
+ * arrivals it holds.
61
+ */
62
+ async function arrivalsOnRows(state, lt, dispatchTtlMs) {
63
+ const actions = [];
64
+ for (const row of state.open()) {
65
+ const to = row.toMachine;
66
+ if (!to || (row.dispatch !== state_1.DISPATCH_STATE.ARRIVING && row.dispatch !== state_1.DISPATCH_STATE.ACCEPTED))
67
+ continue;
68
+ const items = await lt.items(row.id);
69
+ row.noted = items.map((i) => i.itemKey);
70
+ const units = row.units.map((u) => u.unitId);
71
+ const { arrived, missing } = (0, contracts_1.handoffProgress)(units, items);
72
+ for (const job of (0, jobs_1.dispatchJobs)(row.jobId, row.units, state.unitsPerDispatch)) {
73
+ for (const u of job.units)
74
+ if (arrived.includes(u.unitId))
75
+ state.hold(to, u.unitId, job.jobId);
76
+ }
77
+ if (missing.length)
78
+ continue;
79
+ if (row.dispatch === state_1.DISPATCH_STATE.ACCEPTED) {
80
+ actions.push({ kind: 'resolve', as: to, rowId: row.id, payload: { received: true, receiver: to, units } });
81
+ continue;
82
+ }
83
+ const byUnit = new Map(items.filter((i) => (0, contracts_1.arrivedUnit)(i.itemKey)).map((i) => [i.payload.unitId, i.payload]));
84
+ row.arrivals = Object.fromEntries(units.map((u) => [u, byUnit.get(u)?.arrival ?? { unitId: u, carrierId: byUnit.get(u)?.carrierId ?? null }]));
85
+ for (const arrival of byUnit.values())
86
+ row.dueAt = (0, pacing_1.latestArrival)(row.dueAt, arrival.at);
87
+ actions.push((0, pacing_1.dispatchAction)(row, to, dispatchTtlMs));
88
+ }
89
+ return actions;
90
+ }
91
+ /** With lifecycle notes on, the items each handoff out of a machine already holds, so a restart notes nothing twice. */
92
+ async function notesOnRows(state, lt) {
93
+ if (!state.lifecycle)
94
+ return;
95
+ for (const row of state.open()) {
96
+ if (row.fromMachine && !row.noted)
97
+ row.noted = (await lt.items(row.id)).map((i) => i.itemKey);
98
+ }
99
+ }
@@ -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[];
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ /**
3
+ * Learned addresses. On a line that learns them, a station is bound to the
4
+ * address that first claims it. A claim that disagrees with a binding (a
5
+ * station already bound to another address, or an address bound to another
6
+ * station) is a person's decision: one registration row per claim, and the
7
+ * claimant's messages have no effect until it is approved. Lines that declare
8
+ * their addresses pass through unchanged.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.SENDER = void 0;
12
+ exports.resolveSender = resolveSender;
13
+ exports.identFor = identFor;
14
+ exports.reachable = reachable;
15
+ exports.registrationRow = registrationRow;
16
+ exports.planRegistrationRow = planRegistrationRow;
17
+ const contracts_1 = require("../contracts");
18
+ const line_1 = require("../../../lib/machines/line");
19
+ const protocol_1 = require("../../../lib/machines/protocol");
20
+ exports.SENDER = { KNOWN: 'known', BOUND: 'bound', CONTESTED: 'contested', UNKNOWN: 'unknown' };
21
+ const onLine = (line, machineId) => !!machineId && line.machines.some((m) => m.machineId === machineId);
22
+ function declaredAt(line, machineId, address) {
23
+ return onLine(line, machineId) && !(0, line_1.learnsAddress)(line, machineId) && (0, protocol_1.addressOf)((0, line_1.identOf)(line, machineId)) === address;
24
+ }
25
+ function asStation(seen, machineId, address) {
26
+ return { ...seen, parsed: { ...seen.parsed, ident: { ...seen.parsed.ident, machineId, address } } };
27
+ }
28
+ /** Who sent a message: a known station, one bound just now, a contested claim, or no station at all. */
29
+ function resolveSender(state, line, seen) {
30
+ const ident = seen.parsed.ident;
31
+ const address = ident.address ?? ident.machineId;
32
+ if (line.addressing !== line_1.ADDRESSING.LEARNED)
33
+ return { status: exports.SENDER.KNOWN, address, machineId: ident.machineId, seen };
34
+ // A station whose address the line declares is known only on that address.
35
+ if (declaredAt(line, ident.machineId, address))
36
+ return { status: exports.SENDER.KNOWN, address, machineId: ident.machineId, seen };
37
+ const claim = seen.claim ?? (ident.address ? ident.machineId : undefined);
38
+ const bound = state.stationAt(address);
39
+ if (bound) {
40
+ if (claim && claim !== bound)
41
+ return { status: exports.SENDER.CONTESTED, address, machineId: claim };
42
+ return { status: exports.SENDER.KNOWN, address, machineId: bound, seen: asStation(seen, bound, address) };
43
+ }
44
+ if (!onLine(line, claim) || !(0, line_1.learnsAddress)(line, claim))
45
+ return { status: exports.SENDER.UNKNOWN, address };
46
+ if (state.learnedAddress(claim))
47
+ return { status: exports.SENDER.CONTESTED, address, machineId: claim };
48
+ return { status: exports.SENDER.BOUND, address, machineId: claim, seen: asStation(seen, claim, address) };
49
+ }
50
+ /** A station's identity on the wire, with the address the line learned for it. */
51
+ function identFor(state, line, machineId) {
52
+ const learned = (0, line_1.learnsAddress)(line, machineId) ? state.learnedAddress(machineId) : undefined;
53
+ return { ...(0, line_1.identOf)(line, machineId), ...(learned ? { address: learned } : {}) };
54
+ }
55
+ /** Whether the gateway knows where to send to a station: a declared address, or one it learned. */
56
+ function reachable(state, line, machineId) {
57
+ return !(0, line_1.learnsAddress)(line, machineId) || !!state.learnedAddress(machineId);
58
+ }
59
+ /** The row asking a person whether `address` is `machineId`. */
60
+ function registrationRow(state, line, address, machineId) {
61
+ const boundAddress = state.learnedAddress(machineId) ?? null;
62
+ const boundStation = state.stationAt(address) ?? null;
63
+ const now = [
64
+ boundAddress ? `${machineId} is bound to ${boundAddress}` : null,
65
+ boundStation ? `${address} is bound to ${boundStation}` : null,
66
+ ].filter(Boolean).join('; ');
67
+ return {
68
+ type: contracts_1.ROW_TYPE.MACHINE_REGISTRATION, role: (0, line_1.operatorRole)(line), priority: 2,
69
+ description: `${address} says it is ${machineId}. ${now}. Approve to bind ${machineId} to ${address}.`,
70
+ metadata: { siteId: line.siteId, lineId: line.lineId, machineId, address, boundAddress, boundStation },
71
+ };
72
+ }
73
+ /** A row's resolver payload, which Long Tail stores serialized. */
74
+ function resolutionOf(raw) {
75
+ if (raw && typeof raw === 'object')
76
+ return raw;
77
+ if (typeof raw !== 'string')
78
+ return null;
79
+ try {
80
+ const parsed = JSON.parse(raw);
81
+ return parsed && typeof parsed === 'object' ? parsed : null;
82
+ }
83
+ catch {
84
+ return null;
85
+ }
86
+ }
87
+ /**
88
+ * Why an approved registration may not bind; null when it may. The row is only a request:
89
+ * the station must learn its address, the address must be one a machine could send from,
90
+ * and a machine at that address must have claimed the station on the wire.
91
+ */
92
+ function bindingRefusal(state, line, machineId, address) {
93
+ if (!onLine(line, machineId) || !(0, line_1.learnsAddress)(line, machineId))
94
+ return `${machineId} is not a station on this line that learns its address`;
95
+ if (!(0, protocol_1.isTopicLevel)(address))
96
+ return `"${address}" is not an address a machine can send from`;
97
+ if (line.machines.some((m) => declaredAt(line, m.machineId, address)))
98
+ return `${address} is the declared address of another station`;
99
+ if (!state.claimHeard(address, machineId))
100
+ return `no machine at ${address} has claimed ${machineId} since this gateway took the line`;
101
+ return null;
102
+ }
103
+ /** A person decided a registration row: approval binds the station to the claimant; anything else leaves the binding. */
104
+ function planRegistrationRow(state, line, event) {
105
+ const md = event.data?.metadata ?? {};
106
+ if (typeof md.machineId !== 'string' || typeof md.address !== 'string')
107
+ return [];
108
+ if (event.verb === 'created') {
109
+ state.raiseClaim(md.address, md.machineId);
110
+ return [];
111
+ }
112
+ state.settleClaim(md.address, md.machineId);
113
+ const approved = event.verb === 'resolved' && resolutionOf(event.data?.resolver_payload)?.decision === contracts_1.DECISION.APPROVE;
114
+ const by = event.data?.resolved_by ?? event.data?.metadata?.resolved_by ?? undefined;
115
+ if (!approved)
116
+ return [];
117
+ const reason = bindingRefusal(state, line, md.machineId, md.address);
118
+ return reason
119
+ ? [{ kind: 'refuseBinding', machineId: md.machineId, address: md.address, reason }]
120
+ : [{ kind: 'bindAddress', machineId: md.machineId, address: md.address, by }];
121
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Command replies by cmdId, and machine snapshots by machine. A wait registers
3
+ * before the command is published and resolves with the reply, or null at the
4
+ * deadline.
5
+ */
6
+ export interface CommandReply {
7
+ cmdId: string;
8
+ status: string;
9
+ reason: string;
10
+ }
11
+ /** The jobs a machine reports holding, queued or in work. */
12
+ export type HeldJobs = Set<string>;
13
+ export declare class ReplyWaiter {
14
+ private replies;
15
+ private snapshots;
16
+ wait(cmdId: string, timeoutMs: number): Promise<CommandReply | null>;
17
+ /** True when a waiter took the reply. */
18
+ settle(reply: CommandReply): boolean;
19
+ /** The next snapshot from `machineId`, or null at the deadline. */
20
+ waitSnapshot(machineId: string, timeoutMs: number): Promise<HeldJobs | null>;
21
+ /** Answer a status query in flight; null when there is nothing to wait for. */
22
+ settleSnapshot(machineId: string, jobs: HeldJobs | null): void;
23
+ cancelAll(): void;
24
+ }
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ /**
3
+ * Command replies by cmdId, and machine snapshots by machine. A wait registers
4
+ * before the command is published and resolves with the reply, or null at the
5
+ * deadline.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.ReplyWaiter = void 0;
9
+ class Waits {
10
+ constructor() {
11
+ this.waiting = new Map();
12
+ }
13
+ wait(key, timeoutMs) {
14
+ this.waiting.get(key)?.resolve(null);
15
+ return new Promise((resolve) => {
16
+ const timer = setTimeout(() => {
17
+ this.waiting.delete(key);
18
+ resolve(null);
19
+ }, timeoutMs);
20
+ this.waiting.set(key, { resolve, timer });
21
+ });
22
+ }
23
+ settle(key, value) {
24
+ const w = this.waiting.get(key);
25
+ if (!w)
26
+ return false;
27
+ clearTimeout(w.timer);
28
+ this.waiting.delete(key);
29
+ w.resolve(value);
30
+ return true;
31
+ }
32
+ cancelAll() {
33
+ for (const [, w] of this.waiting) {
34
+ clearTimeout(w.timer);
35
+ w.resolve(null);
36
+ }
37
+ this.waiting.clear();
38
+ }
39
+ }
40
+ class ReplyWaiter {
41
+ constructor() {
42
+ this.replies = new Waits();
43
+ this.snapshots = new Waits();
44
+ }
45
+ wait(cmdId, timeoutMs) {
46
+ return this.replies.wait(cmdId, timeoutMs);
47
+ }
48
+ /** True when a waiter took the reply. */
49
+ settle(reply) {
50
+ return this.replies.settle(reply.cmdId, reply);
51
+ }
52
+ /** The next snapshot from `machineId`, or null at the deadline. */
53
+ waitSnapshot(machineId, timeoutMs) {
54
+ return this.snapshots.wait(machineId, timeoutMs);
55
+ }
56
+ /** Answer a status query in flight; null when there is nothing to wait for. */
57
+ settleSnapshot(machineId, jobs) {
58
+ this.snapshots.settle(machineId, jobs);
59
+ }
60
+ cancelAll() {
61
+ this.replies.cancelAll();
62
+ this.snapshots.cancelAll();
63
+ }
64
+ }
65
+ exports.ReplyWaiter = ReplyWaiter;