@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,23 @@
1
+ /**
2
+ * LineEscalations backed by the escalation API functions in process: the same
3
+ * calls the HTTP routes make, run as the user each actor maps to.
4
+ */
5
+ import type { Actor, CreateRowInput } from './actions';
6
+ import { type LineEscalations, type RowItem, type VerbResult } from './escalations';
7
+ import type { OpenRowQuery } from '../rows';
8
+ export declare class ApiLineEscalations implements LineEscalations {
9
+ private readonly userFor;
10
+ constructor(userFor: (as: Actor) => string);
11
+ private auth;
12
+ private verb;
13
+ claim(as: Actor, id: string): Promise<VerbResult>;
14
+ accumulate(as: Actor, id: string, itemKey: string, payload: Record<string, any>, metadata?: Record<string, any>): Promise<VerbResult>;
15
+ resolve(as: Actor, id: string, payload: Record<string, any>): Promise<VerbResult>;
16
+ create(as: Actor, input: CreateRowInput): Promise<{
17
+ id: string;
18
+ }>;
19
+ itemKeys(id: string): Promise<string[]>;
20
+ items(id: string): Promise<RowItem[]>;
21
+ get(id: string): Promise<Record<string, any> | null>;
22
+ listOpen(query: OpenRowQuery): Promise<Record<string, any>[]>;
23
+ }
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ /**
3
+ * LineEscalations backed by the escalation API functions in process: the same
4
+ * calls the HTTP routes make, run as the user each actor maps to.
5
+ */
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
18
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
19
+ }) : function(o, v) {
20
+ o["default"] = v;
21
+ });
22
+ var __importStar = (this && this.__importStar) || (function () {
23
+ var ownKeys = function(o) {
24
+ ownKeys = Object.getOwnPropertyNames || function (o) {
25
+ var ar = [];
26
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
27
+ return ar;
28
+ };
29
+ return ownKeys(o);
30
+ };
31
+ return function (mod) {
32
+ if (mod && mod.__esModule) return mod;
33
+ var result = {};
34
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
35
+ __setModuleDefault(result, mod);
36
+ return result;
37
+ };
38
+ })();
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ exports.ApiLineEscalations = void 0;
41
+ const escalationApi = __importStar(require("../../../api/escalations"));
42
+ const uuid_1 = require("../../../lib/uuid");
43
+ const escalations_1 = require("./escalations");
44
+ const actions_1 = require("./actions");
45
+ class ApiLineEscalations {
46
+ constructor(userFor) {
47
+ this.userFor = userFor;
48
+ }
49
+ auth(as) {
50
+ return { userId: this.userFor(as) };
51
+ }
52
+ verb(result) {
53
+ const outcome = (0, escalations_1.verbOutcomeOf)(result.status);
54
+ if (!outcome)
55
+ throw new Error(`escalation verb failed ${result.status}: ${result.error}`);
56
+ return outcome === escalations_1.VERB_OUTCOME.DONE ? { outcome } : { outcome, detail: result.error, status: result.status };
57
+ }
58
+ async claim(as, id) {
59
+ return this.verb(await escalationApi.claimEscalation({ id, durationMinutes: 60 }, this.auth(as)));
60
+ }
61
+ async accumulate(as, id, itemKey, payload, metadata) {
62
+ return this.verb(await escalationApi.accumulateItem({ id, itemKey, payload, ...(metadata ? { metadata } : {}) }, this.auth(as)));
63
+ }
64
+ async resolve(as, id, payload) {
65
+ return this.verb(await escalationApi.resolveEscalation({ id, resolverPayload: payload }, this.auth(as)));
66
+ }
67
+ async create(as, input) {
68
+ const result = await escalationApi.createEscalation({ ...input }, this.auth(as));
69
+ if (result.status >= 300)
70
+ throw new Error(`create failed ${result.status}: ${result.error}`);
71
+ return { id: result.data.id };
72
+ }
73
+ async itemKeys(id) {
74
+ return (await this.items(id)).map((i) => i.itemKey);
75
+ }
76
+ async items(id) {
77
+ const result = await escalationApi.getEscalationItems({ id }, this.auth(actions_1.CONTROLLER));
78
+ if (result.status >= 300)
79
+ throw new Error(`items failed ${result.status}: ${result.error}`);
80
+ return result.data.items.map((i) => ({ itemKey: i.itemKey, payload: i.payload ?? {} }));
81
+ }
82
+ async get(id) {
83
+ if (!(0, uuid_1.isUuid)(id))
84
+ return null;
85
+ const result = await escalationApi.getEscalation({ id }, this.auth(actions_1.CONTROLLER));
86
+ if (result.status === 403 || result.status === 404)
87
+ return null;
88
+ if (result.status >= 300)
89
+ throw new Error(`get failed ${result.status}: ${result.error}`);
90
+ return result.data;
91
+ }
92
+ async listOpen(query) {
93
+ return (0, escalations_1.readAllPages)(async (offset) => {
94
+ const result = await escalationApi.listEscalations({ ...query, status: 'pending', limit: escalations_1.OPEN_PAGE, offset, sort_by: 'created_at', order: 'asc' }, this.auth(actions_1.CONTROLLER));
95
+ if (result.status >= 300)
96
+ throw new Error(`list failed ${result.status}: ${result.error}`);
97
+ return result.data.escalations;
98
+ });
99
+ }
100
+ }
101
+ exports.ApiLineEscalations = ApiLineEscalations;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Commands to machines and their replies. A dispatch becomes one command per
3
+ * job (gateway/jobs.ts); missing replies are asked for again with the same
4
+ * command ids, and the row is claimed once every job is accepted. Silence is
5
+ * not a refusal: the gateway asks the machine what it holds, and a job it
6
+ * holds counts as accepted. When a dispatch ends without that, the row is
7
+ * resolved with the error and every job is cancelled, so a dispatch still
8
+ * queued for a machine that was away never runs later. A machine-command row is resolved with its command's
9
+ * reply. Other commands are tracked for their reply and logged. A command the
10
+ * codec has no message for is unsupported: nothing is sent or awaited.
11
+ */
12
+ import type { LineDef } from '../../../lib/machines/line';
13
+ import { type LineCodec } from '../../../lib/machines/protocol';
14
+ import type { MachineBus } from '../../../lib/machines/transport';
15
+ import { type Action } from './actions';
16
+ import { type LineEscalations } from './escalations';
17
+ import { type GatewayLog } from './log';
18
+ import type { CommandReply, ReplyWaiter } from './reply-waiter';
19
+ import { type GatewayState } from './state';
20
+ export interface DispatcherDeps {
21
+ bus: MachineBus;
22
+ codec: LineCodec;
23
+ line: LineDef;
24
+ lt: LineEscalations;
25
+ replies: ReplyWaiter;
26
+ state: GatewayState;
27
+ replyTimeoutMs: number;
28
+ dispatchAttempts: number;
29
+ log: GatewayLog;
30
+ }
31
+ type DispatchAction = Extract<Action, {
32
+ kind: 'dispatch';
33
+ }>;
34
+ type CommandAction = Extract<Action, {
35
+ kind: 'command';
36
+ }>;
37
+ type ControlAction = Extract<Action, {
38
+ kind: 'control';
39
+ }>;
40
+ export declare class Dispatcher {
41
+ private readonly d;
42
+ constructor(d: DispatcherDeps);
43
+ /** False when the codec has no message for the command. */
44
+ publish(machineId: string, op: string, cmdId: string, args: Record<string, any>): Promise<boolean>;
45
+ /** Publish and wait for the reply, asking again with the same id; null when none came. */
46
+ ask(machineId: string, op: string, cmdId: string, args: Record<string, any>): Promise<CommandReply | null>;
47
+ command(a: CommandAction): Promise<void>;
48
+ /**
49
+ * A machine-command row: send it, then resolve the row with the reply, as the
50
+ * machine that gave it. The command expires when the asking ends, so one that
51
+ * reaches a machine after its row resolved no-reply is refused, never run.
52
+ */
53
+ control(a: ControlAction): Promise<void>;
54
+ dispatch(a: DispatchAction): Promise<void>;
55
+ /** The units were already at the machine: its acceptance completes the handoff. */
56
+ private received;
57
+ /** Ask a silent machine what it holds; null when it cannot say or does not answer in time. */
58
+ private heldJobs;
59
+ private check;
60
+ /** Cancel jobs the machine accepted or may still receive; replies are only logged. */
61
+ private withdraw;
62
+ }
63
+ export {};
@@ -0,0 +1,157 @@
1
+ "use strict";
2
+ /**
3
+ * Commands to machines and their replies. A dispatch becomes one command per
4
+ * job (gateway/jobs.ts); missing replies are asked for again with the same
5
+ * command ids, and the row is claimed once every job is accepted. Silence is
6
+ * not a refusal: the gateway asks the machine what it holds, and a job it
7
+ * holds counts as accepted. When a dispatch ends without that, the row is
8
+ * resolved with the error and every job is cancelled, so a dispatch still
9
+ * queued for a machine that was away never runs later. A machine-command row is resolved with its command's
10
+ * reply. Other commands are tracked for their reply and logged. A command the
11
+ * codec has no message for is unsupported: nothing is sent or awaited.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.Dispatcher = void 0;
15
+ const promises_1 = require("timers/promises");
16
+ const contracts_1 = require("../contracts");
17
+ const registration_1 = require("./registration");
18
+ const protocol_1 = require("../../../lib/machines/protocol");
19
+ const actions_1 = require("./actions");
20
+ const escalations_1 = require("./escalations");
21
+ const jobs_1 = require("./jobs");
22
+ const log_1 = require("./log");
23
+ const state_1 = require("./state");
24
+ /** A reply the machine sent, as opposed to none or an unsupported command. */
25
+ function fromMachine(reply) {
26
+ return !!reply && reply.status !== contracts_1.COMMAND_STATUS.UNSUPPORTED;
27
+ }
28
+ /** The machine took the work: recorded it, or already has it ready. */
29
+ function took(reply) {
30
+ return reply?.status === protocol_1.REPLY_STATUS.ACCEPTED || reply?.status === protocol_1.REPLY_STATUS.COMPLETED;
31
+ }
32
+ class Dispatcher {
33
+ constructor(d) {
34
+ this.d = d;
35
+ }
36
+ /** False when the codec has no message for the command. */
37
+ async publish(machineId, op, cmdId, args) {
38
+ const wires = this.d.codec.encode((0, registration_1.identFor)(this.d.state, this.d.line, machineId), { cmdId, op, args });
39
+ for (const wire of wires)
40
+ await this.d.bus.publish(wire.topic, wire.payload);
41
+ return wires.length > 0;
42
+ }
43
+ /** Publish and wait for the reply, asking again with the same id; null when none came. */
44
+ async ask(machineId, op, cmdId, args) {
45
+ for (let attempt = 1; attempt <= this.d.dispatchAttempts; attempt++) {
46
+ const waiting = this.d.replies.wait(cmdId, this.d.replyTimeoutMs);
47
+ if (!await this.publish(machineId, op, cmdId, args)) {
48
+ this.d.replies.settle({ cmdId, status: contracts_1.COMMAND_STATUS.UNSUPPORTED, reason: `the line's codec has no ${op} command` });
49
+ }
50
+ const reply = await waiting;
51
+ if (reply)
52
+ return reply;
53
+ }
54
+ return null;
55
+ }
56
+ async command(a) {
57
+ const reply = await this.ask(a.machineId, a.op, a.cmdId, a.args);
58
+ if (!reply)
59
+ this.d.log.warn(log_1.GATEWAY_LOG.COMMAND_UNANSWERED, { machine: a.machineId, op: a.op, cmdId: a.cmdId });
60
+ else if (reply.status === contracts_1.COMMAND_STATUS.UNSUPPORTED)
61
+ this.d.log.info(log_1.GATEWAY_LOG.COMMAND_UNSUPPORTED, { machine: a.machineId, op: a.op });
62
+ else if (reply.status !== protocol_1.REPLY_STATUS.ACCEPTED) {
63
+ this.d.log.warn(log_1.GATEWAY_LOG.COMMAND_REFUSED, { machine: a.machineId, op: a.op, cmdId: a.cmdId, status: reply.status, reason: reply.reason });
64
+ }
65
+ }
66
+ /**
67
+ * A machine-command row: send it, then resolve the row with the reply, as the
68
+ * machine that gave it. The command expires when the asking ends, so one that
69
+ * reaches a machine after its row resolved no-reply is refused, never run.
70
+ */
71
+ async control(a) {
72
+ const expiresAt = Date.now() + this.d.replyTimeoutMs * this.d.dispatchAttempts;
73
+ const reply = await this.ask(a.machineId, a.op, a.cmdId, { ...a.args, expiresAt: (0, protocol_1.isoTs)(expiresAt) });
74
+ // Timers may fire a little early; the command must have expired before the row says no-reply.
75
+ if (!reply && Date.now() <= expiresAt)
76
+ await (0, promises_1.setTimeout)(expiresAt - Date.now() + 1);
77
+ const outcome = reply
78
+ ? { accepted: reply.status === protocol_1.REPLY_STATUS.ACCEPTED, machineId: a.machineId, op: a.op, status: reply.status, reason: reply.reason }
79
+ : { accepted: false, machineId: a.machineId, op: a.op, status: contracts_1.COMMAND_STATUS.NO_REPLY, reason: `no reply after ${this.d.dispatchAttempts} attempts` };
80
+ const context = { row: a.rowId, machine: a.machineId, op: a.op, status: outcome.status, reason: outcome.reason };
81
+ if (outcome.accepted)
82
+ this.d.log.info(log_1.GATEWAY_LOG.COMMANDED, context);
83
+ else
84
+ this.d.log.warn(!reply ? log_1.GATEWAY_LOG.COMMAND_UNANSWERED : fromMachine(reply) ? log_1.GATEWAY_LOG.COMMAND_REFUSED : log_1.GATEWAY_LOG.COMMAND_UNSUPPORTED, context);
85
+ this.check(a.rowId, 'resolve', await this.d.lt.resolve(fromMachine(reply) ? a.machineId : actions_1.CONTROLLER, a.rowId, outcome));
86
+ }
87
+ async dispatch(a) {
88
+ const { lt, state, log } = this.d;
89
+ if (a.receivedOnAccept) {
90
+ // An arrival may be delivered again after the job went out; send it once.
91
+ const row = state.get(a.rowId);
92
+ if (row?.dispatch !== state_1.DISPATCH_STATE.ARRIVING)
93
+ return;
94
+ row.dispatch = state_1.DISPATCH_STATE.PENDING;
95
+ }
96
+ const jobs = (0, jobs_1.dispatchJobs)(a.args.jobId, a.args.units, state.unitsPerDispatch);
97
+ // Known before any reply, so a failure that follows an acceptance finds its row.
98
+ for (const job of jobs)
99
+ state.rememberDispatch(`${a.cmdId}${job.suffix}`, a.rowId);
100
+ const replies = await Promise.all(jobs.map((job) => this.ask(a.machineId, protocol_1.OP.DISPATCH, `${a.cmdId}${job.suffix}`, { ...a.args, jobId: job.jobId, units: job.units })));
101
+ const refused = replies.find((r) => r && !took(r));
102
+ const silent = jobs.filter((_, i) => !replies[i]);
103
+ const held = !refused && silent.length ? await this.heldJobs(a.machineId, a.cmdId) : null;
104
+ const reconciled = !!held && silent.every((j) => held.has(j.jobId));
105
+ const row = state.get(a.rowId);
106
+ if (!refused && (!silent.length || reconciled)) {
107
+ if (row)
108
+ row.dispatch = state_1.DISPATCH_STATE.ACCEPTED;
109
+ if (reconciled)
110
+ log.info(log_1.GATEWAY_LOG.DISPATCH_RECONCILED, { row: a.rowId, machine: a.machineId, jobs: silent.map((j) => j.jobId) });
111
+ log.info(log_1.GATEWAY_LOG.DISPATCHED, { row: a.rowId, machine: a.machineId, reason: replies.map((r) => r?.reason ?? 'held').join('; ') });
112
+ this.check(a.rowId, 'claim', await lt.claim(a.machineId, a.rowId));
113
+ if (a.receivedOnAccept)
114
+ await this.received(a);
115
+ return;
116
+ }
117
+ // The handoff may have ended meanwhile: a machine that ran the job anyway reports the arrival.
118
+ if (!row)
119
+ return;
120
+ row.dispatch = state_1.DISPATCH_STATE.REFUSED;
121
+ const unanswered = `${a.machineId} sent no reply after ${this.d.dispatchAttempts} attempts`;
122
+ const resolution = refused
123
+ ? { received: false, errorCode: contracts_1.HANDOFF_ERROR.DISPATCH_REFUSED, detail: `${a.machineId} ${refused.status}: ${refused.reason}` }
124
+ : { received: false, errorCode: contracts_1.HANDOFF_ERROR.NO_REPLY, detail: held ? `${unanswered} and does not hold the job` : `${unanswered}; its state is unknown` };
125
+ log.warn(log_1.GATEWAY_LOG.DISPATCH_REFUSED, { row: a.rowId, machine: a.machineId, ...resolution });
126
+ const ended = await lt.resolve(fromMachine(refused ?? null) ? a.machineId : actions_1.CONTROLLER, a.rowId, resolution);
127
+ this.check(a.rowId, 'resolve', ended);
128
+ state.close(a.rowId);
129
+ if (ended.outcome !== escalations_1.VERB_OUTCOME.DONE)
130
+ return;
131
+ await this.withdraw(a, jobs.filter((_, i) => !replies[i] || took(replies[i])));
132
+ }
133
+ /** The units were already at the machine: its acceptance completes the handoff. */
134
+ async received(a) {
135
+ const units = a.args.units.map((u) => u.unitId);
136
+ this.check(a.rowId, 'resolve', await this.d.lt.resolve(a.machineId, a.rowId, { received: true, receiver: a.machineId, units }));
137
+ this.d.state.close(a.rowId);
138
+ }
139
+ /** Ask a silent machine what it holds; null when it cannot say or does not answer in time. */
140
+ async heldJobs(machineId, cmdId) {
141
+ const waiting = this.d.replies.waitSnapshot(machineId, this.d.replyTimeoutMs);
142
+ if (!await this.publish(machineId, protocol_1.OP.SNAPSHOT, `${cmdId}:status`, {}))
143
+ this.d.replies.settleSnapshot(machineId, null);
144
+ return waiting;
145
+ }
146
+ check(rowId, verb, result) {
147
+ if (result.outcome === escalations_1.VERB_OUTCOME.INVALID)
148
+ this.d.log.error(log_1.GATEWAY_LOG.VERB_INVALID, { action: verb, row: rowId, detail: result.detail });
149
+ }
150
+ /** Cancel jobs the machine accepted or may still receive; replies are only logged. */
151
+ async withdraw(a, jobs) {
152
+ await Promise.all(jobs.map((job) => this.command({
153
+ kind: 'command', machineId: a.machineId, op: protocol_1.OP.CANCEL, cmdId: `${(0, contracts_1.cancelCommandId)(a.rowId)}${job.suffix}`, args: { jobId: job.jobId },
154
+ })));
155
+ }
156
+ }
157
+ exports.Dispatcher = Dispatcher;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The escalation verbs the gateway performs, as an interface so the executor
3
+ * runs the same against Long Tail over HTTP or an in-memory fake. Outcomes
4
+ * the gateway treats as done (the row already ended, or the claim is already
5
+ * held) are not errors: replays and races land there.
6
+ */
7
+ import type { OpenRowQuery } from '../rows';
8
+ import type { Actor, CreateRowInput } from './actions';
9
+ export declare const VERB_OUTCOME: {
10
+ readonly DONE: "done";
11
+ /** 404, or 409 because the row already ended: nothing left to do. */
12
+ readonly ALREADY: "already";
13
+ /** 422: the payload does not satisfy the role's form schema. */
14
+ readonly INVALID: "invalid";
15
+ };
16
+ export type VerbOutcome = typeof VERB_OUTCOME[keyof typeof VERB_OUTCOME];
17
+ export interface VerbResult {
18
+ outcome: VerbOutcome;
19
+ detail?: string;
20
+ /** The HTTP status behind an `already` or `invalid` outcome. */
21
+ status?: number;
22
+ /** After an accumulate: the item keys the row now holds, when the response carries them. */
23
+ itemKeys?: string[];
24
+ }
25
+ export interface RowItem {
26
+ itemKey: string;
27
+ payload: Record<string, any>;
28
+ }
29
+ export interface LineEscalations {
30
+ claim(as: Actor, id: string): Promise<VerbResult>;
31
+ /** `metadata` is merged (shallow) into the row's metadata in the same statement as the item. */
32
+ accumulate(as: Actor, id: string, itemKey: string, payload: Record<string, any>, metadata?: Record<string, any>): Promise<VerbResult>;
33
+ resolve(as: Actor, id: string, payload: Record<string, any>): Promise<VerbResult>;
34
+ create(as: Actor, input: CreateRowInput): Promise<{
35
+ id: string;
36
+ }>;
37
+ /** The item keys an accumulator row holds. */
38
+ itemKeys(id: string): Promise<string[]>;
39
+ /** The items an accumulator row holds, with their payloads. */
40
+ items(id: string): Promise<RowItem[]>;
41
+ /** The stored row, as the controller sees it; null when it does not exist or the controller may not read it. */
42
+ get(id: string): Promise<Record<string, any> | null>;
43
+ /** Every pending row matching each given field, oldest first, in the projection escalation events carry. */
44
+ listOpen(query: OpenRowQuery): Promise<Record<string, any>[]>;
45
+ }
46
+ /** Rows per page when reading open rows back. */
47
+ export declare const OPEN_PAGE = 500;
48
+ /** Read every page of `page(offset)`, oldest first, until a short page. */
49
+ export declare function readAllPages(page: (offset: number) => Promise<Record<string, any>[]>): Promise<Record<string, any>[]>;
50
+ /** Map an HTTP status to a verb outcome; anything else is a real failure. */
51
+ export declare function verbOutcomeOf(status: number): VerbOutcome | null;
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ /**
3
+ * The escalation verbs the gateway performs, as an interface so the executor
4
+ * runs the same against Long Tail over HTTP or an in-memory fake. Outcomes
5
+ * the gateway treats as done (the row already ended, or the claim is already
6
+ * held) are not errors: replays and races land there.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.OPEN_PAGE = exports.VERB_OUTCOME = void 0;
10
+ exports.readAllPages = readAllPages;
11
+ exports.verbOutcomeOf = verbOutcomeOf;
12
+ exports.VERB_OUTCOME = {
13
+ DONE: 'done',
14
+ /** 404, or 409 because the row already ended: nothing left to do. */
15
+ ALREADY: 'already',
16
+ /** 422: the payload does not satisfy the role's form schema. */
17
+ INVALID: 'invalid',
18
+ };
19
+ /** Rows per page when reading open rows back. */
20
+ exports.OPEN_PAGE = 500;
21
+ /** Read every page of `page(offset)`, oldest first, until a short page. */
22
+ async function readAllPages(page) {
23
+ const rows = [];
24
+ for (let offset = 0;; offset += exports.OPEN_PAGE) {
25
+ const batch = await page(offset);
26
+ rows.push(...batch);
27
+ if (batch.length < exports.OPEN_PAGE)
28
+ return rows;
29
+ }
30
+ }
31
+ /** Map an HTTP status to a verb outcome; anything else is a real failure. */
32
+ function verbOutcomeOf(status) {
33
+ if (status >= 200 && status < 300)
34
+ return exports.VERB_OUTCOME.DONE;
35
+ if (status === 404 || status === 409)
36
+ return exports.VERB_OUTCOME.ALREADY;
37
+ if (status === 422)
38
+ return exports.VERB_OUTCOME.INVALID;
39
+ return null;
40
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Runs the planner's actions: escalation verbs through LineEscalations,
3
+ * dispatches and commands through the Dispatcher, alarm and machine rows,
4
+ * and the offline debounce. A verb that fails for real throws, so the message that
5
+ * caused it is delivered again.
6
+ */
7
+ import { type LineDef } from '../../../lib/machines/line';
8
+ import { type LineCodec } from '../../../lib/machines/protocol';
9
+ import type { MachineBus } from '../../../lib/machines/transport';
10
+ import { type Action } from './actions';
11
+ import type { AddressBook } from './address-book';
12
+ import { type LineEscalations } from './escalations';
13
+ import { type GatewayLog } from './log';
14
+ import type { ReplyWaiter } from './reply-waiter';
15
+ import type { GatewayState } from './state';
16
+ export interface ExecutorDeps {
17
+ bus: MachineBus;
18
+ lt: LineEscalations;
19
+ replies: ReplyWaiter;
20
+ state: GatewayState;
21
+ line: LineDef;
22
+ codec: LineCodec;
23
+ replyTimeoutMs: number;
24
+ dispatchAttempts: number;
25
+ offlineDebounceMs: number;
26
+ log: GatewayLog;
27
+ /** Where learned addresses are kept (registration.ts). */
28
+ addresses: AddressBook;
29
+ }
30
+ export declare class Executor {
31
+ private readonly d;
32
+ private inflight;
33
+ private quietPending;
34
+ private timers;
35
+ private readonly dispatcher;
36
+ constructor(d: ExecutorDeps);
37
+ /** Dispatches and commands run detached so one slow reply never holds up the feed. */
38
+ apply(actions: Action[]): Promise<void>;
39
+ /** Resolves when every detached dispatch has finished. */
40
+ idle(): Promise<void>;
41
+ stop(): void;
42
+ private track;
43
+ private run;
44
+ /** A dispatch's second reply: ready, or a failure to prepare that ends the handoff. */
45
+ private lateReply;
46
+ /** Add the item; when the row then holds every key the action waits for, resolve it. */
47
+ private accumulate;
48
+ private check;
49
+ private createAlarmRow;
50
+ /** Raise a quiet row for each connected machine silent longer than `quietMs`. */
51
+ checkQuiet(machineIds: string[], quietMs: number): void;
52
+ private raiseQuiet;
53
+ private watchOffline;
54
+ private raiseOffline;
55
+ }