@hotmeshio/long-tail 0.26.0 → 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/build/index.d.ts +4 -0
- package/build/index.js +30 -8
- package/build/lib/events/nats.d.ts +11 -0
- package/build/lib/events/nats.js +33 -2
- package/build/lib/machines/index.d.ts +1 -0
- package/build/lib/machines/index.js +1 -0
- package/build/lib/machines/protocol/codec.d.ts +16 -0
- package/build/lib/machines/protocol/codec.js +1 -0
- package/build/lib/machines/protocol/contract.js +10 -1
- 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/site-conf.d.ts +7 -4
- package/build/lib/machines/site/site-conf.js +13 -6
- package/build/lib/machines/transport/delivery.d.ts +3 -4
- package/build/lib/machines/transport/delivery.js +3 -8
- package/build/lib/machines/transport/index.d.ts +2 -0
- package/build/lib/machines/transport/index.js +2 -0
- package/build/lib/machines/transport/nats-bus.d.ts +7 -13
- package/build/lib/machines/transport/nats-bus.js +23 -43
- package/build/lib/machines/transport/nats-feed.js +2 -2
- 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/stream-bus.d.ts +40 -0
- package/build/lib/machines/transport/stream-bus.js +57 -0
- package/build/lib/machines/transport/streams.d.ts +10 -14
- package/build/lib/machines/transport/streams.js +18 -61
- 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/services/machines/config.d.ts +6 -0
- package/build/services/machines/config.js +16 -6
- package/build/services/machines/contracts.d.ts +19 -2
- package/build/services/machines/contracts.js +12 -3
- package/build/services/machines/gateway/actions.d.ts +1 -1
- package/build/services/machines/gateway/dispatcher.d.ts +16 -3
- package/build/services/machines/gateway/dispatcher.js +50 -10
- package/build/services/machines/gateway/executor.d.ts +7 -2
- package/build/services/machines/gateway/executor.js +35 -9
- package/build/services/machines/gateway/gateway.d.ts +6 -0
- package/build/services/machines/gateway/gateway.js +33 -10
- package/build/services/machines/gateway/lifecycle.d.ts +21 -0
- package/build/services/machines/gateway/lifecycle.js +89 -0
- package/build/services/machines/gateway/pacing.d.ts +14 -0
- package/build/services/machines/gateway/pacing.js +26 -2
- package/build/services/machines/gateway/plan-escalation.js +13 -4
- package/build/services/machines/gateway/plan-machine.d.ts +5 -4
- package/build/services/machines/gateway/plan-machine.js +55 -14
- package/build/services/machines/gateway/reconcile.d.ts +2 -0
- package/build/services/machines/gateway/reconcile.js +13 -0
- package/build/services/machines/gateway/state.d.ts +21 -1
- package/build/services/machines/gateway/state.js +16 -3
- package/build/services/machines/leadership.d.ts +17 -4
- package/build/services/machines/leadership.js +43 -18
- package/build/services/machines/rows.js +17 -2
- package/build/services/machines/select.d.ts +9 -2
- package/build/services/machines/select.js +20 -7
- package/build/services/machines/surface.js +8 -2
- package/build/services/machines/testing/fake-long-tail.d.ts +1 -0
- package/build/services/machines/testing/fake-long-tail.js +6 -3
- package/build/start/adapters.js +11 -2
- package/build/start/index.js +7 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/build/types/index.d.ts +1 -0
- package/build/types/machines.d.ts +2 -1
- package/build/types/messaging.d.ts +72 -0
- package/build/types/messaging.js +11 -0
- package/build/types/startup.d.ts +26 -1
- package/docs/events.md +50 -4
- package/docs/machines.md +38 -9
- package/package.json +3 -1
- package/build/lib/machines/transport/nats-durable.d.ts +0 -11
- package/build/lib/machines/transport/nats-durable.js +0 -28
|
@@ -15,6 +15,7 @@ const escalations_1 = require("./escalations");
|
|
|
15
15
|
const log_1 = require("./log");
|
|
16
16
|
const dispatcher_1 = require("./dispatcher");
|
|
17
17
|
const plan_shared_1 = require("./plan-shared");
|
|
18
|
+
const state_1 = require("./state");
|
|
18
19
|
const NOT_FOUND = 404;
|
|
19
20
|
const CONFLICT = 409;
|
|
20
21
|
class Executor {
|
|
@@ -47,10 +48,14 @@ class Executor {
|
|
|
47
48
|
for (const t of this.timers)
|
|
48
49
|
clearTimeout(t);
|
|
49
50
|
this.timers.clear();
|
|
51
|
+
this.dispatcher.stop();
|
|
50
52
|
this.d.replies.cancelAll();
|
|
51
53
|
}
|
|
52
54
|
track(work) {
|
|
53
|
-
const tracked = work.catch((err) =>
|
|
55
|
+
const tracked = work.catch((err) => {
|
|
56
|
+
if (!(err instanceof dispatcher_1.GatewayStopped))
|
|
57
|
+
this.d.log.error(log_1.GATEWAY_LOG.VERB_FAILED, { error: String(err?.message ?? err) });
|
|
58
|
+
})
|
|
54
59
|
.finally(() => this.inflight.delete(tracked));
|
|
55
60
|
this.inflight.add(tracked);
|
|
56
61
|
}
|
|
@@ -90,19 +95,29 @@ class Executor {
|
|
|
90
95
|
this.d.log.warn(log_1.GATEWAY_LOG.ADDRESS_REFUSED, { machine: a.machineId, address: a.address, reason: a.reason });
|
|
91
96
|
}
|
|
92
97
|
}
|
|
93
|
-
/**
|
|
98
|
+
/**
|
|
99
|
+
* A dispatch's second reply: ready, or a failure to prepare that ends the
|
|
100
|
+
* handoff. When the handoff into the machine has already ended, as one
|
|
101
|
+
* dispatched on arrival does at acceptance, the failure ends the handoff
|
|
102
|
+
* carrying the units out of it.
|
|
103
|
+
*/
|
|
94
104
|
async lateReply(a) {
|
|
95
|
-
const
|
|
96
|
-
|
|
105
|
+
const { state, log } = this.d;
|
|
106
|
+
const into = state.rowForDispatch(a.cmdId);
|
|
107
|
+
const m = into?.toMachine;
|
|
108
|
+
if (!into || !m)
|
|
97
109
|
return;
|
|
98
|
-
const context = { row:
|
|
110
|
+
const context = { row: into.id, machine: m, cmdId: a.cmdId, reason: a.reason };
|
|
99
111
|
if (a.status === protocol_1.REPLY_STATUS.COMPLETED)
|
|
100
|
-
return
|
|
112
|
+
return log.info(log_1.GATEWAY_LOG.DISPATCH_READY, context);
|
|
101
113
|
if (a.status !== protocol_1.REPLY_STATUS.FAILED)
|
|
102
114
|
return;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
115
|
+
const row = into.ended ? onwardRow(state, into, m) : into;
|
|
116
|
+
log.warn(log_1.GATEWAY_LOG.DISPATCH_FAILED, { ...context, ...(into.ended ? { onward: row?.id ?? null } : {}) });
|
|
117
|
+
if (!row)
|
|
118
|
+
return;
|
|
119
|
+
await this.apply((0, plan_shared_1.failHandoff)(state, row, m, {
|
|
120
|
+
received: false, errorCode: contracts_1.HANDOFF_ERROR.DISPATCH_FAILED, detail: `${m} could not prepare the job: ${a.reason}`,
|
|
106
121
|
}));
|
|
107
122
|
}
|
|
108
123
|
/** Add the item; when the row then holds every key the action waits for, resolve it. */
|
|
@@ -193,3 +208,14 @@ class Executor {
|
|
|
193
208
|
}
|
|
194
209
|
}
|
|
195
210
|
exports.Executor = Executor;
|
|
211
|
+
/** The open handoff out of `m` for units a closed handoff brought into it. */
|
|
212
|
+
function onwardRow(state, into, m) {
|
|
213
|
+
if (into.ended !== state_1.ROW_END.RESOLVED || into.dispatch !== state_1.DISPATCH_STATE.ACCEPTED)
|
|
214
|
+
return undefined;
|
|
215
|
+
for (const u of into.units) {
|
|
216
|
+
const row = state.openFor(u.unitId, (r) => r.fromMachine === m);
|
|
217
|
+
if (row)
|
|
218
|
+
return row;
|
|
219
|
+
}
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
@@ -23,6 +23,10 @@ export interface GatewaySettings {
|
|
|
23
23
|
quietMs: number;
|
|
24
24
|
/** How long confirmed message ids gather before a receipt goes out (codecs with receipts). */
|
|
25
25
|
receiptDelayMs?: number;
|
|
26
|
+
/** Note each job's start, progress steps and pauses on the row that holds it; off unless set. */
|
|
27
|
+
lifecycle?: boolean;
|
|
28
|
+
/** With lifecycle notes, progress is noted each time it crosses this many percent; 0 notes none. */
|
|
29
|
+
progressStep?: number;
|
|
26
30
|
}
|
|
27
31
|
export interface GatewayDeps {
|
|
28
32
|
bus: MachineBus;
|
|
@@ -52,6 +56,8 @@ export declare class MachineGateway {
|
|
|
52
56
|
constructor(deps: GatewayDeps);
|
|
53
57
|
start(): Promise<void>;
|
|
54
58
|
stop(): Promise<void>;
|
|
59
|
+
/** Machines that report only changes are asked for their full state once the gateway listens. */
|
|
60
|
+
private askEveryMachine;
|
|
55
61
|
/** Resolves when no dispatch is in flight (tests). */
|
|
56
62
|
idle(): Promise<void>;
|
|
57
63
|
/**
|
|
@@ -32,6 +32,9 @@ class MachineGateway {
|
|
|
32
32
|
this.codec = deps.codec ?? (0, protocol_1.genericCodec)({ root: deps.settings.topicRoot, line: deps.line });
|
|
33
33
|
this.state.unitsPerDispatch = this.codec.unitsPerDispatch;
|
|
34
34
|
this.state.dispatchOnArrival = this.codec.dispatchOn === protocol_1.DISPATCH_ON.ARRIVAL;
|
|
35
|
+
this.state.lifecycle = deps.settings.lifecycle === true;
|
|
36
|
+
this.state.retainedAlarms = this.codec.retainedAlarms === true;
|
|
37
|
+
this.state.progressStep = deps.settings.progressStep ?? this.state.progressStep;
|
|
35
38
|
this.addresses = deps.addresses ?? new address_book_1.MemoryAddressBook();
|
|
36
39
|
this.executor = new executor_1.Executor({
|
|
37
40
|
addresses: this.addresses,
|
|
@@ -45,9 +48,12 @@ class MachineGateway {
|
|
|
45
48
|
async start() {
|
|
46
49
|
await this.deps.bus.connect();
|
|
47
50
|
await this.deps.feed.connect();
|
|
51
|
+
this.state.takenAt = Date.now();
|
|
48
52
|
await this.reconcile();
|
|
49
53
|
await this.deps.feed.consume((event) => this.onEscalation(event));
|
|
50
54
|
await this.deps.bus.consume((msg) => this.onMachine(msg));
|
|
55
|
+
if (this.codec.snapshotOnStart)
|
|
56
|
+
await this.askEveryMachine();
|
|
51
57
|
const { quietMs } = this.deps.settings;
|
|
52
58
|
if (quietMs > 0) {
|
|
53
59
|
const machines = this.deps.line.machines.map((m) => m.machineId);
|
|
@@ -59,10 +65,18 @@ class MachineGateway {
|
|
|
59
65
|
if (this.quietTimer)
|
|
60
66
|
clearInterval(this.quietTimer);
|
|
61
67
|
this.executor.stop();
|
|
68
|
+
await this.executor.idle();
|
|
62
69
|
await this.receipts.flush();
|
|
63
70
|
await this.deps.feed.close();
|
|
64
71
|
await this.deps.bus.close();
|
|
65
72
|
}
|
|
73
|
+
/** Machines that report only changes are asked for their full state once the gateway listens. */
|
|
74
|
+
async askEveryMachine() {
|
|
75
|
+
const at = Date.now();
|
|
76
|
+
await this.executor.apply(this.deps.line.machines.map((m) => ({
|
|
77
|
+
kind: 'command', machineId: m.machineId, op: protocol_1.OP.SNAPSHOT, cmdId: `${m.machineId}:takeover:${at}`, args: {},
|
|
78
|
+
})));
|
|
79
|
+
}
|
|
66
80
|
/** Resolves when no dispatch is in flight (tests). */
|
|
67
81
|
idle() {
|
|
68
82
|
return this.executor.idle();
|
|
@@ -80,6 +94,7 @@ class MachineGateway {
|
|
|
80
94
|
const addresses = await (0, reconcile_1.restoreAddresses)(this.state, this.addresses, this.deps.line);
|
|
81
95
|
const handoffs = await this.reconcileHandoffs();
|
|
82
96
|
await this.executor.apply(await (0, reconcile_1.arrivalsOnRows)(this.state, this.deps.lt, this.deps.settings.dispatchTtlMs));
|
|
97
|
+
await (0, reconcile_1.notesOnRows)(this.state, this.deps.lt);
|
|
83
98
|
const commands = await this.reconcileCommands();
|
|
84
99
|
const restored = await (0, reconcile_1.restoreLineRows)(this.state, this.deps.lt, this.deps.line);
|
|
85
100
|
await this.resync();
|
|
@@ -121,18 +136,26 @@ class MachineGateway {
|
|
|
121
136
|
async resync() {
|
|
122
137
|
const { line } = this.deps;
|
|
123
138
|
const active = new Set();
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
139
|
+
this.state.readingRetained = true;
|
|
140
|
+
try {
|
|
141
|
+
for (const msg of await this.deps.bus.retained((0, protocol_1.lineFilter)(this.codec.root, line.siteId, line.lineId))) {
|
|
142
|
+
for (const raw of this.decode(msg.topic, msg.payload, true)) {
|
|
143
|
+
if (!raw.parsed)
|
|
144
|
+
continue;
|
|
145
|
+
const seen = await (0, admission_1.admitSender)(this.admission, msg.topic, raw);
|
|
146
|
+
if (!('parsed' in seen) || !seen.parsed)
|
|
147
|
+
continue;
|
|
148
|
+
if (seen.parsed.kind === protocol_1.MESSAGE_KIND.ALARM && seen.body?.body?.active)
|
|
149
|
+
active.add(this.state.alarmKey(seen.parsed.ident.machineId, seen.parsed.key));
|
|
150
|
+
await this.applyMachine(msg.topic, seen.parsed, seen.body);
|
|
151
|
+
}
|
|
134
152
|
}
|
|
135
153
|
}
|
|
154
|
+
finally {
|
|
155
|
+
this.state.readingRetained = false;
|
|
156
|
+
}
|
|
157
|
+
if (!this.state.retainedAlarms)
|
|
158
|
+
return;
|
|
136
159
|
for (const gone of (0, reconcile_1.alarmsClearedMeanwhile)(this.state, active)) {
|
|
137
160
|
this.state.alarms.delete(gone.key);
|
|
138
161
|
await this.executor.apply([{ kind: 'resolve', as: actions_1.CONTROLLER, rowId: gone.rowId, payload: { decision: contracts_1.DECISION.CLEARED, note: `cleared at ${gone.machineId} while the gateway was away` } }]);
|
|
@@ -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
|
+
}
|
|
@@ -11,6 +11,20 @@ import { type Action } from './actions';
|
|
|
11
11
|
import { type GatewayState, type HandoffRow } from './state';
|
|
12
12
|
/** Dispatch arguments for a handoff row. */
|
|
13
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;
|
|
14
28
|
export declare function dispatchAction(row: HandoffRow, machineId: string, dispatchTtlMs: number): Action;
|
|
15
29
|
/** A machine on a version the line does not allow is refused before any dispatch. */
|
|
16
30
|
export declare function softwareRefusal(state: GatewayState, line: LineDef, machineId: string): string | null;
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
11
|
exports.dispatchArgs = dispatchArgs;
|
|
12
|
+
exports.dispatchWindow = dispatchWindow;
|
|
13
|
+
exports.latestArrival = latestArrival;
|
|
12
14
|
exports.dispatchAction = dispatchAction;
|
|
13
15
|
exports.softwareRefusal = softwareRefusal;
|
|
14
16
|
exports.artifactRefusal = artifactRefusal;
|
|
@@ -39,9 +41,31 @@ function dispatchArgs(row, expiresAt) {
|
|
|
39
41
|
...(row.parameters ? { parameters: row.parameters } : {}),
|
|
40
42
|
};
|
|
41
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
|
+
}
|
|
42
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);
|
|
43
67
|
return {
|
|
44
|
-
kind: 'dispatch', rowId: row.id, machineId, cmdId: (0, contracts_1.dispatchCommandId)(row.id), args: dispatchArgs(row,
|
|
68
|
+
kind: 'dispatch', rowId: row.id, machineId, cmdId: (0, contracts_1.dispatchCommandId)(row.id, round), args: dispatchArgs(row, expiresAt),
|
|
45
69
|
...(row.dispatch === state_1.DISPATCH_STATE.ARRIVING ? { receivedOnAccept: true } : {}),
|
|
46
70
|
};
|
|
47
71
|
}
|
|
@@ -57,7 +81,7 @@ function softwareRefusal(state, line, machineId) {
|
|
|
57
81
|
function artifactRefusal(line, row) {
|
|
58
82
|
if (!(0, line_1.machineById)(line, row.toMachine).requireArtifact || (row.artifactUrl && row.artifactSha256))
|
|
59
83
|
return null;
|
|
60
|
-
if (row.units.every((u) => u.artifact))
|
|
84
|
+
if (row.units.every((u) => u.artifact || u.artifacts?.length))
|
|
61
85
|
return null;
|
|
62
86
|
return `${row.toMachine} requires an artifact (url and sha256), for the job or for every unit; the row has none`;
|
|
63
87
|
}
|
|
@@ -29,9 +29,17 @@ const VERB = { CREATED: 'created', CLAIMED: 'claimed', RESOLVED: 'resolved', CAN
|
|
|
29
29
|
const STATUS_FOR_VERB = {
|
|
30
30
|
[VERB.CREATED]: 'pending', [VERB.CLAIMED]: 'pending', [VERB.RESOLVED]: 'resolved', [VERB.CANCELLED]: 'cancelled', [VERB.EXPIRED]: 'expired',
|
|
31
31
|
};
|
|
32
|
+
/** The metadata key an accumulator row parked with an item limit carries. */
|
|
33
|
+
const ACCUMULATE_MAX = 'accumulate_max';
|
|
32
34
|
const LINE_ROW_TYPES = [contracts_1.ROW_TYPE.ALARM, contracts_1.ROW_TYPE.MACHINE_OFFLINE, contracts_1.ROW_TYPE.MACHINE_QUIET, contracts_1.ROW_TYPE.MACHINE_COMMAND, contracts_1.ROW_TYPE.MACHINE_REGISTRATION];
|
|
33
|
-
function handoffRow(id, role, facets,
|
|
34
|
-
|
|
35
|
+
function handoffRow(id, role, facets, data) {
|
|
36
|
+
const metadata = data?.metadata;
|
|
37
|
+
const capped = metadata?.[ACCUMULATE_MAX] != null;
|
|
38
|
+
const createdAt = data?.created_at ? new Date(data.created_at).getTime() : NaN;
|
|
39
|
+
return {
|
|
40
|
+
...facets, id, role, dispatch: state_1.DISPATCH_STATE.NONE, trace: (0, trace_1.readTrace)(metadata),
|
|
41
|
+
...(capped ? { capped } : {}), ...(Number.isFinite(createdAt) ? { createdAt } : {}),
|
|
42
|
+
};
|
|
35
43
|
}
|
|
36
44
|
/** Rows for another line belong to that line's gateway. */
|
|
37
45
|
function onThisLine(line, at) {
|
|
@@ -108,7 +116,8 @@ function planCommandRow(state, line, event) {
|
|
|
108
116
|
if (refusal) {
|
|
109
117
|
return [{ kind: 'resolve', as: actions_1.CONTROLLER, rowId: id, payload: { accepted: false, machineId: c.machineId, op: c.op, status: contracts_1.COMMAND_STATUS.UNSUPPORTED, reason: refusal } }];
|
|
110
118
|
}
|
|
111
|
-
|
|
119
|
+
const createdAt = event.data?.created_at ? new Date(event.data.created_at).getTime() : NaN;
|
|
120
|
+
return [{ kind: 'control', rowId: id, machineId: c.machineId, op: c.op, args: c.args, ...(Number.isFinite(createdAt) ? { createdAt } : {}) }];
|
|
112
121
|
}
|
|
113
122
|
function onLine(line, machineId) {
|
|
114
123
|
return !!machineId && line.machines.some((m) => m.machineId === machineId);
|
|
@@ -122,7 +131,7 @@ function planHandoff(state, line, event, facets, dispatchTtlMs) {
|
|
|
122
131
|
if (event.verb === VERB.CREATED || event.verb === VERB.CLAIMED) {
|
|
123
132
|
if (state.isClosed(id) || !facets)
|
|
124
133
|
return [];
|
|
125
|
-
state.index(handoffRow(id, event.role, facets, event.data
|
|
134
|
+
state.index(handoffRow(id, event.role, facets, event.data));
|
|
126
135
|
const row = state.get(id);
|
|
127
136
|
// The units now wait on the sender as this row's sender job, when the sender holds them under it.
|
|
128
137
|
if (row.fromMachine)
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
* Machine messages to gateway actions: the escalation verbs a person would
|
|
3
3
|
* perform, performed for the machine. Each unit's arrival and the sender's
|
|
4
4
|
* finished record accumulate on the handoff row, and the last expected item
|
|
5
|
-
* resolves it. A failure on either side resolves it with an error.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* resolves it. A failure on either side resolves it with an error. A job's
|
|
6
|
+
* start, its progress steps, and the alarms that pause it are noted on the row
|
|
7
|
+
* that holds it (lifecycle.ts). Alarms that stop work and machines that go
|
|
8
|
+
* dark or quiet become rows for people. A restarted machine gets back the
|
|
9
|
+
* dispatches it lost. Everything else stays in the stream.
|
|
9
10
|
*
|
|
10
11
|
* A message whose row does not exist yet is nak'd and arrives again; one whose
|
|
11
12
|
* row already closed is acked, so a replay never resolves anything twice.
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
* Machine messages to gateway actions: the escalation verbs a person would
|
|
4
4
|
* perform, performed for the machine. Each unit's arrival and the sender's
|
|
5
5
|
* finished record accumulate on the handoff row, and the last expected item
|
|
6
|
-
* resolves it. A failure on either side resolves it with an error.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* resolves it. A failure on either side resolves it with an error. A job's
|
|
7
|
+
* start, its progress steps, and the alarms that pause it are noted on the row
|
|
8
|
+
* that holds it (lifecycle.ts). Alarms that stop work and machines that go
|
|
9
|
+
* dark or quiet become rows for people. A restarted machine gets back the
|
|
10
|
+
* dispatches it lost. Everything else stays in the stream.
|
|
10
11
|
*
|
|
11
12
|
* A message whose row does not exist yet is nak'd and arrives again; one whose
|
|
12
13
|
* row already closed is acked, so a replay never resolves anything twice.
|
|
@@ -21,6 +22,7 @@ const actions_1 = require("./actions");
|
|
|
21
22
|
const pacing_1 = require("./pacing");
|
|
22
23
|
const plan_shared_1 = require("./plan-shared");
|
|
23
24
|
const jobs_1 = require("./jobs");
|
|
25
|
+
const lifecycle_1 = require("./lifecycle");
|
|
24
26
|
const trace_1 = require("./trace");
|
|
25
27
|
const state_1 = require("./state");
|
|
26
28
|
const ack = (actions = []) => ({ actions, disposition: transport_1.DISPOSITION.ACK });
|
|
@@ -68,12 +70,29 @@ function ownsJob(jobId, reported) {
|
|
|
68
70
|
return (0, jobs_1.jobOwns)(jobId, reported);
|
|
69
71
|
}
|
|
70
72
|
/** Keep the receiver's arrival event for the unit, to go out with its dispatch. */
|
|
71
|
-
function withArrival(row, unitId, b) {
|
|
73
|
+
function withArrival(row, unitId, b, at) {
|
|
72
74
|
row.arrivals = { ...row.arrivals, [unitId]: b };
|
|
75
|
+
row.dueAt = (0, pacing_1.latestArrival)(row.dueAt, at);
|
|
73
76
|
return row;
|
|
74
77
|
}
|
|
78
|
+
/** The unit an open handoff into `m` carries on `carrierId`, for a scan that read only the carrier. */
|
|
79
|
+
function unitOnCarrier(state, m, carrierId) {
|
|
80
|
+
if (typeof carrierId !== 'string')
|
|
81
|
+
return undefined;
|
|
82
|
+
for (const r of state.open()) {
|
|
83
|
+
if (r.toMachine !== m)
|
|
84
|
+
continue;
|
|
85
|
+
const unit = r.units.find((u) => u.carrierId === carrierId);
|
|
86
|
+
if (unit)
|
|
87
|
+
return unit.unitId;
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
75
91
|
function planJob(state, m, b, at, ttl) {
|
|
76
|
-
const unitId = b.unitId;
|
|
92
|
+
const unitId = typeof b.unitId === 'string' ? b.unitId : unitOnCarrier(state, m, b.carrierId);
|
|
93
|
+
// A carrier no open handoff here carries yet: its row may still be on the way.
|
|
94
|
+
if (unitId === undefined)
|
|
95
|
+
return nak;
|
|
77
96
|
// An event from an earlier attempt's job never touches a later attempt's row.
|
|
78
97
|
const toHere = (r) => r.toMachine === m && ownsJob(r.jobId, b.jobId);
|
|
79
98
|
const fromHere = (r) => r.fromMachine === m && ownsJob(r.senderJobId, b.jobId);
|
|
@@ -95,13 +114,14 @@ function planJob(state, m, b, at, ttl) {
|
|
|
95
114
|
if (row.fromMachine)
|
|
96
115
|
state.letGo(row.fromMachine, unitId);
|
|
97
116
|
const units = row.units.map((u) => u.unitId);
|
|
117
|
+
const arrivedAt = at ?? new Date().toISOString();
|
|
98
118
|
const complete = row.dispatch === state_1.DISPATCH_STATE.ARRIVING
|
|
99
|
-
? { keys: units.map(contracts_1.itemKeys.arrived), then: [(0, pacing_1.dispatchAction)(withArrival(row, unitId, b), m, ttl)] }
|
|
119
|
+
? { keys: units.map(contracts_1.itemKeys.arrived), then: [(0, pacing_1.dispatchAction)(withArrival(row, unitId, b, arrivedAt), m, ttl)] }
|
|
100
120
|
: { keys: units.map(contracts_1.itemKeys.arrived), payload: { received: true, receiver: m, units } };
|
|
101
121
|
return ack([...early, {
|
|
102
122
|
kind: 'accumulate', as: m, rowId: row.id, itemKey: contracts_1.itemKeys.arrived(unitId),
|
|
103
123
|
payload: {
|
|
104
|
-
unitId, carrierId: b.carrierId ?? null, from: b.peer ?? null, at:
|
|
124
|
+
unitId, carrierId: b.carrierId ?? null, from: b.peer ?? null, at: arrivedAt,
|
|
105
125
|
...(row.dispatch === state_1.DISPATCH_STATE.ARRIVING ? { arrival: b } : {}),
|
|
106
126
|
},
|
|
107
127
|
complete,
|
|
@@ -140,6 +160,8 @@ function planJob(state, m, b, at, ttl) {
|
|
|
140
160
|
return ack();
|
|
141
161
|
return ack((0, plan_shared_1.failHandoff)(state, row, m, { received: false, unitId, errorCode: contracts_1.HANDOFF_ERROR.WITHDRAWN, detail: `${m} gave up ${b.jobId}` }));
|
|
142
162
|
}
|
|
163
|
+
if (b.event === protocol_1.JOB_EVENT.STARTED || b.event === protocol_1.JOB_EVENT.PROGRESS)
|
|
164
|
+
return ack((0, lifecycle_1.noteJobEvent)(state, m, b, at));
|
|
143
165
|
return ack();
|
|
144
166
|
}
|
|
145
167
|
function releasedAction(row, m, b) {
|
|
@@ -160,9 +182,10 @@ function planAlarm(state, parsed, alarmId, env) {
|
|
|
160
182
|
if (!env || b.active === false) {
|
|
161
183
|
const track = state.alarms.get(key);
|
|
162
184
|
state.alarms.delete(key);
|
|
185
|
+
const resumed = track ? (0, lifecycle_1.noteResume)(state, track, b) : [];
|
|
163
186
|
if (!track?.rowId || !track.open)
|
|
164
|
-
return ack();
|
|
165
|
-
return ack([{
|
|
187
|
+
return ack(resumed);
|
|
188
|
+
return ack([...resumed, {
|
|
166
189
|
kind: 'resolve', as: actions_1.CONTROLLER, rowId: track.rowId,
|
|
167
190
|
payload: { decision: contracts_1.DECISION.CLEARED, clearedBy: b.clearedBy ?? null, note: `cleared at ${m}${b.clearedBy ? ` by ${b.clearedBy}` : ''}` },
|
|
168
191
|
}]);
|
|
@@ -175,8 +198,9 @@ function planAlarm(state, parsed, alarmId, env) {
|
|
|
175
198
|
const superseded = known?.rowId && known.open
|
|
176
199
|
? [{ kind: 'resolve', as: actions_1.CONTROLLER, rowId: known.rowId, payload: { decision: contracts_1.DECISION.CLEARED, note: `${m} raised ${alarmId} again; this one ended` } }]
|
|
177
200
|
: [];
|
|
178
|
-
|
|
179
|
-
|
|
201
|
+
const track = { machineId: m, alarmId, raisedAt: b.raisedAt, open: true };
|
|
202
|
+
state.alarms.set(key, track);
|
|
203
|
+
return ack([...superseded, ...(0, lifecycle_1.notePause)(state, track, b), {
|
|
180
204
|
kind: 'createAlarmRow', machineId: m, alarmId,
|
|
181
205
|
row: {
|
|
182
206
|
type: contracts_1.ROW_TYPE.ALARM, subtype: b.level, role: contracts_1.LINE_ROLE.OPERATOR, priority: b.level === protocol_1.LEVEL.EMERGENCY ? 1 : 2,
|
|
@@ -230,13 +254,30 @@ function planSnapshot(state, m, b, ttl) {
|
|
|
230
254
|
const jobIds = Array.isArray(b.jobs) ? b.jobs.map((j) => j?.jobId).filter((id) => typeof id === 'string') : [];
|
|
231
255
|
if (Array.isArray(b.jobs))
|
|
232
256
|
state.keepHeld(m, new Set(jobIds));
|
|
257
|
+
const fresh = !state.retainedAlarms && !state.readingRetained && Array.isArray(b.alarms);
|
|
258
|
+
const cleared = fresh ? alarmsNoLongerListed(state, m, b.alarms) : [];
|
|
233
259
|
const answer = { kind: 'settleSnapshot', machineId: m, jobIds };
|
|
234
260
|
const previous = link.bootId;
|
|
235
261
|
link.bootId = b.bootId;
|
|
236
262
|
if (!previous || previous === b.bootId)
|
|
237
|
-
return ack([answer]);
|
|
263
|
+
return ack([...cleared, answer]);
|
|
238
264
|
const held = new Set(jobIds);
|
|
239
265
|
const missing = (r) => (0, jobs_1.dispatchJobs)(r.jobId, r.units, state.unitsPerDispatch).some((j) => !held.has(j.jobId));
|
|
240
266
|
const lost = state.open().filter((r) => r.toMachine === m && r.dispatch === state_1.DISPATCH_STATE.ACCEPTED && missing(r));
|
|
241
|
-
return ack([answer, ...lost.map((r) => (0, pacing_1.dispatchAction)(r, m, ttl))]);
|
|
267
|
+
return ack([...cleared, answer, ...lost.map((r) => (0, pacing_1.dispatchAction)(r, m, ttl))]);
|
|
268
|
+
}
|
|
269
|
+
/** Alarm rows for `m` whose alarm its snapshot no longer lists: they cleared at the machine. */
|
|
270
|
+
function alarmsNoLongerListed(state, m, listed) {
|
|
271
|
+
const active = new Set(listed.filter((id) => typeof id === 'string'));
|
|
272
|
+
const actions = [];
|
|
273
|
+
for (const [key, track] of [...state.alarms.entries()]) {
|
|
274
|
+
if (track.machineId !== m || active.has(track.alarmId))
|
|
275
|
+
continue;
|
|
276
|
+
state.alarms.delete(key);
|
|
277
|
+
actions.push(...(0, lifecycle_1.noteResume)(state, track, {}));
|
|
278
|
+
if (track.open && track.rowId) {
|
|
279
|
+
actions.push({ kind: 'resolve', as: actions_1.CONTROLLER, rowId: track.rowId, payload: { decision: contracts_1.DECISION.CLEARED, note: `cleared at ${m}; its snapshot no longer lists it` } });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return actions;
|
|
242
283
|
}
|
|
@@ -31,3 +31,5 @@ export declare function alarmsClearedMeanwhile(state: GatewayState, active: Set<
|
|
|
31
31
|
* arrivals it holds.
|
|
32
32
|
*/
|
|
33
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>;
|
|
@@ -10,6 +10,7 @@ exports.restoreLineRows = restoreLineRows;
|
|
|
10
10
|
exports.restoreAddresses = restoreAddresses;
|
|
11
11
|
exports.alarmsClearedMeanwhile = alarmsClearedMeanwhile;
|
|
12
12
|
exports.arrivalsOnRows = arrivalsOnRows;
|
|
13
|
+
exports.notesOnRows = notesOnRows;
|
|
13
14
|
const contracts_1 = require("../contracts");
|
|
14
15
|
const line_1 = require("../../../lib/machines/line");
|
|
15
16
|
const jobs_1 = require("./jobs");
|
|
@@ -65,6 +66,7 @@ async function arrivalsOnRows(state, lt, dispatchTtlMs) {
|
|
|
65
66
|
if (!to || (row.dispatch !== state_1.DISPATCH_STATE.ARRIVING && row.dispatch !== state_1.DISPATCH_STATE.ACCEPTED))
|
|
66
67
|
continue;
|
|
67
68
|
const items = await lt.items(row.id);
|
|
69
|
+
row.noted = items.map((i) => i.itemKey);
|
|
68
70
|
const units = row.units.map((u) => u.unitId);
|
|
69
71
|
const { arrived, missing } = (0, contracts_1.handoffProgress)(units, items);
|
|
70
72
|
for (const job of (0, jobs_1.dispatchJobs)(row.jobId, row.units, state.unitsPerDispatch)) {
|
|
@@ -80,7 +82,18 @@ async function arrivalsOnRows(state, lt, dispatchTtlMs) {
|
|
|
80
82
|
}
|
|
81
83
|
const byUnit = new Map(items.filter((i) => (0, contracts_1.arrivedUnit)(i.itemKey)).map((i) => [i.payload.unitId, i.payload]));
|
|
82
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);
|
|
83
87
|
actions.push((0, pacing_1.dispatchAction)(row, to, dispatchTtlMs));
|
|
84
88
|
}
|
|
85
89
|
return actions;
|
|
86
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
|
+
}
|