@hotmeshio/long-tail 0.28.0 → 0.28.2
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/api/files.d.ts +5 -0
- package/build/api/files.js +19 -2
- package/build/api/knowledge.d.ts +3 -1
- package/build/lib/db/schemas/042_knowledge_edition_labels.sql +9 -0
- package/build/lib/events/publish.js +3 -2
- package/build/lib/machines/line.d.ts +3 -0
- package/build/lib/machines/line.js +7 -1
- package/build/lib/machines/protocol/codec.d.ts +6 -0
- package/build/lib/machines/protocol/codec.js +1 -1
- package/build/lib/machines/protocol/contract.d.ts +2 -0
- package/build/lib/machines/protocol/contract.js +12 -7
- package/build/lib/machines/site/mosquitto-conf.d.ts +7 -3
- package/build/lib/machines/site/mosquitto-conf.js +3 -8
- package/build/lib/machines/site/site-conf.d.ts +4 -1
- package/build/lib/machines/site/site-conf.js +5 -1
- package/build/lib/machines/transport/nats-bus.d.ts +4 -0
- package/build/lib/machines/transport/stream-bus.d.ts +7 -1
- package/build/lib/machines/transport/stream-bus.js +20 -6
- package/build/lib/machines/transport/streams.d.ts +11 -2
- package/build/lib/machines/transport/streams.js +33 -5
- package/build/lib/messaging/memory.js +10 -3
- package/build/lib/messaging/nats/permissions.js +1 -0
- package/build/lib/messaging/nats/streams.d.ts +21 -4
- package/build/lib/messaging/nats/streams.js +71 -10
- package/build/lib/storage/local.d.ts +2 -0
- package/build/lib/storage/local.js +3 -0
- package/build/lib/storage/s3.d.ts +3 -2
- package/build/lib/storage/s3.js +6 -1
- package/build/lib/storage/types.d.ts +7 -1
- package/build/routes/file-browser.js +3 -1
- package/build/routes/knowledge.js +6 -5
- package/build/routes/machines.js +33 -0
- package/build/services/knowledge/index.d.ts +4 -2
- package/build/services/knowledge/index.js +8 -2
- package/build/services/knowledge/lookup-cache.d.ts +8 -0
- package/build/services/knowledge/lookup-cache.js +30 -3
- package/build/services/knowledge/lookup-refs.js +1 -1
- package/build/services/knowledge/sql.d.ts +4 -2
- package/build/services/knowledge/sql.js +8 -3
- package/build/services/machines/config.d.ts +2 -0
- package/build/services/machines/config.js +1 -0
- package/build/services/machines/contracts.d.ts +18 -0
- package/build/services/machines/contracts.js +15 -1
- package/build/services/machines/forms.d.ts +26 -1
- package/build/services/machines/forms.js +17 -1
- package/build/services/machines/gateway/actions.d.ts +13 -1
- package/build/services/machines/gateway/admission.d.ts +15 -1
- package/build/services/machines/gateway/admission.js +47 -0
- package/build/services/machines/gateway/api-escalations.js +4 -0
- package/build/services/machines/gateway/dispatcher.d.ts +9 -4
- package/build/services/machines/gateway/dispatcher.js +42 -14
- package/build/services/machines/gateway/executor.d.ts +2 -0
- package/build/services/machines/gateway/executor.js +18 -2
- package/build/services/machines/gateway/gateway.d.ts +5 -0
- package/build/services/machines/gateway/gateway.js +11 -3
- package/build/services/machines/gateway/log.d.ts +3 -0
- package/build/services/machines/gateway/log.js +3 -0
- package/build/services/machines/gateway/lt-http.js +11 -2
- package/build/services/machines/gateway/pacing.js +1 -1
- package/build/services/machines/gateway/plan-machine.js +26 -11
- package/build/services/machines/gateway/plan-shared.d.ts +5 -3
- package/build/services/machines/gateway/plan-shared.js +6 -4
- package/build/services/machines/gateway/reply-waiter.d.ts +2 -0
- package/build/services/machines/gateway/state.d.ts +24 -1
- package/build/services/machines/gateway/state.js +67 -6
- package/build/services/machines/gateway/wire-window.d.ts +21 -0
- package/build/services/machines/gateway/wire-window.js +61 -0
- package/build/services/machines/gateway/wire.js +1 -0
- package/build/services/machines/leadership.d.ts +2 -0
- package/build/services/machines/leadership.js +7 -1
- package/build/services/machines/roles.d.ts +5 -4
- package/build/services/machines/roles.js +13 -4
- package/build/services/machines/select.d.ts +3 -2
- package/build/services/machines/select.js +4 -3
- package/build/services/machines/surface.d.ts +10 -1
- package/build/services/machines/surface.js +27 -5
- package/build/system/activities/knowledge.d.ts +4 -0
- package/build/system/activities/knowledge.js +15 -0
- package/build/system/activities/sql.d.ts +2 -0
- package/build/system/activities/sql.js +6 -1
- package/build/system/mcp-servers/knowledge.js +4 -2
- package/build/tsconfig.tsbuildinfo +1 -1
- package/build/types/escalation.d.ts +9 -6
- package/build/types/escalation.js +8 -2
- package/build/types/machines-view.d.ts +16 -0
- package/build/types/machines.d.ts +1 -0
- package/build/types/messaging.d.ts +7 -1
- package/build/types/startup.d.ts +2 -0
- package/docs/dashboard.md +1 -1
- package/docs/hitl/lookups.md +6 -4
- package/docs/machines.md +26 -17
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* protocol that resolves it differs.
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.cancelCommandId = exports.controlCommandId = exports.dispatchCommandId = exports.COMMAND_STATUS = exports.signalKeys = exports.DECISION = exports.LIFECYCLE_FACET = exports.TRACE_FACET = exports.HANDOFF_ERROR = exports.itemKeys = exports.LINE_ROLE = exports.ROW_TYPE = void 0;
|
|
9
|
+
exports.cancelCommandId = exports.controlCommandId = exports.dispatchCommandId = exports.COMMAND_STATUS = exports.signalKeys = exports.HOLD_DECISIONS = exports.DECISION = exports.LIFECYCLE_FACET = exports.TRACE_FACET = exports.HANDOFF_ERROR = exports.itemKeys = exports.LINE_ROLE = exports.ROW_TYPE = void 0;
|
|
10
10
|
exports.arrivedUnit = arrivedUnit;
|
|
11
11
|
exports.handoffProgress = handoffProgress;
|
|
12
12
|
exports.handoffDecisions = handoffDecisions;
|
|
@@ -24,12 +24,16 @@ exports.ROW_TYPE = {
|
|
|
24
24
|
CONTRACT_VIOLATION: 'contract-violation',
|
|
25
25
|
/** An address claims a station the line already knows by another address, or the reverse: a person approves or rejects. */
|
|
26
26
|
MACHINE_REGISTRATION: 'machine-registration',
|
|
27
|
+
/** Units a machine finished on hold wait for a quality decision: release, rework or scrap. */
|
|
28
|
+
QUALITY_HOLD: 'quality-hold',
|
|
27
29
|
};
|
|
28
30
|
exports.LINE_ROLE = {
|
|
29
31
|
/** People who clear alarms, bring machines back and decide failed handoffs. */
|
|
30
32
|
OPERATOR: line_1.DEFAULT_OPERATOR_ROLE,
|
|
31
33
|
/** People who take finished units off the line: the final handoff. */
|
|
32
34
|
EXIT: line_1.DEFAULT_EXIT_ROLE,
|
|
35
|
+
/** People who decide units a machine finished on hold. */
|
|
36
|
+
QUALITY: line_1.DEFAULT_QUALITY_ROLE,
|
|
33
37
|
/** The gateway's own identity. */
|
|
34
38
|
CONTROLLER: 'line-controller',
|
|
35
39
|
};
|
|
@@ -75,6 +79,8 @@ exports.HANDOFF_ERROR = {
|
|
|
75
79
|
SOFTWARE_UNSUPPORTED: 'software-unsupported',
|
|
76
80
|
WITHDRAWN: 'withdrawn',
|
|
77
81
|
CANCELLED: 'cancelled',
|
|
82
|
+
/** An arrival no open handoff into the machine expects, shown to the operator role once per machine and carrier. */
|
|
83
|
+
UNMATCHED_ARRIVAL: 'unmatched-arrival',
|
|
78
84
|
...protocol_1.TRANSFER_FAILURE,
|
|
79
85
|
};
|
|
80
86
|
/**
|
|
@@ -95,6 +101,10 @@ exports.DECISION = {
|
|
|
95
101
|
RESTORED: 'restored',
|
|
96
102
|
APPROVE: 'approve',
|
|
97
103
|
REJECT: 'reject',
|
|
104
|
+
/** A quality decision on held units: they continue from where they are. */
|
|
105
|
+
RELEASE: 'release',
|
|
106
|
+
/** A quality decision on held units: they run again from a station on the route. */
|
|
107
|
+
REWORK: 'rework',
|
|
98
108
|
};
|
|
99
109
|
/**
|
|
100
110
|
* Failures a retry cannot repair. Units a sender finished as not ok leave the
|
|
@@ -107,11 +117,15 @@ function handoffDecisions(errorCode) {
|
|
|
107
117
|
const noRetry = !!errorCode && NO_RETRY.includes(errorCode);
|
|
108
118
|
return noRetry ? [exports.DECISION.MANUAL, exports.DECISION.SCRAP] : [exports.DECISION.RETRY, exports.DECISION.MANUAL, exports.DECISION.SCRAP];
|
|
109
119
|
}
|
|
120
|
+
/** The decisions a quality role may take on units a machine finished on hold. */
|
|
121
|
+
exports.HOLD_DECISIONS = [exports.DECISION.RELEASE, exports.DECISION.REWORK, exports.DECISION.SCRAP];
|
|
110
122
|
/** Signal keys for the unit workflow; every attempt mints a fresh key. */
|
|
111
123
|
exports.signalKeys = {
|
|
112
124
|
handoff: (jobKey, step, attempt) => `handoff-${jobKey}-s${step}-a${attempt}`,
|
|
113
125
|
handoffError: (jobKey, step, attempt, round = 1) => `handoff-error-${jobKey}-s${step}-a${attempt}${round > 1 ? `-r${round}` : ''}`,
|
|
114
126
|
jobStop: (jobKey, step, attempt) => `job-stop-${jobKey}-s${step}-a${attempt}`,
|
|
127
|
+
hold: (jobKey, step, attempt, round = 1) => `hold-${jobKey}-s${step}-a${attempt}${round > 1 ? `-r${round}` : ''}`,
|
|
128
|
+
disposition: (jobKey, step, attempt) => `disposition-${jobKey}-s${step}-a${attempt}`,
|
|
115
129
|
};
|
|
116
130
|
exports.COMMAND_STATUS = { ...protocol_1.REPLY_STATUS, NO_REPLY: 'no-reply', UNSUPPORTED: 'unsupported' };
|
|
117
131
|
/** A row's dispatch command in its validity round; a later round is a new command (gateway/pacing.ts dispatchWindow). */
|
|
@@ -36,7 +36,7 @@ export declare const OPERATOR_FORM_SCHEMA: {
|
|
|
36
36
|
properties: {
|
|
37
37
|
decision: {
|
|
38
38
|
type: string;
|
|
39
|
-
enum: ("reject" | "scrap" | "retry" | "manual" | "acknowledge" | "cleared" | "restored" | "approve")[];
|
|
39
|
+
enum: ("release" | "reject" | "scrap" | "rework" | "retry" | "manual" | "acknowledge" | "cleared" | "restored" | "approve")[];
|
|
40
40
|
default: "acknowledge";
|
|
41
41
|
description: string;
|
|
42
42
|
};
|
|
@@ -53,3 +53,28 @@ export declare const OPERATOR_FORM_SCHEMA: {
|
|
|
53
53
|
};
|
|
54
54
|
};
|
|
55
55
|
};
|
|
56
|
+
export declare const QUALITY_FORM_SCHEMA: {
|
|
57
|
+
title: string;
|
|
58
|
+
description: string;
|
|
59
|
+
'x-lt-order': string[];
|
|
60
|
+
required: string[];
|
|
61
|
+
properties: {
|
|
62
|
+
decision: {
|
|
63
|
+
type: string;
|
|
64
|
+
enum: ("release" | "scrap" | "rework")[];
|
|
65
|
+
default: "release";
|
|
66
|
+
description: string;
|
|
67
|
+
};
|
|
68
|
+
station: {
|
|
69
|
+
type: string;
|
|
70
|
+
default: string;
|
|
71
|
+
description: string;
|
|
72
|
+
};
|
|
73
|
+
note: {
|
|
74
|
+
type: string;
|
|
75
|
+
format: string;
|
|
76
|
+
default: string;
|
|
77
|
+
description: string;
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
};
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* handoffs and close alarm and machine-state rows.
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.OPERATOR_FORM_SCHEMA = exports.HANDOFF_FORM_SCHEMA = void 0;
|
|
9
|
+
exports.QUALITY_FORM_SCHEMA = exports.OPERATOR_FORM_SCHEMA = exports.HANDOFF_FORM_SCHEMA = void 0;
|
|
10
10
|
const contracts_1 = require("./contracts");
|
|
11
11
|
exports.HANDOFF_FORM_SCHEMA = {
|
|
12
12
|
title: 'Receive units',
|
|
@@ -35,3 +35,19 @@ exports.OPERATOR_FORM_SCHEMA = {
|
|
|
35
35
|
clearedBy: { type: 'string', default: '', description: 'Who cleared it at the machine, when the machine reports it' },
|
|
36
36
|
},
|
|
37
37
|
};
|
|
38
|
+
exports.QUALITY_FORM_SCHEMA = {
|
|
39
|
+
title: 'Quality decision',
|
|
40
|
+
description: 'Units a machine finished on hold: release them to continue, send them back to a station for rework, or scrap them.',
|
|
41
|
+
'x-lt-order': ['decision', 'station', 'note'],
|
|
42
|
+
required: ['decision'],
|
|
43
|
+
properties: {
|
|
44
|
+
decision: {
|
|
45
|
+
type: 'string',
|
|
46
|
+
enum: [contracts_1.DECISION.RELEASE, contracts_1.DECISION.REWORK, contracts_1.DECISION.SCRAP],
|
|
47
|
+
default: contracts_1.DECISION.RELEASE,
|
|
48
|
+
description: 'Release, rework or scrap',
|
|
49
|
+
},
|
|
50
|
+
station: { type: 'string', default: '', description: 'For rework: the station the units run again from; the holding station when empty' },
|
|
51
|
+
note: { type: 'string', format: 'textarea', default: '', description: 'What was found' },
|
|
52
|
+
},
|
|
53
|
+
};
|
|
@@ -18,14 +18,19 @@ export interface CreateRowInput {
|
|
|
18
18
|
escalation_payload?: string;
|
|
19
19
|
}
|
|
20
20
|
export type Action =
|
|
21
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* `receivedOnAccept`: the units are already at the machine, so its acceptance completes the handoff.
|
|
23
|
+
* `ended`: the row already resolved; the job is sent again for units the machine still holds, and no verb touches the row.
|
|
24
|
+
*/
|
|
22
25
|
{
|
|
23
26
|
kind: 'dispatch';
|
|
24
27
|
rowId: string;
|
|
25
28
|
machineId: string;
|
|
26
29
|
cmdId: string;
|
|
27
30
|
args: Record<string, any>;
|
|
31
|
+
issuedAt?: number;
|
|
28
32
|
receivedOnAccept?: boolean;
|
|
33
|
+
ended?: boolean;
|
|
29
34
|
} | {
|
|
30
35
|
kind: 'command';
|
|
31
36
|
machineId: string;
|
|
@@ -79,6 +84,7 @@ export type Action =
|
|
|
79
84
|
status: string;
|
|
80
85
|
reason: string;
|
|
81
86
|
code?: string;
|
|
87
|
+
preparing?: boolean;
|
|
82
88
|
}
|
|
83
89
|
/** A machine reported the jobs it holds: answers a status query in flight. */
|
|
84
90
|
| {
|
|
@@ -99,6 +105,12 @@ export type Action =
|
|
|
99
105
|
machineId: string;
|
|
100
106
|
address: string;
|
|
101
107
|
reason: string;
|
|
108
|
+
}
|
|
109
|
+
/** Jobs a restarted machine lost that this gateway cannot send again: nothing of them is remembered. */
|
|
110
|
+
| {
|
|
111
|
+
kind: 'unrecoverable';
|
|
112
|
+
machineId: string;
|
|
113
|
+
jobs: string[];
|
|
102
114
|
};
|
|
103
115
|
/** Once the row holds every one of `keys`: resolve it with `payload`, or run `then` instead. */
|
|
104
116
|
export interface Completion {
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* and under a strict codec a message that breaks the contract is held for a
|
|
5
5
|
* person instead of acted on.
|
|
6
6
|
*/
|
|
7
|
+
import type { OpenRowQuery } from '../rows';
|
|
7
8
|
import { type LineDef } from '../../../lib/machines/line';
|
|
8
|
-
import type
|
|
9
|
+
import { type Inspection } from '../../../lib/machines/protocol';
|
|
9
10
|
import { type Disposition, type InboundMessage } from '../../../lib/machines/transport';
|
|
10
11
|
import type { WireAction } from '../../../types/machines-view';
|
|
11
12
|
import type { Action } from './actions';
|
|
@@ -36,6 +37,8 @@ export interface AdmissionContext {
|
|
|
36
37
|
log: GatewayLog;
|
|
37
38
|
/** Send waiting handoffs that may now go out. */
|
|
38
39
|
pace(): Promise<void>;
|
|
40
|
+
/** The controller's pending rows of a type, for the rows shown to people once. */
|
|
41
|
+
openRows(query: OpenRowQuery): Promise<Record<string, any>[]>;
|
|
39
42
|
}
|
|
40
43
|
/**
|
|
41
44
|
* The station a message is from (registration.ts). A newly claimed station is
|
|
@@ -45,3 +48,14 @@ export interface AdmissionContext {
|
|
|
45
48
|
export declare function admitSender(ctx: AdmissionContext, topic: string, raw: Inspection): Promise<Admitted | Applied>;
|
|
46
49
|
/** Under a strict codec a broken message is not acted on: a person sees it on a row, once. */
|
|
47
50
|
export declare function holdViolation(ctx: AdmissionContext, msg: InboundMessage, seen: Inspection): Promise<Applied>;
|
|
51
|
+
/** A job arrival: a scan, or a transfer that failed on the carrier read. */
|
|
52
|
+
export declare function isArrival(seen: Inspection): boolean;
|
|
53
|
+
/** An arrival that read no id at all: no row can ever claim it, so it goes to a person on its first delivery. */
|
|
54
|
+
export declare function isUnreadableArrival(seen: Inspection): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* An arrival no open handoff into the machine expects, after every delivery:
|
|
57
|
+
* a person sees it on one handoff-error row per machine and carrier, with the
|
|
58
|
+
* raw arrival, so the unit can be re-scanned or routed. Resolving the row lets
|
|
59
|
+
* the next such arrival raise one again.
|
|
60
|
+
*/
|
|
61
|
+
export declare function holdArrival(ctx: AdmissionContext, msg: InboundMessage, seen: Inspection): Promise<Applied>;
|
|
@@ -8,8 +8,12 @@
|
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
exports.admitSender = admitSender;
|
|
10
10
|
exports.holdViolation = holdViolation;
|
|
11
|
+
exports.isArrival = isArrival;
|
|
12
|
+
exports.isUnreadableArrival = isUnreadableArrival;
|
|
13
|
+
exports.holdArrival = holdArrival;
|
|
11
14
|
const contracts_1 = require("../contracts");
|
|
12
15
|
const line_1 = require("../../../lib/machines/line");
|
|
16
|
+
const protocol_1 = require("../../../lib/machines/protocol");
|
|
13
17
|
const transport_1 = require("../../../lib/machines/transport");
|
|
14
18
|
const dispatcher_1 = require("./dispatcher");
|
|
15
19
|
const log_1 = require("./log");
|
|
@@ -79,3 +83,46 @@ async function holdViolation(ctx, msg, seen) {
|
|
|
79
83
|
return { disposition: transport_1.DISPOSITION.NAK, failed: true, actions: [] };
|
|
80
84
|
}
|
|
81
85
|
}
|
|
86
|
+
/** A job arrival: a scan, or a transfer that failed on the carrier read. */
|
|
87
|
+
function isArrival(seen) {
|
|
88
|
+
const event = seen.body?.body?.event;
|
|
89
|
+
return seen.parsed?.kind === protocol_1.MESSAGE_KIND.JOB && (event === protocol_1.JOB_EVENT.ARRIVED || event === protocol_1.JOB_EVENT.TRANSFER_FAILED);
|
|
90
|
+
}
|
|
91
|
+
/** An arrival that read no id at all: no row can ever claim it, so it goes to a person on its first delivery. */
|
|
92
|
+
function isUnreadableArrival(seen) {
|
|
93
|
+
return seen.parsed?.kind === protocol_1.MESSAGE_KIND.JOB && (0, protocol_1.isUnreadableRead)(seen.body?.body ?? {});
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* An arrival no open handoff into the machine expects, after every delivery:
|
|
97
|
+
* a person sees it on one handoff-error row per machine and carrier, with the
|
|
98
|
+
* raw arrival, so the unit can be re-scanned or routed. Resolving the row lets
|
|
99
|
+
* the next such arrival raise one again.
|
|
100
|
+
*/
|
|
101
|
+
async function holdArrival(ctx, msg, seen) {
|
|
102
|
+
const { ident } = seen.parsed;
|
|
103
|
+
const body = seen.body?.body ?? {};
|
|
104
|
+
const carrierId = typeof body.carrierId === 'string' ? body.carrierId : null;
|
|
105
|
+
const unitId = typeof body.unitId === 'string' ? body.unitId : null;
|
|
106
|
+
const detail = `no handoff into ${ident.machineId} expects ${carrierId ? `carrier ${carrierId}` : 'an unreadable carrier'}`;
|
|
107
|
+
try {
|
|
108
|
+
const open = await ctx.openRows({ type: contracts_1.ROW_TYPE.HANDOFF_ERROR, role: (0, line_1.operatorRole)(ctx.line) });
|
|
109
|
+
const shown = open.find((r) => r.metadata?.errorCode === contracts_1.HANDOFF_ERROR.UNMATCHED_ARRIVAL && r.metadata?.machineId === ident.machineId && (r.metadata?.carrierId ?? null) === carrierId);
|
|
110
|
+
if (shown)
|
|
111
|
+
return { disposition: transport_1.DISPOSITION.ACK, failed: false, actions: [{ verb: 'unmatched', rowId: shown.id, detail: `${detail} (already shown to a person)` }] };
|
|
112
|
+
const [made] = await ctx.executor.apply([{ kind: 'createRow', row: {
|
|
113
|
+
type: contracts_1.ROW_TYPE.HANDOFF_ERROR, role: (0, line_1.operatorRole)(ctx.line), priority: 2,
|
|
114
|
+
description: `${ident.machineId} reports an arrival no handoff expects: ${carrierId ? `carrier ${carrierId}` : 'the carrier could not be read'}`,
|
|
115
|
+
metadata: {
|
|
116
|
+
siteId: ident.siteId, lineId: ident.lineId, machineId: ident.machineId, errorCode: contracts_1.HANDOFF_ERROR.UNMATCHED_ARRIVAL,
|
|
117
|
+
carrierId, unitId, event: body.event, topic: msg.topic, messageId: seen.body?.id ?? null, arrival: body,
|
|
118
|
+
},
|
|
119
|
+
escalation_payload: msg.payload,
|
|
120
|
+
} }]);
|
|
121
|
+
return { disposition: transport_1.DISPOSITION.ACK, failed: false, actions: [{ verb: 'create-row', rowId: made?.rowId, detail }] };
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
if (!(err instanceof dispatcher_1.GatewayStopped))
|
|
125
|
+
ctx.log.error(log_1.GATEWAY_LOG.VERB_FAILED, { topic: msg.topic, error: err.message });
|
|
126
|
+
return { disposition: transport_1.DISPOSITION.NAK, failed: true, actions: [] };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -42,6 +42,8 @@ const escalationApi = __importStar(require("../../../api/escalations"));
|
|
|
42
42
|
const uuid_1 = require("../../../lib/uuid");
|
|
43
43
|
const escalations_1 = require("./escalations");
|
|
44
44
|
const actions_1 = require("./actions");
|
|
45
|
+
/** A row that was never given items (one parked by hand) answers 400; it holds none. */
|
|
46
|
+
const NO_ITEMS_STATUS = 400;
|
|
45
47
|
class ApiLineEscalations {
|
|
46
48
|
constructor(userFor) {
|
|
47
49
|
this.userFor = userFor;
|
|
@@ -75,6 +77,8 @@ class ApiLineEscalations {
|
|
|
75
77
|
}
|
|
76
78
|
async items(id) {
|
|
77
79
|
const result = await escalationApi.getEscalationItems({ id }, this.auth(actions_1.CONTROLLER));
|
|
80
|
+
if (result.status === NO_ITEMS_STATUS)
|
|
81
|
+
return [];
|
|
78
82
|
if (result.status >= 300)
|
|
79
83
|
throw new Error(`items failed ${result.status}: ${result.error}`);
|
|
80
84
|
return result.data.items.map((i) => ({ itemKey: i.itemKey, payload: i.payload ?? {} }));
|
|
@@ -52,12 +52,15 @@ export declare class Dispatcher {
|
|
|
52
52
|
/** Work still running sends nothing more and changes no row. */
|
|
53
53
|
stop(): void;
|
|
54
54
|
private live;
|
|
55
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* This send's attempt number, whether the machine may have the command already, and when it was
|
|
57
|
+
* issued: the time given (a round's start, the same for every gateway), else the first send's.
|
|
58
|
+
*/
|
|
56
59
|
private countSend;
|
|
57
60
|
/** False when the codec has no message for the command. `since` is when the command's row was created. */
|
|
58
|
-
publish(machineId: string, op: string, cmdId: string, args: Record<string, any>, since?: number): Promise<boolean>;
|
|
61
|
+
publish(machineId: string, op: string, cmdId: string, args: Record<string, any>, since?: number, issuedAt?: number): Promise<boolean>;
|
|
59
62
|
/** Publish and wait for the reply, asking again with the same id; null when none came. */
|
|
60
|
-
ask(machineId: string, op: string, cmdId: string, args: Record<string, any>, since?: number): Promise<CommandReply | null>;
|
|
63
|
+
ask(machineId: string, op: string, cmdId: string, args: Record<string, any>, since?: number, issuedAt?: number): Promise<CommandReply | null>;
|
|
61
64
|
command(a: CommandAction): Promise<void>;
|
|
62
65
|
/**
|
|
63
66
|
* A machine-command row: send it, then resolve the row with the reply, as the
|
|
@@ -68,7 +71,9 @@ export declare class Dispatcher {
|
|
|
68
71
|
*/
|
|
69
72
|
control(a: ControlAction): Promise<void>;
|
|
70
73
|
dispatch(a: DispatchAction): Promise<void>;
|
|
71
|
-
/**
|
|
74
|
+
/** A job sent again after the machine restarted, for a row that already ended: the reply is only logged. */
|
|
75
|
+
private resent;
|
|
76
|
+
/** The units were already at the machine: a job it has taken and is ready for completes the handoff. */
|
|
72
77
|
private received;
|
|
73
78
|
/** Ask a silent machine what it holds; null when it cannot say or does not answer in time. */
|
|
74
79
|
private heldJobs;
|
|
@@ -57,20 +57,25 @@ class Dispatcher {
|
|
|
57
57
|
if (this.stopping.signal.aborted)
|
|
58
58
|
throw new GatewayStopped();
|
|
59
59
|
}
|
|
60
|
-
/**
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
/**
|
|
61
|
+
* This send's attempt number, whether the machine may have the command already, and when it was
|
|
62
|
+
* issued: the time given (a round's start, the same for every gateway), else the first send's.
|
|
63
|
+
*/
|
|
64
|
+
countSend(cmdId, since, issuedAt) {
|
|
65
|
+
const before = this.sent.get(cmdId);
|
|
66
|
+
const record = { attempt: (before?.attempt ?? 0) + 1, issuedAt: issuedAt ?? before?.issuedAt ?? Date.now() };
|
|
63
67
|
this.sent.delete(cmdId);
|
|
64
|
-
this.sent.set(cmdId,
|
|
68
|
+
this.sent.set(cmdId, record);
|
|
65
69
|
if (this.sent.size > SENT_REMEMBERED)
|
|
66
70
|
this.sent.delete(this.sent.keys().next().value);
|
|
67
|
-
|
|
71
|
+
const retry = record.attempt > 1 || (since !== undefined && since < this.d.state.takenAt);
|
|
72
|
+
return { attempt: record.attempt, retry, issuedAt: (0, protocol_1.isoTs)(record.issuedAt) };
|
|
68
73
|
}
|
|
69
74
|
/** False when the codec has no message for the command. `since` is when the command's row was created. */
|
|
70
|
-
async publish(machineId, op, cmdId, args, since) {
|
|
75
|
+
async publish(machineId, op, cmdId, args, since, issuedAt) {
|
|
71
76
|
this.live();
|
|
72
77
|
const ident = (0, registration_1.identFor)(this.d.state, this.d.line, machineId);
|
|
73
|
-
const command = { cmdId, op, args, ...this.countSend(cmdId, since) };
|
|
78
|
+
const command = { cmdId, op, args, ...this.countSend(cmdId, since, issuedAt) };
|
|
74
79
|
const wires = this.d.codec.encode(ident, command);
|
|
75
80
|
for (const wire of wires) {
|
|
76
81
|
await this.d.bus.publish(wire.topic, wire.payload);
|
|
@@ -79,11 +84,11 @@ class Dispatcher {
|
|
|
79
84
|
return wires.length > 0;
|
|
80
85
|
}
|
|
81
86
|
/** Publish and wait for the reply, asking again with the same id; null when none came. */
|
|
82
|
-
async ask(machineId, op, cmdId, args, since) {
|
|
87
|
+
async ask(machineId, op, cmdId, args, since, issuedAt) {
|
|
83
88
|
for (let attempt = 1; attempt <= this.d.dispatchAttempts; attempt++) {
|
|
84
89
|
this.live();
|
|
85
90
|
const waiting = this.d.replies.wait(cmdId, this.d.replyTimeoutMs);
|
|
86
|
-
if (!await this.publish(machineId, op, cmdId, args, since)) {
|
|
91
|
+
if (!await this.publish(machineId, op, cmdId, args, since, issuedAt)) {
|
|
87
92
|
this.d.replies.settle({ cmdId, status: contracts_1.COMMAND_STATUS.UNSUPPORTED, reason: `the line's codec has no ${op} command` });
|
|
88
93
|
}
|
|
89
94
|
const reply = await waiting;
|
|
@@ -112,8 +117,9 @@ class Dispatcher {
|
|
|
112
117
|
*/
|
|
113
118
|
async control(a) {
|
|
114
119
|
const now = Date.now();
|
|
115
|
-
const
|
|
116
|
-
const
|
|
120
|
+
const ttlMs = this.d.replyTimeoutMs * this.d.dispatchAttempts;
|
|
121
|
+
const { round, expiresAt } = (0, pacing_1.dispatchWindow)(a.createdAt ?? now, now, ttlMs);
|
|
122
|
+
const reply = await this.ask(a.machineId, a.op, (0, contracts_1.controlCommandId)(a.rowId, round), { ...a.args, expiresAt: (0, protocol_1.isoTs)(expiresAt) }, a.createdAt, expiresAt - ttlMs);
|
|
117
123
|
// Timers may fire a little early; the command must have expired before the row says no-reply.
|
|
118
124
|
if (!reply && Date.now() <= expiresAt)
|
|
119
125
|
await (0, promises_1.setTimeout)(expiresAt - Date.now() + 1, undefined, { signal: this.stopping.signal }).catch(() => undefined);
|
|
@@ -142,22 +148,36 @@ class Dispatcher {
|
|
|
142
148
|
for (const job of jobs)
|
|
143
149
|
state.rememberDispatch(`${a.cmdId}${job.suffix}`, a.rowId);
|
|
144
150
|
const since = state.get(a.rowId)?.createdAt;
|
|
145
|
-
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 }, since)));
|
|
151
|
+
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 }, since, a.issuedAt)));
|
|
146
152
|
const refused = replies.find((r) => r && !took(r));
|
|
147
153
|
const silent = jobs.filter((_, i) => !replies[i]);
|
|
148
154
|
const held = !refused && silent.length ? await this.heldJobs(a.machineId, a.cmdId) : null;
|
|
149
155
|
this.live();
|
|
150
156
|
const reconciled = !!held && silent.every((j) => held.has(j.jobId));
|
|
157
|
+
if (a.ended) {
|
|
158
|
+
this.resent(a, jobs, refused ?? null, !refused && (!silent.length || reconciled));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
151
161
|
const row = state.get(a.rowId);
|
|
152
162
|
if (!refused && (!silent.length || reconciled)) {
|
|
153
163
|
if (row)
|
|
154
164
|
row.dispatch = state_1.DISPATCH_STATE.ACCEPTED;
|
|
165
|
+
for (const job of jobs)
|
|
166
|
+
state.rememberSent(a.machineId, job.jobId, a.rowId);
|
|
155
167
|
if (reconciled)
|
|
156
168
|
log.info(log_1.GATEWAY_LOG.DISPATCH_RECONCILED, { row: a.rowId, machine: a.machineId, jobs: silent.map((j) => j.jobId) });
|
|
157
169
|
log.info(log_1.GATEWAY_LOG.DISPATCHED, { row: a.rowId, machine: a.machineId, reason: replies.map((r) => r?.reason ?? 'held').join('; ') });
|
|
158
170
|
this.check(a.rowId, 'claim', await lt.claim(a.machineId, a.rowId));
|
|
159
|
-
if (a.receivedOnAccept)
|
|
171
|
+
if (!a.receivedOnAccept)
|
|
172
|
+
return;
|
|
173
|
+
const preparing = jobs.filter((job, i) => replies[i]?.status === protocol_1.REPLY_STATUS.ACCEPTED && replies[i]?.preparing).map((job) => `${a.cmdId}${job.suffix}`);
|
|
174
|
+
if (!preparing.length) {
|
|
160
175
|
await this.received(a);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (row)
|
|
179
|
+
row.awaitingReady = new Set(preparing);
|
|
180
|
+
log.info(log_1.GATEWAY_LOG.DISPATCH_PREPARING, { row: a.rowId, machine: a.machineId, jobs: preparing });
|
|
161
181
|
return;
|
|
162
182
|
}
|
|
163
183
|
// The handoff may have ended meanwhile: a machine that ran the job anyway reports the arrival.
|
|
@@ -176,7 +196,15 @@ class Dispatcher {
|
|
|
176
196
|
return;
|
|
177
197
|
await this.withdraw(a, jobs.filter((_, i) => !replies[i] || took(replies[i])));
|
|
178
198
|
}
|
|
179
|
-
/**
|
|
199
|
+
/** A job sent again after the machine restarted, for a row that already ended: the reply is only logged. */
|
|
200
|
+
resent(a, jobs, refused, taken) {
|
|
201
|
+
const context = { row: a.rowId, machine: a.machineId, jobs: jobs.map((j) => j.jobId), resent: true };
|
|
202
|
+
if (taken)
|
|
203
|
+
this.d.log.info(log_1.GATEWAY_LOG.DISPATCHED, { ...context, reason: 'sent again after a restart' });
|
|
204
|
+
else
|
|
205
|
+
this.d.log.warn(log_1.GATEWAY_LOG.DISPATCH_REFUSED, { ...context, reason: refused ? `${refused.status}: ${refused.reason}` : 'no reply' });
|
|
206
|
+
}
|
|
207
|
+
/** The units were already at the machine: a job it has taken and is ready for completes the handoff. */
|
|
180
208
|
async received(a) {
|
|
181
209
|
this.live();
|
|
182
210
|
const units = a.args.units.map((u) => u.unitId);
|
|
@@ -57,6 +57,8 @@ export declare class Executor {
|
|
|
57
57
|
* carrying the units out of it.
|
|
58
58
|
*/
|
|
59
59
|
private lateReply;
|
|
60
|
+
/** On arrival dispatch, a row that waited for the machine to prepare is received once every job it sent is ready. */
|
|
61
|
+
private readyForArrival;
|
|
60
62
|
/** Add the item; when the row then holds every key the action waits for, resolve it. */
|
|
61
63
|
private accumulate;
|
|
62
64
|
private check;
|
|
@@ -109,7 +109,7 @@ class Executor {
|
|
|
109
109
|
this.watchOffline(a.machineId);
|
|
110
110
|
return [(0, wire_1.plannedAction)(a)];
|
|
111
111
|
case 'settleReply':
|
|
112
|
-
if (replies.settle({ cmdId: a.cmdId, status: a.status, reason: a.reason, ...(a.code ? { code: a.code } : {}) }))
|
|
112
|
+
if (replies.settle({ cmdId: a.cmdId, status: a.status, reason: a.reason, ...(a.code ? { code: a.code } : {}), ...(a.preparing ? { preparing: true } : {}) }))
|
|
113
113
|
return [(0, wire_1.plannedAction)(a)];
|
|
114
114
|
return [{ verb: 'ack', detail: `late ${a.status} ${a.cmdId}` }, ...await this.lateReply(a)];
|
|
115
115
|
case 'settleSnapshot':
|
|
@@ -123,6 +123,9 @@ class Executor {
|
|
|
123
123
|
case 'refuseBinding':
|
|
124
124
|
this.d.log.warn(log_1.GATEWAY_LOG.ADDRESS_REFUSED, { machine: a.machineId, address: a.address, reason: a.reason });
|
|
125
125
|
return [(0, wire_1.plannedAction)(a)];
|
|
126
|
+
case 'unrecoverable':
|
|
127
|
+
this.d.log.warn(log_1.GATEWAY_LOG.DISPATCH_UNRECOVERABLE, { machine: a.machineId, jobs: a.jobs });
|
|
128
|
+
return [(0, wire_1.plannedAction)(a)];
|
|
126
129
|
}
|
|
127
130
|
}
|
|
128
131
|
/**
|
|
@@ -140,7 +143,7 @@ class Executor {
|
|
|
140
143
|
const context = { row: into.id, machine: m, cmdId: a.cmdId, reason: a.reason };
|
|
141
144
|
if (a.status === protocol_1.REPLY_STATUS.COMPLETED) {
|
|
142
145
|
log.info(log_1.GATEWAY_LOG.DISPATCH_READY, context);
|
|
143
|
-
return
|
|
146
|
+
return this.readyForArrival(into, m, a.cmdId);
|
|
144
147
|
}
|
|
145
148
|
if (a.status !== protocol_1.REPLY_STATUS.FAILED)
|
|
146
149
|
return [];
|
|
@@ -152,6 +155,19 @@ class Executor {
|
|
|
152
155
|
received: false, errorCode: contracts_1.HANDOFF_ERROR.DISPATCH_FAILED, detail: `${m} could not prepare the job: ${a.reason}`, ...(a.code ? { code: a.code } : {}),
|
|
153
156
|
}));
|
|
154
157
|
}
|
|
158
|
+
/** On arrival dispatch, a row that waited for the machine to prepare is received once every job it sent is ready. */
|
|
159
|
+
async readyForArrival(into, m, cmdId) {
|
|
160
|
+
const { lt, state } = this.d;
|
|
161
|
+
if (into.ended || !into.awaitingReady?.has(cmdId))
|
|
162
|
+
return [];
|
|
163
|
+
into.awaitingReady.delete(cmdId);
|
|
164
|
+
if (into.awaitingReady.size)
|
|
165
|
+
return [];
|
|
166
|
+
const units = into.units.map((u) => u.unitId);
|
|
167
|
+
const resolved = await lt.resolve(m, into.id, { received: true, receiver: m, units });
|
|
168
|
+
state.close(into.id);
|
|
169
|
+
return [outcomeOf('resolve', into.id, resolved)];
|
|
170
|
+
}
|
|
155
171
|
/** Add the item; when the row then holds every key the action waits for, resolve it. */
|
|
156
172
|
async accumulate(a) {
|
|
157
173
|
const { lt, state } = this.d;
|
|
@@ -15,6 +15,7 @@ import type { LineEscalations } from './escalations';
|
|
|
15
15
|
import { type GatewayLog } from './log';
|
|
16
16
|
import { GatewayState } from './state';
|
|
17
17
|
import { type WireSink } from './wire';
|
|
18
|
+
import { WireWindow } from './wire-window';
|
|
18
19
|
export interface GatewaySettings {
|
|
19
20
|
topicRoot: string;
|
|
20
21
|
replyTimeoutMs: number;
|
|
@@ -32,6 +33,8 @@ export interface GatewaySettings {
|
|
|
32
33
|
progressStep?: number;
|
|
33
34
|
/** Record telemetry messages on the wire too; off unless set. */
|
|
34
35
|
wireTelemetry?: boolean;
|
|
36
|
+
/** How many of the latest wire events the gateway keeps for reading over HTTP; 500 by default. */
|
|
37
|
+
wireWindow?: number;
|
|
35
38
|
}
|
|
36
39
|
export interface GatewayDeps {
|
|
37
40
|
bus: MachineBus;
|
|
@@ -60,6 +63,8 @@ export declare class MachineGateway {
|
|
|
60
63
|
private readonly addresses;
|
|
61
64
|
private readonly admission;
|
|
62
65
|
private readonly wire;
|
|
66
|
+
/** The line's last wire events, newest first on read. */
|
|
67
|
+
readonly window: WireWindow;
|
|
63
68
|
private quietTimer?;
|
|
64
69
|
constructor(deps: GatewayDeps);
|
|
65
70
|
start(): Promise<void>;
|
|
@@ -28,6 +28,7 @@ const plan_machine_1 = require("./plan-machine");
|
|
|
28
28
|
const state_1 = require("./state");
|
|
29
29
|
const receipts_1 = require("./receipts");
|
|
30
30
|
const wire_1 = require("./wire");
|
|
31
|
+
const wire_window_1 = require("./wire-window");
|
|
31
32
|
class MachineGateway {
|
|
32
33
|
constructor(deps) {
|
|
33
34
|
this.deps = deps;
|
|
@@ -42,14 +43,16 @@ class MachineGateway {
|
|
|
42
43
|
this.state.retainedAlarms = this.codec.retainedAlarms === true;
|
|
43
44
|
this.state.progressStep = deps.settings.progressStep ?? this.state.progressStep;
|
|
44
45
|
this.addresses = deps.addresses ?? new address_book_1.MemoryAddressBook();
|
|
45
|
-
this.
|
|
46
|
+
this.window = new wire_window_1.WireWindow(deps.settings.wireWindow);
|
|
47
|
+
const sink = deps.wire ?? publish_1.publishMachineWireEvent;
|
|
48
|
+
this.wire = new wire_1.WireRecorder(deps.line, (event) => { this.window.push(event); sink(event); }, { telemetry: deps.settings.wireTelemetry === true });
|
|
46
49
|
this.executor = new executor_1.Executor({
|
|
47
50
|
addresses: this.addresses, wire: this.wire,
|
|
48
51
|
bus: deps.bus, lt: deps.lt, replies: new reply_waiter_1.ReplyWaiter(), state: this.state, line: deps.line,
|
|
49
52
|
codec: this.codec, replyTimeoutMs: deps.settings.replyTimeoutMs,
|
|
50
53
|
dispatchAttempts: deps.settings.dispatchAttempts, offlineDebounceMs: deps.settings.offlineDebounceMs, log: this.log,
|
|
51
54
|
});
|
|
52
|
-
this.admission = { state: this.state, line: deps.line, executor: this.executor, log: this.log, pace: () => this.pace() };
|
|
55
|
+
this.admission = { state: this.state, line: deps.line, executor: this.executor, log: this.log, pace: () => this.pace(), openRows: (q) => deps.lt.listOpen(q) };
|
|
53
56
|
this.receipts = new receipts_1.ReceiptBatcher({ bus: deps.bus, codec: this.codec, delayMs: deps.settings.receiptDelayMs ?? DEFAULT_RECEIPT_DELAY_MS, log: this.log, wire: this.wire });
|
|
54
57
|
}
|
|
55
58
|
async start() {
|
|
@@ -231,9 +234,14 @@ class MachineGateway {
|
|
|
231
234
|
if (seen.violations.length && msg.deliveries === 1) {
|
|
232
235
|
this.log.warn(log_1.GATEWAY_LOG.CONTRACT_VIOLATION, { topic: msg.topic, violations: seen.violations });
|
|
233
236
|
}
|
|
234
|
-
|
|
237
|
+
let applied = seen.violations.length && this.codec.strict
|
|
235
238
|
? await (0, admission_1.holdViolation)(this.admission, msg, seen)
|
|
236
239
|
: await this.applyMachine(msg.topic, seen.parsed, seen.body);
|
|
240
|
+
// An arrival still waiting for its row after every delivery is shown to a person instead; one that read no id at once.
|
|
241
|
+
const waited = msg.deliveries >= wire_1.MAX_DELIVERIES || (0, admission_1.isUnreadableArrival)(seen);
|
|
242
|
+
if (applied.disposition === transport_1.DISPOSITION.NAK && !applied.failed && waited && (0, admission_1.isArrival)(seen)) {
|
|
243
|
+
applied = await (0, admission_1.holdArrival)(this.admission, msg, seen);
|
|
244
|
+
}
|
|
237
245
|
failed ||= applied.failed;
|
|
238
246
|
outcome = worse(outcome, applied.disposition);
|
|
239
247
|
this.wire.inbound(msg, seen, { ...applied, actions: [...admitted.actions, ...applied.actions] }, msg.deliveries);
|
|
@@ -5,8 +5,10 @@ export declare const GATEWAY_LOG: {
|
|
|
5
5
|
readonly DISPATCHED: "machine_gateway.dispatch.accepted";
|
|
6
6
|
readonly DISPATCH_REFUSED: "machine_gateway.dispatch.refused";
|
|
7
7
|
readonly DISPATCH_READY: "machine_gateway.dispatch.ready";
|
|
8
|
+
readonly DISPATCH_PREPARING: "machine_gateway.dispatch.preparing";
|
|
8
9
|
readonly DISPATCH_FAILED: "machine_gateway.dispatch.failed";
|
|
9
10
|
readonly DISPATCH_RECONCILED: "machine_gateway.dispatch.reconciled";
|
|
11
|
+
readonly DISPATCH_UNRECOVERABLE: "machine_gateway.dispatch.unrecoverable";
|
|
10
12
|
readonly VERB_INVALID: "machine_gateway.verb.invalid";
|
|
11
13
|
readonly VERB_FAILED: "machine_gateway.verb.failed";
|
|
12
14
|
readonly ROW_NOT_VISIBLE: "machine_gateway.verb.row_not_visible";
|
|
@@ -30,6 +32,7 @@ export declare const GATEWAY_LOG: {
|
|
|
30
32
|
readonly LEADING: "machine_gateway.line.leading";
|
|
31
33
|
readonly STANDBY: "machine_gateway.line.standby";
|
|
32
34
|
readonly LEADERSHIP_LOST: "machine_gateway.line.leadership_lost";
|
|
35
|
+
readonly WIRE_LOST: "machine_gateway.line.wire_lost";
|
|
33
36
|
readonly START_FAILED: "machine_gateway.line.start_failed";
|
|
34
37
|
};
|
|
35
38
|
export type GatewayLogEvent = typeof GATEWAY_LOG[keyof typeof GATEWAY_LOG];
|
|
@@ -10,8 +10,10 @@ exports.GATEWAY_LOG = {
|
|
|
10
10
|
DISPATCHED: 'machine_gateway.dispatch.accepted',
|
|
11
11
|
DISPATCH_REFUSED: 'machine_gateway.dispatch.refused',
|
|
12
12
|
DISPATCH_READY: 'machine_gateway.dispatch.ready',
|
|
13
|
+
DISPATCH_PREPARING: 'machine_gateway.dispatch.preparing',
|
|
13
14
|
DISPATCH_FAILED: 'machine_gateway.dispatch.failed',
|
|
14
15
|
DISPATCH_RECONCILED: 'machine_gateway.dispatch.reconciled',
|
|
16
|
+
DISPATCH_UNRECOVERABLE: 'machine_gateway.dispatch.unrecoverable',
|
|
15
17
|
VERB_INVALID: 'machine_gateway.verb.invalid',
|
|
16
18
|
VERB_FAILED: 'machine_gateway.verb.failed',
|
|
17
19
|
ROW_NOT_VISIBLE: 'machine_gateway.verb.row_not_visible',
|
|
@@ -35,6 +37,7 @@ exports.GATEWAY_LOG = {
|
|
|
35
37
|
LEADING: 'machine_gateway.line.leading',
|
|
36
38
|
STANDBY: 'machine_gateway.line.standby',
|
|
37
39
|
LEADERSHIP_LOST: 'machine_gateway.line.leadership_lost',
|
|
40
|
+
WIRE_LOST: 'machine_gateway.line.wire_lost',
|
|
38
41
|
START_FAILED: 'machine_gateway.line.start_failed',
|
|
39
42
|
};
|
|
40
43
|
exports.consoleLog = {
|
|
@@ -56,6 +56,8 @@ class LtHttp {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
exports.LtHttp = LtHttp;
|
|
59
|
+
/** A row that was never given items (one parked by hand) answers 400; it holds none. */
|
|
60
|
+
const NO_ITEMS_STATUS = 400;
|
|
59
61
|
/** Escalation verbs over HTTP, each run with the bearer token of the actor it runs as. */
|
|
60
62
|
class HttpLineEscalations {
|
|
61
63
|
constructor(http, tokenFor, claimMinutes = 24 * 60) {
|
|
@@ -80,8 +82,15 @@ class HttpLineEscalations {
|
|
|
80
82
|
return (await this.items(id)).map((i) => i.itemKey);
|
|
81
83
|
}
|
|
82
84
|
async items(id) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
+
try {
|
|
86
|
+
const view = await this.http.request('GET', `/escalations/${id}/items`, this.tokenFor(actions_1.CONTROLLER));
|
|
87
|
+
return view.items.map((i) => ({ itemKey: i.itemKey, payload: i.payload ?? {} }));
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
if (err instanceof LtHttpError && err.status === NO_ITEMS_STATUS)
|
|
91
|
+
return [];
|
|
92
|
+
throw err;
|
|
93
|
+
}
|
|
85
94
|
}
|
|
86
95
|
async get(id) {
|
|
87
96
|
if (!(0, uuid_1.isUuid)(id))
|
|
@@ -66,7 +66,7 @@ function dispatchAction(row, machineId, dispatchTtlMs) {
|
|
|
66
66
|
row.createdAt ??= now;
|
|
67
67
|
const { round, expiresAt } = dispatchWindow(row.dueAt ?? row.createdAt, now, dispatchTtlMs);
|
|
68
68
|
return {
|
|
69
|
-
kind: 'dispatch', rowId: row.id, machineId, cmdId: (0, contracts_1.dispatchCommandId)(row.id, round), args: dispatchArgs(row, expiresAt),
|
|
69
|
+
kind: 'dispatch', rowId: row.id, machineId, cmdId: (0, contracts_1.dispatchCommandId)(row.id, round), args: dispatchArgs(row, expiresAt), issuedAt: expiresAt - dispatchTtlMs,
|
|
70
70
|
...(row.dispatch === state_1.DISPATCH_STATE.ARRIVING ? { receivedOnAccept: true } : {}),
|
|
71
71
|
};
|
|
72
72
|
}
|