@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.
- package/README.md +1 -0
- package/build/index.d.ts +5 -0
- package/build/index.js +36 -7
- package/build/lib/events/nats.d.ts +11 -0
- package/build/lib/events/nats.js +33 -2
- package/build/lib/machines/index.d.ts +6 -0
- package/build/lib/machines/index.js +22 -0
- package/build/lib/machines/line.d.ts +44 -0
- package/build/lib/machines/line.js +114 -0
- package/build/lib/machines/protocol/codec.d.ts +108 -0
- package/build/lib/machines/protocol/codec.js +80 -0
- package/build/lib/machines/protocol/constants.d.ts +120 -0
- package/build/lib/machines/protocol/constants.js +97 -0
- package/build/lib/machines/protocol/contract.d.ts +19 -0
- package/build/lib/machines/protocol/contract.js +141 -0
- package/build/lib/machines/protocol/envelope.d.ts +14 -0
- package/build/lib/machines/protocol/envelope.js +43 -0
- package/build/lib/machines/protocol/index.d.ts +5 -0
- package/build/lib/machines/protocol/index.js +21 -0
- package/build/lib/machines/protocol/topics.d.ts +33 -0
- package/build/lib/machines/protocol/topics.js +79 -0
- package/build/lib/machines/site/edge-account.d.ts +11 -0
- package/build/lib/machines/site/edge-account.js +24 -0
- package/build/lib/machines/site/mosquitto-conf.d.ts +27 -0
- package/build/lib/machines/site/mosquitto-conf.js +81 -0
- package/build/lib/machines/site/site-conf.d.ts +23 -0
- package/build/lib/machines/site/site-conf.js +62 -0
- package/build/lib/machines/transport/callback-feed.d.ts +18 -0
- package/build/lib/machines/transport/callback-feed.js +31 -0
- package/build/lib/machines/transport/delivery.d.ts +20 -0
- package/build/lib/machines/transport/delivery.js +37 -0
- package/build/lib/machines/transport/index.d.ts +13 -0
- package/build/lib/machines/transport/index.js +29 -0
- package/build/lib/machines/transport/memory-broker.d.ts +35 -0
- package/build/lib/machines/transport/memory-broker.js +145 -0
- package/build/lib/machines/transport/memory.d.ts +40 -0
- package/build/lib/machines/transport/memory.js +79 -0
- package/build/lib/machines/transport/mqtt-bus.d.ts +24 -0
- package/build/lib/machines/transport/mqtt-bus.js +43 -0
- package/build/lib/machines/transport/mqtt-link.d.ts +16 -0
- package/build/lib/machines/transport/mqtt-link.js +37 -0
- package/build/lib/machines/transport/mqtt-session.d.ts +34 -0
- package/build/lib/machines/transport/mqtt-session.js +127 -0
- package/build/lib/machines/transport/nats-bus.d.ts +36 -0
- package/build/lib/machines/transport/nats-bus.js +47 -0
- package/build/lib/machines/transport/nats-feed.d.ts +26 -0
- package/build/lib/machines/transport/nats-feed.js +49 -0
- package/build/lib/machines/transport/pubsub-feed.d.ts +17 -0
- package/build/lib/machines/transport/pubsub-feed.js +39 -0
- package/build/lib/machines/transport/serial.d.ts +21 -0
- package/build/lib/machines/transport/serial.js +54 -0
- package/build/lib/machines/transport/stream-bus.d.ts +40 -0
- package/build/lib/machines/transport/stream-bus.js +57 -0
- package/build/lib/machines/transport/streams.d.ts +24 -0
- package/build/lib/machines/transport/streams.js +49 -0
- package/build/lib/messaging/config.d.ts +25 -0
- package/build/lib/messaging/config.js +55 -0
- package/build/lib/messaging/constants.d.ts +26 -0
- package/build/lib/messaging/constants.js +26 -0
- package/build/lib/messaging/index.d.ts +24 -0
- package/build/lib/messaging/index.js +65 -0
- package/build/lib/messaging/memory.d.ts +16 -0
- package/build/lib/messaging/memory.js +147 -0
- package/build/lib/messaging/nats/permissions.d.ts +13 -0
- package/build/lib/messaging/nats/permissions.js +22 -0
- package/build/lib/messaging/nats/provider.d.ts +30 -0
- package/build/lib/messaging/nats/provider.js +77 -0
- package/build/lib/messaging/nats/render-conf.d.ts +68 -0
- package/build/lib/messaging/nats/render-conf.js +58 -0
- package/build/lib/messaging/nats/streams.d.ts +26 -0
- package/build/lib/messaging/nats/streams.js +97 -0
- package/build/lib/messaging/subjects.d.ts +8 -0
- package/build/lib/messaging/subjects.js +25 -0
- package/build/scripts/bambu-boat.d.ts +17 -0
- package/build/scripts/bambu-boat.js +134 -0
- package/build/scripts/bambu-print.d.ts +18 -0
- package/build/scripts/bambu-print.js +114 -0
- package/build/scripts/machine-line.d.ts +33 -0
- package/build/scripts/machine-line.js +301 -0
- package/build/services/machines/commands.d.ts +29 -0
- package/build/services/machines/commands.js +37 -0
- package/build/services/machines/config.d.ts +49 -0
- package/build/services/machines/config.js +94 -0
- package/build/services/machines/contracts.d.ts +210 -0
- package/build/services/machines/contracts.js +124 -0
- package/build/services/machines/forms.d.ts +55 -0
- package/build/services/machines/forms.js +37 -0
- package/build/services/machines/gateway/actions.d.ts +111 -0
- package/build/services/machines/gateway/actions.js +8 -0
- package/build/services/machines/gateway/address-book.d.ts +16 -0
- package/build/services/machines/gateway/address-book.js +24 -0
- package/build/services/machines/gateway/admission.d.ts +35 -0
- package/build/services/machines/gateway/admission.js +66 -0
- package/build/services/machines/gateway/api-escalations.d.ts +23 -0
- package/build/services/machines/gateway/api-escalations.js +101 -0
- package/build/services/machines/gateway/dispatcher.d.ts +76 -0
- package/build/services/machines/gateway/dispatcher.js +197 -0
- package/build/services/machines/gateway/escalations.d.ts +51 -0
- package/build/services/machines/gateway/escalations.js +40 -0
- package/build/services/machines/gateway/executor.d.ts +60 -0
- package/build/services/machines/gateway/executor.js +221 -0
- package/build/services/machines/gateway/gateway.d.ts +88 -0
- package/build/services/machines/gateway/gateway.js +270 -0
- package/build/services/machines/gateway/index.d.ts +10 -0
- package/build/services/machines/gateway/index.js +26 -0
- package/build/services/machines/gateway/jobs.d.ts +16 -0
- package/build/services/machines/gateway/jobs.js +24 -0
- package/build/services/machines/gateway/lifecycle.d.ts +21 -0
- package/build/services/machines/gateway/lifecycle.js +89 -0
- package/build/services/machines/gateway/log.d.ts +41 -0
- package/build/services/machines/gateway/log.js +42 -0
- package/build/services/machines/gateway/lt-http.d.ts +51 -0
- package/build/services/machines/gateway/lt-http.js +120 -0
- package/build/services/machines/gateway/pacing.d.ts +43 -0
- package/build/services/machines/gateway/pacing.js +158 -0
- package/build/services/machines/gateway/plan-escalation.d.ts +19 -0
- package/build/services/machines/gateway/plan-escalation.js +210 -0
- package/build/services/machines/gateway/plan-machine.d.ts +17 -0
- package/build/services/machines/gateway/plan-machine.js +283 -0
- package/build/services/machines/gateway/plan-shared.d.ts +18 -0
- package/build/services/machines/gateway/plan-shared.js +36 -0
- package/build/services/machines/gateway/provision.d.ts +22 -0
- package/build/services/machines/gateway/provision.js +79 -0
- package/build/services/machines/gateway/receipts.d.ts +33 -0
- package/build/services/machines/gateway/receipts.js +65 -0
- package/build/services/machines/gateway/reconcile.d.ts +35 -0
- package/build/services/machines/gateway/reconcile.js +99 -0
- package/build/services/machines/gateway/registration.d.ts +38 -0
- package/build/services/machines/gateway/registration.js +121 -0
- package/build/services/machines/gateway/reply-waiter.d.ts +24 -0
- package/build/services/machines/gateway/reply-waiter.js +65 -0
- package/build/services/machines/gateway/state.d.ts +145 -0
- package/build/services/machines/gateway/state.js +241 -0
- package/build/services/machines/gateway/trace.d.ts +16 -0
- package/build/services/machines/gateway/trace.js +43 -0
- package/build/services/machines/identities.d.ts +25 -0
- package/build/services/machines/identities.js +77 -0
- package/build/services/machines/index.d.ts +11 -0
- package/build/services/machines/index.js +27 -0
- package/build/services/machines/leadership.d.ts +35 -0
- package/build/services/machines/leadership.js +100 -0
- package/build/services/machines/roles.d.ts +13 -0
- package/build/services/machines/roles.js +62 -0
- package/build/services/machines/rows.d.ts +30 -0
- package/build/services/machines/rows.js +71 -0
- package/build/services/machines/select.d.ts +25 -0
- package/build/services/machines/select.js +76 -0
- package/build/services/machines/surface.d.ts +33 -0
- package/build/services/machines/surface.js +104 -0
- package/build/services/machines/testing/fake-long-tail.d.ts +79 -0
- package/build/services/machines/testing/fake-long-tail.js +158 -0
- package/build/services/machines/testing/index.d.ts +3 -0
- package/build/services/machines/testing/index.js +19 -0
- package/build/services/machines/testing/recording-log.d.ts +14 -0
- package/build/services/machines/testing/recording-log.js +22 -0
- package/build/services/machines/testing/wait.d.ts +3 -0
- package/build/services/machines/testing/wait.js +17 -0
- package/build/services/mcp/db-server/schemas.d.ts +2 -2
- package/build/start/adapters.js +11 -2
- package/build/start/index.js +20 -1
- package/build/system/mcp-servers/admin/schemas.d.ts +221 -221
- package/build/system/mcp-servers/db-query/schemas.d.ts +2 -2
- package/build/system/mcp-servers/human-queue-schemas.d.ts +6 -6
- package/build/tsconfig.tsbuildinfo +1 -1
- package/build/types/index.d.ts +3 -1
- package/build/types/machines.d.ts +167 -0
- package/build/types/machines.js +7 -0
- package/build/types/messaging.d.ts +72 -0
- package/build/types/messaging.js +11 -0
- package/build/types/startup.d.ts +49 -1
- package/docs/events.md +50 -4
- package/docs/machines.md +322 -0
- package/package.json +13 -1
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Job lifecycle on the row. A machine's started and progress events, and the
|
|
3
|
+
* alarms that stop it, are noted as items on the open row that holds the job:
|
|
4
|
+
* the accepted dispatch into the machine until its units arrive, then the
|
|
5
|
+
* handoff out of it. Progress is noted only when it crosses a step, or reaches
|
|
6
|
+
* a named stage it has not reached before, so a row carries a handful of items
|
|
7
|
+
* while the stream keeps every report.
|
|
8
|
+
*
|
|
9
|
+
* Off unless the line turns it on (MACHINE_LIFECYCLE=on). A row parked with a
|
|
10
|
+
* limit on its items gets no notes, so they never use a slot its units need.
|
|
11
|
+
*/
|
|
12
|
+
import type { Action } from './actions';
|
|
13
|
+
import { type AlarmTrack, type GatewayState } from './state';
|
|
14
|
+
/** The progress step `percent` has reached; null below the first step, at 100, or when steps are off. */
|
|
15
|
+
export declare function progressStepOf(percent: unknown, step: number): number | null;
|
|
16
|
+
/** A started or progress event, noted on the row that holds the job. */
|
|
17
|
+
export declare function noteJobEvent(state: GatewayState, m: string, b: Record<string, any>, at: string | undefined): Action[];
|
|
18
|
+
/** Rows a stopping alarm pauses: those running or leaving with its job, or every row the machine holds. */
|
|
19
|
+
export declare function notePause(state: GatewayState, track: AlarmTrack, b: Record<string, any>): Action[];
|
|
20
|
+
/** The machine cleared an alarm: the rows it paused, still open, note the resume. */
|
|
21
|
+
export declare function noteResume(state: GatewayState, track: AlarmTrack, b: Record<string, any>): Action[];
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Job lifecycle on the row. A machine's started and progress events, and the
|
|
4
|
+
* alarms that stop it, are noted as items on the open row that holds the job:
|
|
5
|
+
* the accepted dispatch into the machine until its units arrive, then the
|
|
6
|
+
* handoff out of it. Progress is noted only when it crosses a step, or reaches
|
|
7
|
+
* a named stage it has not reached before, so a row carries a handful of items
|
|
8
|
+
* while the stream keeps every report.
|
|
9
|
+
*
|
|
10
|
+
* Off unless the line turns it on (MACHINE_LIFECYCLE=on). A row parked with a
|
|
11
|
+
* limit on its items gets no notes, so they never use a slot its units need.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.progressStepOf = progressStepOf;
|
|
15
|
+
exports.noteJobEvent = noteJobEvent;
|
|
16
|
+
exports.notePause = notePause;
|
|
17
|
+
exports.noteResume = noteResume;
|
|
18
|
+
const contracts_1 = require("../contracts");
|
|
19
|
+
const protocol_1 = require("../../../lib/machines/protocol");
|
|
20
|
+
const jobs_1 = require("./jobs");
|
|
21
|
+
const state_1 = require("./state");
|
|
22
|
+
const owns = (jobId, reported) => !jobId || typeof reported !== 'string' || (0, jobs_1.jobOwns)(jobId, reported);
|
|
23
|
+
const runningAt = (r, m, jobId) => r.toMachine === m && r.dispatch === state_1.DISPATCH_STATE.ACCEPTED && owns(r.jobId, jobId);
|
|
24
|
+
const leavingFrom = (r, m, jobId) => r.fromMachine === m && !!r.senderJobId && owns(r.senderJobId, jobId);
|
|
25
|
+
/** The open row holding `unitId` at `m`: its accepted dispatch, else the handoff out of `m`. */
|
|
26
|
+
function holderRow(state, m, unitId, jobId) {
|
|
27
|
+
return state.openFor(unitId, (r) => runningAt(r, m, jobId)) ?? state.openFor(unitId, (r) => leavingFrom(r, m, jobId));
|
|
28
|
+
}
|
|
29
|
+
/** The progress step `percent` has reached; null below the first step, at 100, or when steps are off. */
|
|
30
|
+
function progressStepOf(percent, step) {
|
|
31
|
+
if (step <= 0 || typeof percent !== 'number' || !Number.isFinite(percent))
|
|
32
|
+
return null;
|
|
33
|
+
const reached = Math.floor(percent / step) * step;
|
|
34
|
+
return reached > 0 && reached < 100 ? reached : null;
|
|
35
|
+
}
|
|
36
|
+
/** An item the row does not hold yet, added as the machine. */
|
|
37
|
+
function note(row, m, itemKey, payload, metadata) {
|
|
38
|
+
if (row.capped || row.noted?.includes(itemKey))
|
|
39
|
+
return [];
|
|
40
|
+
row.noted = [...(row.noted ?? []), itemKey];
|
|
41
|
+
return [{ kind: 'accumulate', as: m, rowId: row.id, itemKey, payload, ...(metadata ? { metadata } : {}) }];
|
|
42
|
+
}
|
|
43
|
+
/** A started or progress event, noted on the row that holds the job. */
|
|
44
|
+
function noteJobEvent(state, m, b, at) {
|
|
45
|
+
const unitId = b.unitId;
|
|
46
|
+
if (!state.lifecycle || typeof unitId !== 'string')
|
|
47
|
+
return [];
|
|
48
|
+
const row = holderRow(state, m, unitId, b.jobId);
|
|
49
|
+
if (!row)
|
|
50
|
+
return [];
|
|
51
|
+
const when = at ?? new Date().toISOString();
|
|
52
|
+
if (b.event === protocol_1.JOB_EVENT.STARTED) {
|
|
53
|
+
return note(row, m, contracts_1.itemKeys.started(unitId), { unitId, jobId: b.jobId ?? null, at: when });
|
|
54
|
+
}
|
|
55
|
+
const step = progressStepOf(b.percent, state.progressStep);
|
|
56
|
+
const stage = typeof b.stage === 'string' && b.stage ? b.stage : null;
|
|
57
|
+
if (step === null && stage === null)
|
|
58
|
+
return [];
|
|
59
|
+
const progress = {
|
|
60
|
+
...(typeof b.percent === 'number' ? { percent: b.percent } : {}), remainingSec: b.remainingSec ?? null, ...(stage ? { stage } : {}), at: when,
|
|
61
|
+
...(b.detail && typeof b.detail === 'object' ? { detail: b.detail } : {}),
|
|
62
|
+
};
|
|
63
|
+
const facet = { [contracts_1.LIFECYCLE_FACET.PROGRESS]: progress };
|
|
64
|
+
const job = { unitId, jobId: b.jobId ?? null };
|
|
65
|
+
return [
|
|
66
|
+
...(step === null ? [] : note(row, m, contracts_1.itemKeys.progress(unitId, step), { ...job, step, ...progress }, facet)),
|
|
67
|
+
...(stage === null ? [] : note(row, m, contracts_1.itemKeys.stage(unitId, stage), { ...job, ...progress }, facet)),
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
/** Rows a stopping alarm pauses: those running or leaving with its job, or every row the machine holds. */
|
|
71
|
+
function notePause(state, track, b) {
|
|
72
|
+
if (!state.lifecycle)
|
|
73
|
+
return [];
|
|
74
|
+
const m = track.machineId;
|
|
75
|
+
const jobId = typeof b.jobId === 'string' ? b.jobId : undefined;
|
|
76
|
+
const rows = state.open().filter((r) => runningAt(r, m, jobId) || leavingFrom(r, m, jobId));
|
|
77
|
+
track.noted = rows.map((r) => r.id);
|
|
78
|
+
const payload = { alarmId: track.alarmId, code: b.code ?? null, level: b.level ?? null, text: b.text ?? null, raisedAt: b.raisedAt ?? null };
|
|
79
|
+
return rows.flatMap((r) => note(r, m, contracts_1.itemKeys.paused(track.alarmId, String(b.raisedAt ?? '')), payload));
|
|
80
|
+
}
|
|
81
|
+
/** The machine cleared an alarm: the rows it paused, still open, note the resume. */
|
|
82
|
+
function noteResume(state, track, b) {
|
|
83
|
+
const key = contracts_1.itemKeys.resumed(track.alarmId, String(track.raisedAt ?? ''));
|
|
84
|
+
const payload = { alarmId: track.alarmId, clearedAt: b.clearedAt ?? new Date().toISOString(), clearedBy: b.clearedBy ?? null };
|
|
85
|
+
return (track.noted ?? [])
|
|
86
|
+
.map((id) => state.get(id))
|
|
87
|
+
.filter((r) => !!r && !r.ended)
|
|
88
|
+
.flatMap((r) => note(r, track.machineId, key, payload));
|
|
89
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export declare const GATEWAY_LOG: {
|
|
2
|
+
readonly STARTED: "machine_gateway.started";
|
|
3
|
+
readonly RECONCILED: "machine_gateway.reconciled";
|
|
4
|
+
readonly DISPATCHED: "machine_gateway.dispatch.accepted";
|
|
5
|
+
readonly DISPATCH_REFUSED: "machine_gateway.dispatch.refused";
|
|
6
|
+
readonly DISPATCH_READY: "machine_gateway.dispatch.ready";
|
|
7
|
+
readonly DISPATCH_FAILED: "machine_gateway.dispatch.failed";
|
|
8
|
+
readonly DISPATCH_RECONCILED: "machine_gateway.dispatch.reconciled";
|
|
9
|
+
readonly VERB_INVALID: "machine_gateway.verb.invalid";
|
|
10
|
+
readonly VERB_FAILED: "machine_gateway.verb.failed";
|
|
11
|
+
readonly ROW_NOT_VISIBLE: "machine_gateway.verb.row_not_visible";
|
|
12
|
+
readonly ROW_CLAIMED_ELSEWHERE: "machine_gateway.verb.row_claimed_elsewhere";
|
|
13
|
+
readonly UNMATCHED: "machine_gateway.message.unmatched";
|
|
14
|
+
readonly UNREADABLE: "machine_gateway.message.unreadable";
|
|
15
|
+
readonly CONTRACT_VIOLATION: "machine_gateway.contract.violation";
|
|
16
|
+
readonly VIOLATION_ROW: "machine_gateway.contract.row_created";
|
|
17
|
+
readonly ADDRESS_BOUND: "machine_gateway.address.bound";
|
|
18
|
+
readonly ADDRESS_CONTESTED: "machine_gateway.address.contested";
|
|
19
|
+
readonly ADDRESS_UNKNOWN: "machine_gateway.address.unknown";
|
|
20
|
+
readonly ADDRESS_REFUSED: "machine_gateway.address.refused";
|
|
21
|
+
readonly RECEIPT_FAILED: "machine_gateway.receipt.failed";
|
|
22
|
+
readonly ALARM_ROW: "machine_gateway.alarm.row_created";
|
|
23
|
+
readonly OFFLINE_ROW: "machine_gateway.offline.row_created";
|
|
24
|
+
readonly QUIET_ROW: "machine_gateway.quiet.row_created";
|
|
25
|
+
readonly COMMANDED: "machine_gateway.command.accepted";
|
|
26
|
+
readonly COMMAND_REFUSED: "machine_gateway.command.refused";
|
|
27
|
+
readonly COMMAND_UNANSWERED: "machine_gateway.command.unanswered";
|
|
28
|
+
readonly COMMAND_UNSUPPORTED: "machine_gateway.command.unsupported";
|
|
29
|
+
readonly LEADING: "machine_gateway.line.leading";
|
|
30
|
+
readonly STANDBY: "machine_gateway.line.standby";
|
|
31
|
+
readonly LEADERSHIP_LOST: "machine_gateway.line.leadership_lost";
|
|
32
|
+
readonly START_FAILED: "machine_gateway.line.start_failed";
|
|
33
|
+
};
|
|
34
|
+
export type GatewayLogEvent = typeof GATEWAY_LOG[keyof typeof GATEWAY_LOG];
|
|
35
|
+
export interface GatewayLog {
|
|
36
|
+
info(event: GatewayLogEvent, context?: Record<string, unknown>): void;
|
|
37
|
+
warn(event: GatewayLogEvent, context?: Record<string, unknown>): void;
|
|
38
|
+
error(event: GatewayLogEvent, context?: Record<string, unknown>): void;
|
|
39
|
+
}
|
|
40
|
+
export declare const consoleLog: GatewayLog;
|
|
41
|
+
export declare const silentLog: GatewayLog;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.silentLog = exports.consoleLog = exports.GATEWAY_LOG = void 0;
|
|
4
|
+
exports.GATEWAY_LOG = {
|
|
5
|
+
STARTED: 'machine_gateway.started',
|
|
6
|
+
RECONCILED: 'machine_gateway.reconciled',
|
|
7
|
+
DISPATCHED: 'machine_gateway.dispatch.accepted',
|
|
8
|
+
DISPATCH_REFUSED: 'machine_gateway.dispatch.refused',
|
|
9
|
+
DISPATCH_READY: 'machine_gateway.dispatch.ready',
|
|
10
|
+
DISPATCH_FAILED: 'machine_gateway.dispatch.failed',
|
|
11
|
+
DISPATCH_RECONCILED: 'machine_gateway.dispatch.reconciled',
|
|
12
|
+
VERB_INVALID: 'machine_gateway.verb.invalid',
|
|
13
|
+
VERB_FAILED: 'machine_gateway.verb.failed',
|
|
14
|
+
ROW_NOT_VISIBLE: 'machine_gateway.verb.row_not_visible',
|
|
15
|
+
ROW_CLAIMED_ELSEWHERE: 'machine_gateway.verb.row_claimed_elsewhere',
|
|
16
|
+
UNMATCHED: 'machine_gateway.message.unmatched',
|
|
17
|
+
UNREADABLE: 'machine_gateway.message.unreadable',
|
|
18
|
+
CONTRACT_VIOLATION: 'machine_gateway.contract.violation',
|
|
19
|
+
VIOLATION_ROW: 'machine_gateway.contract.row_created',
|
|
20
|
+
ADDRESS_BOUND: 'machine_gateway.address.bound',
|
|
21
|
+
ADDRESS_CONTESTED: 'machine_gateway.address.contested',
|
|
22
|
+
ADDRESS_UNKNOWN: 'machine_gateway.address.unknown',
|
|
23
|
+
ADDRESS_REFUSED: 'machine_gateway.address.refused',
|
|
24
|
+
RECEIPT_FAILED: 'machine_gateway.receipt.failed',
|
|
25
|
+
ALARM_ROW: 'machine_gateway.alarm.row_created',
|
|
26
|
+
OFFLINE_ROW: 'machine_gateway.offline.row_created',
|
|
27
|
+
QUIET_ROW: 'machine_gateway.quiet.row_created',
|
|
28
|
+
COMMANDED: 'machine_gateway.command.accepted',
|
|
29
|
+
COMMAND_REFUSED: 'machine_gateway.command.refused',
|
|
30
|
+
COMMAND_UNANSWERED: 'machine_gateway.command.unanswered',
|
|
31
|
+
COMMAND_UNSUPPORTED: 'machine_gateway.command.unsupported',
|
|
32
|
+
LEADING: 'machine_gateway.line.leading',
|
|
33
|
+
STANDBY: 'machine_gateway.line.standby',
|
|
34
|
+
LEADERSHIP_LOST: 'machine_gateway.line.leadership_lost',
|
|
35
|
+
START_FAILED: 'machine_gateway.line.start_failed',
|
|
36
|
+
};
|
|
37
|
+
exports.consoleLog = {
|
|
38
|
+
info: (event, context) => console.log(JSON.stringify({ level: 'info', event, ...context })),
|
|
39
|
+
warn: (event, context) => console.warn(JSON.stringify({ level: 'warn', event, ...context })),
|
|
40
|
+
error: (event, context) => console.error(JSON.stringify({ level: 'error', event, ...context })),
|
|
41
|
+
};
|
|
42
|
+
exports.silentLog = { info: () => undefined, warn: () => undefined, error: () => undefined };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gateway's HTTP client to Long Tail. A non-2xx response throws
|
|
3
|
+
* LtHttpError with the status, body and request; the escalation verbs turn
|
|
4
|
+
* the outcomes that mean "already done" (404, 409) and "invalid" (422) into
|
|
5
|
+
* VerbResults and let every other failure throw, so the message that caused
|
|
6
|
+
* it is delivered again.
|
|
7
|
+
*/
|
|
8
|
+
import type { OpenRowQuery } from '../rows';
|
|
9
|
+
import { type Actor, type CreateRowInput } from './actions';
|
|
10
|
+
import { type LineEscalations, type RowItem, type VerbResult } from './escalations';
|
|
11
|
+
export declare class LtHttpError extends Error {
|
|
12
|
+
readonly status: number | undefined;
|
|
13
|
+
readonly body: unknown;
|
|
14
|
+
readonly context: {
|
|
15
|
+
method: string;
|
|
16
|
+
path: string;
|
|
17
|
+
elapsedMs: number;
|
|
18
|
+
};
|
|
19
|
+
readonly cause?: unknown;
|
|
20
|
+
constructor(message: string, status: number | undefined, body: unknown, context: {
|
|
21
|
+
method: string;
|
|
22
|
+
path: string;
|
|
23
|
+
elapsedMs: number;
|
|
24
|
+
}, options?: {
|
|
25
|
+
cause?: unknown;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export declare class LtHttp {
|
|
29
|
+
private readonly baseUrl;
|
|
30
|
+
private readonly timeoutMs;
|
|
31
|
+
constructor(baseUrl: string, timeoutMs?: number);
|
|
32
|
+
request<T = any>(method: string, path: string, token: string | null, body?: unknown): Promise<T>;
|
|
33
|
+
}
|
|
34
|
+
/** Escalation verbs over HTTP, each run with the bearer token of the actor it runs as. */
|
|
35
|
+
export declare class HttpLineEscalations implements LineEscalations {
|
|
36
|
+
private readonly http;
|
|
37
|
+
private readonly tokenFor;
|
|
38
|
+
private readonly claimMinutes;
|
|
39
|
+
constructor(http: LtHttp, tokenFor: (as: Actor) => string, claimMinutes?: number);
|
|
40
|
+
claim(as: Actor, id: string): Promise<VerbResult>;
|
|
41
|
+
accumulate(as: Actor, id: string, itemKey: string, payload: Record<string, any>, metadata?: Record<string, any>): Promise<VerbResult>;
|
|
42
|
+
resolve(as: Actor, id: string, payload: Record<string, any>): Promise<VerbResult>;
|
|
43
|
+
create(as: Actor, input: CreateRowInput): Promise<{
|
|
44
|
+
id: string;
|
|
45
|
+
}>;
|
|
46
|
+
itemKeys(id: string): Promise<string[]>;
|
|
47
|
+
items(id: string): Promise<RowItem[]>;
|
|
48
|
+
get(id: string): Promise<Record<string, any> | null>;
|
|
49
|
+
listOpen(filter: OpenRowQuery): Promise<Record<string, any>[]>;
|
|
50
|
+
private verb;
|
|
51
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The gateway's HTTP client to Long Tail. A non-2xx response throws
|
|
4
|
+
* LtHttpError with the status, body and request; the escalation verbs turn
|
|
5
|
+
* the outcomes that mean "already done" (404, 409) and "invalid" (422) into
|
|
6
|
+
* VerbResults and let every other failure throw, so the message that caused
|
|
7
|
+
* it is delivered again.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.HttpLineEscalations = exports.LtHttp = exports.LtHttpError = void 0;
|
|
11
|
+
const uuid_1 = require("../../../lib/uuid");
|
|
12
|
+
const actions_1 = require("./actions");
|
|
13
|
+
const escalations_1 = require("./escalations");
|
|
14
|
+
class LtHttpError extends Error {
|
|
15
|
+
constructor(message, status, body, context, options) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.status = status;
|
|
18
|
+
this.body = body;
|
|
19
|
+
this.context = context;
|
|
20
|
+
this.name = 'LtHttpError';
|
|
21
|
+
this.cause = options?.cause;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
exports.LtHttpError = LtHttpError;
|
|
25
|
+
class LtHttp {
|
|
26
|
+
constructor(baseUrl, timeoutMs = 10_000) {
|
|
27
|
+
this.baseUrl = baseUrl;
|
|
28
|
+
this.timeoutMs = timeoutMs;
|
|
29
|
+
}
|
|
30
|
+
async request(method, path, token, body) {
|
|
31
|
+
const started = performance.now();
|
|
32
|
+
const controller = new AbortController();
|
|
33
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
34
|
+
const context = () => ({ method, path, elapsedMs: Math.round(performance.now() - started) });
|
|
35
|
+
let res;
|
|
36
|
+
try {
|
|
37
|
+
res = await fetch(`${this.baseUrl}/api${path}`, {
|
|
38
|
+
method,
|
|
39
|
+
headers: { 'content-type': 'application/json', ...(token ? { authorization: `Bearer ${token}` } : {}) },
|
|
40
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
41
|
+
signal: controller.signal,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
const reason = controller.signal.aborted ? `timed out after ${this.timeoutMs} ms` : err.message;
|
|
46
|
+
throw new LtHttpError(`${method} ${path}: ${reason}`, undefined, undefined, context(), { cause: err });
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
clearTimeout(timer);
|
|
50
|
+
}
|
|
51
|
+
const text = await res.text();
|
|
52
|
+
const parsed = (res.headers.get('content-type') ?? '').includes('json') && text ? JSON.parse(text) : text;
|
|
53
|
+
if (!res.ok)
|
|
54
|
+
throw new LtHttpError(`${method} ${path} → ${res.status}`, res.status, parsed, context());
|
|
55
|
+
return parsed;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
exports.LtHttp = LtHttp;
|
|
59
|
+
/** Escalation verbs over HTTP, each run with the bearer token of the actor it runs as. */
|
|
60
|
+
class HttpLineEscalations {
|
|
61
|
+
constructor(http, tokenFor, claimMinutes = 24 * 60) {
|
|
62
|
+
this.http = http;
|
|
63
|
+
this.tokenFor = tokenFor;
|
|
64
|
+
this.claimMinutes = claimMinutes;
|
|
65
|
+
}
|
|
66
|
+
claim(as, id) {
|
|
67
|
+
return this.verb(as, 'POST', `/escalations/${id}/claim`, { durationMinutes: this.claimMinutes });
|
|
68
|
+
}
|
|
69
|
+
accumulate(as, id, itemKey, payload, metadata) {
|
|
70
|
+
return this.verb(as, 'POST', `/escalations/${id}/accumulate`, { itemKey, payload, ...(metadata ? { metadata } : {}) });
|
|
71
|
+
}
|
|
72
|
+
resolve(as, id, payload) {
|
|
73
|
+
return this.verb(as, 'POST', `/escalations/${id}/resolve`, { resolverPayload: payload });
|
|
74
|
+
}
|
|
75
|
+
async create(as, input) {
|
|
76
|
+
const row = await this.http.request('POST', '/escalations', this.tokenFor(as), input);
|
|
77
|
+
return { id: row.id };
|
|
78
|
+
}
|
|
79
|
+
async itemKeys(id) {
|
|
80
|
+
return (await this.items(id)).map((i) => i.itemKey);
|
|
81
|
+
}
|
|
82
|
+
async items(id) {
|
|
83
|
+
const view = await this.http.request('GET', `/escalations/${id}/items`, this.tokenFor(actions_1.CONTROLLER));
|
|
84
|
+
return view.items.map((i) => ({ itemKey: i.itemKey, payload: i.payload ?? {} }));
|
|
85
|
+
}
|
|
86
|
+
async get(id) {
|
|
87
|
+
if (!(0, uuid_1.isUuid)(id))
|
|
88
|
+
return null;
|
|
89
|
+
try {
|
|
90
|
+
return await this.http.request('GET', `/escalations/${encodeURIComponent(id)}`, this.tokenFor(actions_1.CONTROLLER));
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
if (err instanceof LtHttpError && (err.status === 403 || err.status === 404))
|
|
94
|
+
return null;
|
|
95
|
+
throw err;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async listOpen(filter) {
|
|
99
|
+
return (0, escalations_1.readAllPages)(async (offset) => {
|
|
100
|
+
const query = new URLSearchParams({
|
|
101
|
+
...filter, status: 'pending', limit: String(escalations_1.OPEN_PAGE), offset: String(offset), sort_by: 'created_at', order: 'asc',
|
|
102
|
+
});
|
|
103
|
+
const page = await this.http.request('GET', `/escalations?${query}`, this.tokenFor(actions_1.CONTROLLER));
|
|
104
|
+
return page.escalations;
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
async verb(as, method, path, body) {
|
|
108
|
+
try {
|
|
109
|
+
await this.http.request(method, path, this.tokenFor(as), body);
|
|
110
|
+
return { outcome: escalations_1.VERB_OUTCOME.DONE };
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
const outcome = err instanceof LtHttpError && err.status !== undefined ? (0, escalations_1.verbOutcomeOf)(err.status) : null;
|
|
114
|
+
if (!outcome)
|
|
115
|
+
throw err;
|
|
116
|
+
return { outcome, detail: JSON.stringify(err.body), status: err.status };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
exports.HttpLineEscalations = HttpLineEscalations;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pacing: a machine with a queue depth is sent no more jobs than that at once.
|
|
3
|
+
* A handoff beyond it waits in the machine's role queue, unclaimed, the way
|
|
4
|
+
* work waits for a busy person, and the oldest waiting row goes out when the
|
|
5
|
+
* machine has room. A machine's load is every job it has been sent or still
|
|
6
|
+
* holds: rows dispatched into it, rows whose units wait on it to move on, and
|
|
7
|
+
* units that arrived and have not left yet.
|
|
8
|
+
*/
|
|
9
|
+
import { type LineDef } from '../../../lib/machines/line';
|
|
10
|
+
import { type Action } from './actions';
|
|
11
|
+
import { type GatewayState, type HandoffRow } from './state';
|
|
12
|
+
/** Dispatch arguments for a handoff row. */
|
|
13
|
+
export declare function dispatchArgs(row: HandoffRow, expiresAt: number): Record<string, any>;
|
|
14
|
+
/**
|
|
15
|
+
* The validity round a dispatch belongs to. Rounds of one validity period count
|
|
16
|
+
* from when the dispatch became due, and a round begins only when the one
|
|
17
|
+
* before it has expired, so a machine never holds two valid commands for one
|
|
18
|
+
* row. Within a round a dispatch sent again, by a retry, a restarted machine or
|
|
19
|
+
* a restarted gateway, is the same command with the same arguments; a new round
|
|
20
|
+
* is a new command, never an old one with changed arguments.
|
|
21
|
+
*/
|
|
22
|
+
export declare function dispatchWindow(dueAt: number, now: number, ttlMs: number): {
|
|
23
|
+
round: number;
|
|
24
|
+
expiresAt: number;
|
|
25
|
+
};
|
|
26
|
+
/** The later of a row's due time and an arrival's time; an unreadable time leaves it as it was. */
|
|
27
|
+
export declare function latestArrival(dueAt: number | undefined, at: unknown): number | undefined;
|
|
28
|
+
export declare function dispatchAction(row: HandoffRow, machineId: string, dispatchTtlMs: number): Action;
|
|
29
|
+
/** A machine on a version the line does not allow is refused before any dispatch. */
|
|
30
|
+
export declare function softwareRefusal(state: GatewayState, line: LineDef, machineId: string): string | null;
|
|
31
|
+
/** A machine that requires an artifact is never sent a handoff without one: the job's, or one for every unit. */
|
|
32
|
+
export declare function artifactRefusal(line: LineDef, row: HandoffRow): string | null;
|
|
33
|
+
/** The jobs `machineId` has been sent or still holds. */
|
|
34
|
+
export declare function machineLoad(state: GatewayState, machineId: string): Set<string>;
|
|
35
|
+
export declare function hasRoom(state: GatewayState, line: LineDef, row: HandoffRow): boolean;
|
|
36
|
+
/** Two open handoffs may not name the same job at one machine: each row, subsets of a route's units included, needs its own. */
|
|
37
|
+
export declare function jobRefusal(state: GatewayState, row: HandoffRow): string | null;
|
|
38
|
+
/** The resolution refusing a handoff for the machine's software, a missing artifact or a job id in use; empty when it may go. */
|
|
39
|
+
export declare function refuseUnfit(state: GatewayState, line: LineDef, row: HandoffRow): Action[];
|
|
40
|
+
/** Send a handoff to its machine now, or refuse it for the machine's software or a missing artifact. */
|
|
41
|
+
export declare function admit(state: GatewayState, line: LineDef, row: HandoffRow, dispatchTtlMs: number): Action[];
|
|
42
|
+
/** Dispatch waiting rows, oldest first, while their machines have room; a full machine holds back its later rows too. */
|
|
43
|
+
export declare function planPacing(state: GatewayState, line: LineDef, dispatchTtlMs: number): Action[];
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Pacing: a machine with a queue depth is sent no more jobs than that at once.
|
|
4
|
+
* A handoff beyond it waits in the machine's role queue, unclaimed, the way
|
|
5
|
+
* work waits for a busy person, and the oldest waiting row goes out when the
|
|
6
|
+
* machine has room. A machine's load is every job it has been sent or still
|
|
7
|
+
* holds: rows dispatched into it, rows whose units wait on it to move on, and
|
|
8
|
+
* units that arrived and have not left yet.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.dispatchArgs = dispatchArgs;
|
|
12
|
+
exports.dispatchWindow = dispatchWindow;
|
|
13
|
+
exports.latestArrival = latestArrival;
|
|
14
|
+
exports.dispatchAction = dispatchAction;
|
|
15
|
+
exports.softwareRefusal = softwareRefusal;
|
|
16
|
+
exports.artifactRefusal = artifactRefusal;
|
|
17
|
+
exports.machineLoad = machineLoad;
|
|
18
|
+
exports.hasRoom = hasRoom;
|
|
19
|
+
exports.jobRefusal = jobRefusal;
|
|
20
|
+
exports.refuseUnfit = refuseUnfit;
|
|
21
|
+
exports.admit = admit;
|
|
22
|
+
exports.planPacing = planPacing;
|
|
23
|
+
const contracts_1 = require("../contracts");
|
|
24
|
+
const protocol_1 = require("../../../lib/machines/protocol");
|
|
25
|
+
const line_1 = require("../../../lib/machines/line");
|
|
26
|
+
const actions_1 = require("./actions");
|
|
27
|
+
const jobs_1 = require("./jobs");
|
|
28
|
+
const state_1 = require("./state");
|
|
29
|
+
const registration_1 = require("./registration");
|
|
30
|
+
/** Dispatch arguments for a handoff row. */
|
|
31
|
+
function dispatchArgs(row, expiresAt) {
|
|
32
|
+
return {
|
|
33
|
+
jobId: row.jobId,
|
|
34
|
+
orderRef: row.orderRef,
|
|
35
|
+
recipe: { id: row.recipeId, version: row.recipeVersion },
|
|
36
|
+
units: row.arrivals ? row.units.map((u) => ({ ...u, arrival: row.arrivals?.[u.unitId] ?? null })) : row.units,
|
|
37
|
+
process: row.process,
|
|
38
|
+
from: row.fromMachine,
|
|
39
|
+
expiresAt: (0, protocol_1.isoTs)(expiresAt),
|
|
40
|
+
...(row.artifactUrl ? { artifact: { url: row.artifactUrl, sha256: row.artifactSha256 } } : {}),
|
|
41
|
+
...(row.parameters ? { parameters: row.parameters } : {}),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The validity round a dispatch belongs to. Rounds of one validity period count
|
|
46
|
+
* from when the dispatch became due, and a round begins only when the one
|
|
47
|
+
* before it has expired, so a machine never holds two valid commands for one
|
|
48
|
+
* row. Within a round a dispatch sent again, by a retry, a restarted machine or
|
|
49
|
+
* a restarted gateway, is the same command with the same arguments; a new round
|
|
50
|
+
* is a new command, never an old one with changed arguments.
|
|
51
|
+
*/
|
|
52
|
+
function dispatchWindow(dueAt, now, ttlMs) {
|
|
53
|
+
const round = Math.max(1, Math.floor((now - dueAt) / ttlMs) + 1);
|
|
54
|
+
return { round, expiresAt: dueAt + round * ttlMs };
|
|
55
|
+
}
|
|
56
|
+
/** The later of a row's due time and an arrival's time; an unreadable time leaves it as it was. */
|
|
57
|
+
function latestArrival(dueAt, at) {
|
|
58
|
+
const t = typeof at === 'string' ? Date.parse(at) : NaN;
|
|
59
|
+
if (!Number.isFinite(t))
|
|
60
|
+
return dueAt;
|
|
61
|
+
return dueAt === undefined ? t : Math.max(dueAt, t);
|
|
62
|
+
}
|
|
63
|
+
function dispatchAction(row, machineId, dispatchTtlMs) {
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
row.createdAt ??= now;
|
|
66
|
+
const { round, expiresAt } = dispatchWindow(row.dueAt ?? row.createdAt, now, dispatchTtlMs);
|
|
67
|
+
return {
|
|
68
|
+
kind: 'dispatch', rowId: row.id, machineId, cmdId: (0, contracts_1.dispatchCommandId)(row.id, round), args: dispatchArgs(row, expiresAt),
|
|
69
|
+
...(row.dispatch === state_1.DISPATCH_STATE.ARRIVING ? { receivedOnAccept: true } : {}),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** A machine on a version the line does not allow is refused before any dispatch. */
|
|
73
|
+
function softwareRefusal(state, line, machineId) {
|
|
74
|
+
const allowed = (0, line_1.machineById)(line, machineId).allowedSoftware;
|
|
75
|
+
const software = state.links.get(machineId)?.software;
|
|
76
|
+
if (!allowed || !software || allowed.includes(software))
|
|
77
|
+
return null;
|
|
78
|
+
return `${machineId} runs ${software}; allowed: ${allowed.join(', ')}`;
|
|
79
|
+
}
|
|
80
|
+
/** A machine that requires an artifact is never sent a handoff without one: the job's, or one for every unit. */
|
|
81
|
+
function artifactRefusal(line, row) {
|
|
82
|
+
if (!(0, line_1.machineById)(line, row.toMachine).requireArtifact || (row.artifactUrl && row.artifactSha256))
|
|
83
|
+
return null;
|
|
84
|
+
if (row.units.every((u) => u.artifact || u.artifacts?.length))
|
|
85
|
+
return null;
|
|
86
|
+
return `${row.toMachine} requires an artifact (url and sha256), for the job or for every unit; the row has none`;
|
|
87
|
+
}
|
|
88
|
+
const SENT = [state_1.DISPATCH_STATE.PENDING, state_1.DISPATCH_STATE.ACCEPTED];
|
|
89
|
+
const jobIdsOf = (state, jobId, row) => (0, jobs_1.dispatchJobs)(jobId, row.units, state.unitsPerDispatch).map((j) => j.jobId);
|
|
90
|
+
/** The jobs `machineId` has been sent or still holds. */
|
|
91
|
+
function machineLoad(state, machineId) {
|
|
92
|
+
const jobs = new Set(state.heldAt(machineId));
|
|
93
|
+
for (const r of state.open()) {
|
|
94
|
+
if (r.toMachine === machineId && SENT.includes(r.dispatch))
|
|
95
|
+
for (const id of jobIdsOf(state, r.jobId, r))
|
|
96
|
+
jobs.add(id);
|
|
97
|
+
if (r.fromMachine === machineId && r.senderJobId)
|
|
98
|
+
for (const id of jobIdsOf(state, r.senderJobId, r))
|
|
99
|
+
jobs.add(id);
|
|
100
|
+
}
|
|
101
|
+
return jobs;
|
|
102
|
+
}
|
|
103
|
+
/** Whether a row's jobs fit at its receiver; an idle machine always takes the next row, however large. */
|
|
104
|
+
function fits(line, row, load, state) {
|
|
105
|
+
const depth = (0, line_1.machineById)(line, row.toMachine).queueDepth;
|
|
106
|
+
return !depth || load.size === 0 || load.size + jobIdsOf(state, row.jobId, row).length <= depth;
|
|
107
|
+
}
|
|
108
|
+
function hasRoom(state, line, row) {
|
|
109
|
+
return fits(line, row, machineLoad(state, row.toMachine), state);
|
|
110
|
+
}
|
|
111
|
+
/** Two open handoffs may not name the same job at one machine: each row, subsets of a route's units included, needs its own. */
|
|
112
|
+
function jobRefusal(state, row) {
|
|
113
|
+
const other = state.open().find((r) => r.id !== row.id && r.toMachine === row.toMachine && r.jobId === row.jobId && r.dispatch !== state_1.DISPATCH_STATE.REFUSED);
|
|
114
|
+
return other ? `job ${row.jobId} at ${row.toMachine} already belongs to handoff ${other.id}` : null;
|
|
115
|
+
}
|
|
116
|
+
/** The resolution refusing a handoff for the machine's software, a missing artifact or a job id in use; empty when it may go. */
|
|
117
|
+
function refuseUnfit(state, line, row) {
|
|
118
|
+
const software = softwareRefusal(state, line, row.toMachine);
|
|
119
|
+
const other = software ?? artifactRefusal(line, row) ?? jobRefusal(state, row);
|
|
120
|
+
if (!other)
|
|
121
|
+
return [];
|
|
122
|
+
row.dispatch = state_1.DISPATCH_STATE.REFUSED;
|
|
123
|
+
const errorCode = software ? contracts_1.HANDOFF_ERROR.SOFTWARE_UNSUPPORTED : contracts_1.HANDOFF_ERROR.DISPATCH_REFUSED;
|
|
124
|
+
return [{ kind: 'resolve', as: actions_1.CONTROLLER, rowId: row.id, payload: { received: false, errorCode, detail: other } }];
|
|
125
|
+
}
|
|
126
|
+
/** Send a handoff to its machine now, or refuse it for the machine's software or a missing artifact. */
|
|
127
|
+
function admit(state, line, row, dispatchTtlMs) {
|
|
128
|
+
const refused = refuseUnfit(state, line, row);
|
|
129
|
+
if (refused.length)
|
|
130
|
+
return refused;
|
|
131
|
+
row.dispatch = state_1.DISPATCH_STATE.PENDING;
|
|
132
|
+
return [dispatchAction(row, row.toMachine, dispatchTtlMs)];
|
|
133
|
+
}
|
|
134
|
+
/** Dispatch waiting rows, oldest first, while their machines have room; a full machine holds back its later rows too. */
|
|
135
|
+
function planPacing(state, line, dispatchTtlMs) {
|
|
136
|
+
const actions = [];
|
|
137
|
+
const loads = new Map();
|
|
138
|
+
const full = new Set();
|
|
139
|
+
for (const row of state.open()) {
|
|
140
|
+
const m = row.toMachine;
|
|
141
|
+
// A station that has not registered its address yet keeps its rows waiting.
|
|
142
|
+
if (row.dispatch !== state_1.DISPATCH_STATE.WAITING || !m || full.has(m) || !(0, registration_1.reachable)(state, line, m))
|
|
143
|
+
continue;
|
|
144
|
+
if (!loads.has(m))
|
|
145
|
+
loads.set(m, machineLoad(state, m));
|
|
146
|
+
const load = loads.get(m);
|
|
147
|
+
if (!fits(line, row, load, state)) {
|
|
148
|
+
full.add(m);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const admitted = admit(state, line, row, dispatchTtlMs);
|
|
152
|
+
actions.push(...admitted);
|
|
153
|
+
if (admitted.some((a) => a.kind === 'dispatch'))
|
|
154
|
+
for (const id of jobIdsOf(state, row.jobId, row))
|
|
155
|
+
load.add(id);
|
|
156
|
+
}
|
|
157
|
+
return actions;
|
|
158
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escalation events to gateway actions. A new handoff row for a machine is the
|
|
3
|
+
* cue to dispatch, exactly as a new row is the cue for a person to look at the
|
|
4
|
+
* dashboard. A cancelled or expired row withdraws its dispatch; a row commands
|
|
5
|
+
* only the machine whose role it is parked on, so the sender's job is the
|
|
6
|
+
* workflow's to stop (a machine-command row on the sender's role). An alarm
|
|
7
|
+
* row a person resolves acknowledges the alarm at the machine.
|
|
8
|
+
* A machine-command row is sent to its machine as the command it names.
|
|
9
|
+
*/
|
|
10
|
+
import { type LineDef } from '../../../lib/machines/line';
|
|
11
|
+
import type { EscalationEvent } from '../../../lib/machines/transport';
|
|
12
|
+
import { type Action } from './actions';
|
|
13
|
+
import { type GatewayState } from './state';
|
|
14
|
+
import { type HandoffRowAdapter } from '../rows';
|
|
15
|
+
/** Whether an event names a row this line acts on; only then is the stored row read. */
|
|
16
|
+
export declare function concernsLine(state: GatewayState, line: LineDef, event: EscalationEvent, rows?: HandoffRowAdapter): boolean;
|
|
17
|
+
/** Whether a stored row's status agrees with the event's verb, so a stale or invented event does nothing. */
|
|
18
|
+
export declare function statusFits(verb: string, status: unknown): boolean;
|
|
19
|
+
export declare function planEscalationEvent(state: GatewayState, line: LineDef, event: EscalationEvent, dispatchTtlMs: number, rows?: HandoffRowAdapter): Action[];
|