@hotmeshio/long-tail 0.25.3 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/build/index.d.ts +1 -0
- package/build/index.js +8 -1
- package/build/lib/machines/index.d.ts +5 -0
- package/build/lib/machines/index.js +21 -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 +92 -0
- package/build/lib/machines/protocol/codec.js +79 -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 +132 -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/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 +20 -0
- package/build/lib/machines/site/site-conf.js +55 -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 +21 -0
- package/build/lib/machines/transport/delivery.js +42 -0
- package/build/lib/machines/transport/index.d.ts +11 -0
- package/build/lib/machines/transport/index.js +27 -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 +42 -0
- package/build/lib/machines/transport/nats-bus.js +67 -0
- package/build/lib/machines/transport/nats-durable.d.ts +11 -0
- package/build/lib/machines/transport/nats-durable.js +28 -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/serial.d.ts +21 -0
- package/build/lib/machines/transport/serial.js +54 -0
- package/build/lib/machines/transport/streams.d.ts +28 -0
- package/build/lib/machines/transport/streams.js +92 -0
- package/build/routes/oauth-server/authorize.js +4 -4
- package/build/scripts/machine-line.d.ts +33 -0
- package/build/scripts/machine-line.js +301 -0
- package/build/services/auth/oauth-server/authorization.d.ts +5 -0
- package/build/services/auth/oauth-server/authorization.js +8 -0
- package/build/services/auth/oauth-server/metadata.d.ts +1 -1
- package/build/services/auth/oauth-server/metadata.js +2 -1
- package/build/services/machines/commands.d.ts +29 -0
- package/build/services/machines/commands.js +37 -0
- package/build/services/machines/config.d.ts +43 -0
- package/build/services/machines/config.js +84 -0
- package/build/services/machines/contracts.d.ts +193 -0
- package/build/services/machines/contracts.js +115 -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 +63 -0
- package/build/services/machines/gateway/dispatcher.js +157 -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 +55 -0
- package/build/services/machines/gateway/executor.js +195 -0
- package/build/services/machines/gateway/gateway.d.ts +82 -0
- package/build/services/machines/gateway/gateway.js +247 -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/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 +29 -0
- package/build/services/machines/gateway/pacing.js +134 -0
- package/build/services/machines/gateway/plan-escalation.d.ts +19 -0
- package/build/services/machines/gateway/plan-escalation.js +201 -0
- package/build/services/machines/gateway/plan-machine.d.ts +16 -0
- package/build/services/machines/gateway/plan-machine.js +242 -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 +33 -0
- package/build/services/machines/gateway/reconcile.js +86 -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 +125 -0
- package/build/services/machines/gateway/state.js +228 -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 +22 -0
- package/build/services/machines/leadership.js +75 -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 +56 -0
- package/build/services/machines/select.d.ts +18 -0
- package/build/services/machines/select.js +63 -0
- package/build/services/machines/surface.d.ts +33 -0
- package/build/services/machines/surface.js +98 -0
- package/build/services/machines/testing/fake-long-tail.d.ts +78 -0
- package/build/services/machines/testing/fake-long-tail.js +155 -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/index.js +13 -0
- 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 +2 -1
- package/build/types/machines.d.ts +166 -0
- package/build/types/machines.js +7 -0
- package/build/types/startup.d.ts +23 -0
- package/docs/machines.md +293 -0
- package/docs/mcp.md +6 -1
- package/package.json +11 -1
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The line protocol as JSON Schema plus the rules a schema cannot express.
|
|
4
|
+
* Known fields are checked; unknown fields are allowed, so a machine on a
|
|
5
|
+
* newer minor version can add fields without breaking older gateways. The
|
|
6
|
+
* gateway checks every inbound message; the tests check everything the mock sends.
|
|
7
|
+
*/
|
|
8
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
9
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.inspectMessage = inspectMessage;
|
|
13
|
+
const ajv_1 = __importDefault(require("ajv"));
|
|
14
|
+
const constants_1 = require("./constants");
|
|
15
|
+
const envelope_1 = require("./envelope");
|
|
16
|
+
const topics_1 = require("./topics");
|
|
17
|
+
const STR = { type: 'string' };
|
|
18
|
+
const STR_N = { type: ['string', 'null'] };
|
|
19
|
+
const NUM = { type: 'number' };
|
|
20
|
+
const INT0 = { type: 'integer', minimum: 0 };
|
|
21
|
+
const BOOL = { type: 'boolean' };
|
|
22
|
+
const TS = { type: 'string', pattern: '^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$' };
|
|
23
|
+
const TS_N = { anyOf: [TS, { type: 'null' }] };
|
|
24
|
+
const UUID = { type: 'string', pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' };
|
|
25
|
+
const oneOf = (o, nullable = false) => ({ enum: nullable ? [...Object.values(o), null] : Object.values(o) });
|
|
26
|
+
function shape(props, optional = []) {
|
|
27
|
+
return { type: 'object', properties: props, required: Object.keys(props).filter((k) => !optional.includes(k)) };
|
|
28
|
+
}
|
|
29
|
+
const ENVELOPE = shape({
|
|
30
|
+
v: { type: 'string', pattern: '^1(\\.\\d+)?$' },
|
|
31
|
+
id: UUID,
|
|
32
|
+
at: TS,
|
|
33
|
+
source: shape({ site: STR, line: STR, machine: STR, station: STR }, ['station']),
|
|
34
|
+
body: { type: 'object' },
|
|
35
|
+
});
|
|
36
|
+
const RECIPE = { anyOf: [shape({ id: STR, version: STR }), { type: 'null' }] };
|
|
37
|
+
const BODY = {
|
|
38
|
+
[topics_1.MESSAGE_KIND.PRESENCE]: shape({ online: BOOL, software: STR }, ['software']),
|
|
39
|
+
[topics_1.MESSAGE_KIND.SNAPSHOT]: shape({
|
|
40
|
+
bootId: STR, software: STR, mode: oneOf(constants_1.MODE), enabled: BOOL, recipe: RECIPE,
|
|
41
|
+
jobs: { type: 'array', items: shape({ jobId: STR, units: { type: 'array', items: STR }, status: STR }) },
|
|
42
|
+
alarms: { type: 'array', items: STR },
|
|
43
|
+
cycles: INT0,
|
|
44
|
+
}),
|
|
45
|
+
[topics_1.MESSAGE_KIND.JOB]: shape({
|
|
46
|
+
event: oneOf(constants_1.JOB_EVENT), jobId: STR, unitId: STR, orderRef: STR, recipe: RECIPE, carrierId: STR_N, peer: STR_N,
|
|
47
|
+
result: oneOf(constants_1.JOB_RESULT), reason: STR_N,
|
|
48
|
+
measurements: {
|
|
49
|
+
type: 'array',
|
|
50
|
+
items: shape({ name: STR, value: NUM, unit: STR_N, min: { type: ['number', 'null'] }, max: { type: ['number', 'null'] }, ok: BOOL }),
|
|
51
|
+
},
|
|
52
|
+
materials: { type: 'array', items: shape({ material: STR, lot: STR }) },
|
|
53
|
+
startedAt: TS, endedAt: TS, durationMs: INT0, failure: oneOf(constants_1.TRANSFER_FAILURE),
|
|
54
|
+
}, ['peer', 'result', 'reason', 'measurements', 'materials', 'startedAt', 'endedAt', 'durationMs', 'failure']),
|
|
55
|
+
[topics_1.MESSAGE_KIND.ALARM]: shape({
|
|
56
|
+
alarmId: STR, code: STR, level: oneOf(constants_1.LEVEL), text: STR, active: BOOL,
|
|
57
|
+
raisedAt: TS, clearedAt: TS_N, clearedBy: STR_N, ackedBy: STR_N, ackedAt: TS_N,
|
|
58
|
+
zone: STR_N, autoClears: BOOL, action: STR_N, jobId: STR_N,
|
|
59
|
+
}),
|
|
60
|
+
[topics_1.MESSAGE_KIND.TELEMETRY]: shape({ readings: { type: 'object', additionalProperties: NUM } }),
|
|
61
|
+
[topics_1.MESSAGE_KIND.CMD]: shape({ cmdId: STR, op: oneOf(constants_1.OP), args: { type: 'object' } }),
|
|
62
|
+
[topics_1.MESSAGE_KIND.REPLY]: shape({ cmdId: STR, status: oneOf(constants_1.REPLY_STATUS), reason: STR }),
|
|
63
|
+
[topics_1.MESSAGE_KIND.REPORT]: { type: 'object' },
|
|
64
|
+
};
|
|
65
|
+
const ajv = new ajv_1.default({ allErrors: true, strict: false });
|
|
66
|
+
const ENVELOPE_V = ajv.compile(ENVELOPE);
|
|
67
|
+
const BODY_V = Object.fromEntries(Object.entries(BODY).map(([k, s]) => [k, ajv.compile(s)]));
|
|
68
|
+
const RETAINED = [topics_1.MESSAGE_KIND.PRESENCE, topics_1.MESSAGE_KIND.SNAPSHOT, topics_1.MESSAGE_KIND.ALARM];
|
|
69
|
+
const errorsOf = (prefix, fn) => (fn.errors ?? []).map((e) => `${prefix}: ${e.instancePath || '.'} ${e.message}`);
|
|
70
|
+
/** Validate one message. `retained` is the publisher's flag when the transport reports it. */
|
|
71
|
+
function inspectMessage(root, topic, raw, retained) {
|
|
72
|
+
const parsed = (0, topics_1.parseTopic)(root, topic);
|
|
73
|
+
if (!parsed)
|
|
74
|
+
return { parsed, body: null, violations: ['unknown topic'] };
|
|
75
|
+
const violations = [];
|
|
76
|
+
const size = typeof raw === 'string' ? Buffer.byteLength(raw) : raw.length;
|
|
77
|
+
if (size > constants_1.MAX_PAYLOAD_BYTES)
|
|
78
|
+
violations.push(`payload ${size} B exceeds ${constants_1.MAX_PAYLOAD_BYTES} B`);
|
|
79
|
+
if (size === 0) {
|
|
80
|
+
if (parsed.kind !== topics_1.MESSAGE_KIND.ALARM)
|
|
81
|
+
violations.push('empty payload');
|
|
82
|
+
return { parsed, body: null, violations };
|
|
83
|
+
}
|
|
84
|
+
const body = (0, envelope_1.parsePayload)(raw);
|
|
85
|
+
if (!body)
|
|
86
|
+
return { parsed, body: null, violations: [...violations, 'not a JSON object'] };
|
|
87
|
+
if (!ENVELOPE_V(body))
|
|
88
|
+
violations.push(...errorsOf('envelope', ENVELOPE_V));
|
|
89
|
+
const bodyV = BODY_V[parsed.kind];
|
|
90
|
+
if (!bodyV(body.body ?? {}))
|
|
91
|
+
violations.push(...errorsOf(parsed.kind, bodyV));
|
|
92
|
+
violations.push(...crossFieldRules(parsed, body, retained));
|
|
93
|
+
return { parsed, body, violations };
|
|
94
|
+
}
|
|
95
|
+
function crossFieldRules(parsed, env, retained) {
|
|
96
|
+
const out = [];
|
|
97
|
+
const b = env.body ?? {};
|
|
98
|
+
const { ident, kind, key } = parsed;
|
|
99
|
+
const s = env.source ?? {};
|
|
100
|
+
if (s.site !== ident.siteId || s.line !== ident.lineId || s.machine !== ident.machineId)
|
|
101
|
+
out.push('source disagrees with the topic');
|
|
102
|
+
if (kind === topics_1.MESSAGE_KIND.ALARM && b.alarmId !== key)
|
|
103
|
+
out.push('alarmId disagrees with the topic');
|
|
104
|
+
if (kind === topics_1.MESSAGE_KIND.JOB) {
|
|
105
|
+
if (b.event === constants_1.JOB_EVENT.FINISHED) {
|
|
106
|
+
for (const field of ['result', 'measurements', 'materials', 'startedAt', 'endedAt']) {
|
|
107
|
+
if (!(field in b))
|
|
108
|
+
out.push(`finished without ${field}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (b.result && b.result !== constants_1.JOB_RESULT.OK && !b.reason)
|
|
112
|
+
out.push(`${b.result} without a reason`);
|
|
113
|
+
if (b.event === constants_1.JOB_EVENT.TRANSFER_FAILED && !b.failure)
|
|
114
|
+
out.push('transfer-failed without a failure');
|
|
115
|
+
// The schema has already reported a malformed list; these rules read only well-formed entries.
|
|
116
|
+
for (const m of Array.isArray(b.measurements) ? b.measurements : []) {
|
|
117
|
+
if (!m || typeof m !== 'object')
|
|
118
|
+
continue;
|
|
119
|
+
if (m.min != null && m.max != null && (m.min <= m.value && m.value <= m.max) !== m.ok)
|
|
120
|
+
out.push(`${m.name}: ok disagrees with min/max`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (retained === undefined)
|
|
124
|
+
return out;
|
|
125
|
+
if ((kind === topics_1.MESSAGE_KIND.PRESENCE || kind === topics_1.MESSAGE_KIND.SNAPSHOT) && !retained)
|
|
126
|
+
out.push(`${kind} not retained`);
|
|
127
|
+
if (kind === topics_1.MESSAGE_KIND.ALARM && b.active === true && !retained)
|
|
128
|
+
out.push('active alarm not retained');
|
|
129
|
+
if (!RETAINED.includes(kind) && retained)
|
|
130
|
+
out.push(`${kind} must not be retained`);
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Envelope, MachineIdent } from '../../../types/machines';
|
|
2
|
+
export type { Envelope, MachineIdent };
|
|
3
|
+
/** The machine's wire address: its topic level and client id. */
|
|
4
|
+
export declare function addressOf(ident: MachineIdent): string;
|
|
5
|
+
/** {site}-{line}-{address}, stable across reconnects. */
|
|
6
|
+
export declare function clientIdOf(ident: MachineIdent): string;
|
|
7
|
+
/** ISO 8601 UTC with milliseconds. */
|
|
8
|
+
export declare function isoTs(at?: Date | number): string;
|
|
9
|
+
export declare function buildEnvelope<T>(ident: MachineIdent, body: T, opts?: {
|
|
10
|
+
at?: string;
|
|
11
|
+
id?: string;
|
|
12
|
+
}): Envelope<T>;
|
|
13
|
+
/** Parse a JSON payload; null when it is empty or not an object. */
|
|
14
|
+
export declare function parsePayload(raw: string | Buffer): Envelope | null;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.addressOf = addressOf;
|
|
4
|
+
exports.clientIdOf = clientIdOf;
|
|
5
|
+
exports.isoTs = isoTs;
|
|
6
|
+
exports.buildEnvelope = buildEnvelope;
|
|
7
|
+
exports.parsePayload = parsePayload;
|
|
8
|
+
const crypto_1 = require("crypto");
|
|
9
|
+
const constants_1 = require("./constants");
|
|
10
|
+
/** The machine's wire address: its topic level and client id. */
|
|
11
|
+
function addressOf(ident) {
|
|
12
|
+
return ident.address ?? ident.machineId;
|
|
13
|
+
}
|
|
14
|
+
/** {site}-{line}-{address}, stable across reconnects. */
|
|
15
|
+
function clientIdOf(ident) {
|
|
16
|
+
return `${ident.siteId}-${ident.lineId}-${addressOf(ident)}`;
|
|
17
|
+
}
|
|
18
|
+
/** ISO 8601 UTC with milliseconds. */
|
|
19
|
+
function isoTs(at = Date.now()) {
|
|
20
|
+
return new Date(at).toISOString();
|
|
21
|
+
}
|
|
22
|
+
function buildEnvelope(ident, body, opts = {}) {
|
|
23
|
+
return {
|
|
24
|
+
v: constants_1.PROTOCOL_VERSION,
|
|
25
|
+
id: opts.id ?? (0, crypto_1.randomUUID)(),
|
|
26
|
+
at: opts.at ?? isoTs(),
|
|
27
|
+
source: { site: ident.siteId, line: ident.lineId, machine: addressOf(ident), ...(ident.address && ident.address !== ident.machineId ? { station: ident.machineId } : {}) },
|
|
28
|
+
body,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
/** Parse a JSON payload; null when it is empty or not an object. */
|
|
32
|
+
function parsePayload(raw) {
|
|
33
|
+
const text = typeof raw === 'string' ? raw : raw.toString('utf8');
|
|
34
|
+
if (!text)
|
|
35
|
+
return null;
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(text);
|
|
38
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./constants"), exports);
|
|
18
|
+
__exportStar(require("./envelope"), exports);
|
|
19
|
+
__exportStar(require("./topics"), exports);
|
|
20
|
+
__exportStar(require("./contract"), exports);
|
|
21
|
+
__exportStar(require("./codec"), exports);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Topic builders and the parser that classifies an inbound topic. NATS
|
|
3
|
+
* carries an MQTT topic on the subject with `/` replaced by `.`, so no topic
|
|
4
|
+
* level may contain `.`, `/`, `+`, `#` or whitespace.
|
|
5
|
+
*/
|
|
6
|
+
import { type MachineIdent } from './envelope';
|
|
7
|
+
export declare const MESSAGE_KIND: {
|
|
8
|
+
readonly PRESENCE: "presence";
|
|
9
|
+
readonly SNAPSHOT: "snapshot";
|
|
10
|
+
readonly JOB: "job";
|
|
11
|
+
readonly ALARM: "alarm";
|
|
12
|
+
readonly TELEMETRY: "telemetry";
|
|
13
|
+
readonly CMD: "cmd";
|
|
14
|
+
readonly REPLY: "reply";
|
|
15
|
+
readonly REPORT: "report";
|
|
16
|
+
};
|
|
17
|
+
export type MessageKind = typeof MESSAGE_KIND[keyof typeof MESSAGE_KIND];
|
|
18
|
+
export interface ParsedTopic {
|
|
19
|
+
ident: MachineIdent;
|
|
20
|
+
kind: MessageKind;
|
|
21
|
+
/** The alarm id on alarm topics; the report name on report topics. */
|
|
22
|
+
key?: string;
|
|
23
|
+
}
|
|
24
|
+
export declare function isTopicLevel(value: unknown): value is string;
|
|
25
|
+
export declare function assertTopicLevel(name: string, value: string): void;
|
|
26
|
+
export declare function machineTopic(root: string, ident: MachineIdent, channel: string): string;
|
|
27
|
+
export declare function alarmTopic(root: string, ident: MachineIdent, alarmId: string): string;
|
|
28
|
+
/** Every topic on one line, for a controller subscription. */
|
|
29
|
+
export declare function lineFilter(root: string, siteId: string, lineId: string): string;
|
|
30
|
+
export declare function toSubject(topic: string): string;
|
|
31
|
+
export declare function toTopic(subject: string): string;
|
|
32
|
+
/** Classify a topic under `root`; null when it is not a machine topic. */
|
|
33
|
+
export declare function parseTopic(root: string, topic: string): ParsedTopic | null;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Topic builders and the parser that classifies an inbound topic. NATS
|
|
4
|
+
* carries an MQTT topic on the subject with `/` replaced by `.`, so no topic
|
|
5
|
+
* level may contain `.`, `/`, `+`, `#` or whitespace.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.MESSAGE_KIND = void 0;
|
|
9
|
+
exports.isTopicLevel = isTopicLevel;
|
|
10
|
+
exports.assertTopicLevel = assertTopicLevel;
|
|
11
|
+
exports.machineTopic = machineTopic;
|
|
12
|
+
exports.alarmTopic = alarmTopic;
|
|
13
|
+
exports.lineFilter = lineFilter;
|
|
14
|
+
exports.toSubject = toSubject;
|
|
15
|
+
exports.toTopic = toTopic;
|
|
16
|
+
exports.parseTopic = parseTopic;
|
|
17
|
+
const constants_1 = require("./constants");
|
|
18
|
+
const envelope_1 = require("./envelope");
|
|
19
|
+
exports.MESSAGE_KIND = {
|
|
20
|
+
PRESENCE: 'presence',
|
|
21
|
+
SNAPSHOT: 'snapshot',
|
|
22
|
+
JOB: 'job',
|
|
23
|
+
ALARM: 'alarm',
|
|
24
|
+
TELEMETRY: 'telemetry',
|
|
25
|
+
CMD: 'cmd',
|
|
26
|
+
REPLY: 'reply',
|
|
27
|
+
REPORT: 'report',
|
|
28
|
+
};
|
|
29
|
+
const RESERVED = /[/+#.\s]/;
|
|
30
|
+
function isTopicLevel(value) {
|
|
31
|
+
return typeof value === 'string' && !!value && !RESERVED.test(value);
|
|
32
|
+
}
|
|
33
|
+
function assertTopicLevel(name, value) {
|
|
34
|
+
if (!isTopicLevel(value)) {
|
|
35
|
+
throw new Error(`${name} "${value}" is not a valid topic level (no / + # . or whitespace)`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function machineTopic(root, ident, channel) {
|
|
39
|
+
return `${root}/${ident.siteId}/${ident.lineId}/${(0, envelope_1.addressOf)(ident)}/${channel}`;
|
|
40
|
+
}
|
|
41
|
+
function alarmTopic(root, ident, alarmId) {
|
|
42
|
+
return machineTopic(root, ident, `${constants_1.CHANNEL.ALARM}/${alarmId}`);
|
|
43
|
+
}
|
|
44
|
+
/** Every topic on one line, for a controller subscription. */
|
|
45
|
+
function lineFilter(root, siteId, lineId) {
|
|
46
|
+
return `${root}/${siteId}/${lineId}/#`;
|
|
47
|
+
}
|
|
48
|
+
function toSubject(topic) {
|
|
49
|
+
return topic.split('/').join('.');
|
|
50
|
+
}
|
|
51
|
+
function toTopic(subject) {
|
|
52
|
+
return subject.split('.').join('/');
|
|
53
|
+
}
|
|
54
|
+
const CHANNEL_KIND = {
|
|
55
|
+
[constants_1.CHANNEL.PRESENCE]: exports.MESSAGE_KIND.PRESENCE,
|
|
56
|
+
[constants_1.CHANNEL.SNAPSHOT]: exports.MESSAGE_KIND.SNAPSHOT,
|
|
57
|
+
[constants_1.CHANNEL.JOB]: exports.MESSAGE_KIND.JOB,
|
|
58
|
+
[constants_1.CHANNEL.TELEMETRY]: exports.MESSAGE_KIND.TELEMETRY,
|
|
59
|
+
[constants_1.CHANNEL.CMD]: exports.MESSAGE_KIND.CMD,
|
|
60
|
+
[constants_1.CHANNEL.REPLY]: exports.MESSAGE_KIND.REPLY,
|
|
61
|
+
};
|
|
62
|
+
/** Classify a topic under `root`; null when it is not a machine topic. */
|
|
63
|
+
function parseTopic(root, topic) {
|
|
64
|
+
const prefix = `${root}/`;
|
|
65
|
+
if (!topic.startsWith(prefix))
|
|
66
|
+
return null;
|
|
67
|
+
const [siteId, lineId, machineId, ...rest] = topic.slice(prefix.length).split('/');
|
|
68
|
+
if (!siteId || !lineId || !machineId || rest.length === 0)
|
|
69
|
+
return null;
|
|
70
|
+
const ident = { siteId, lineId, machineId };
|
|
71
|
+
const kind = CHANNEL_KIND[rest.join('/')];
|
|
72
|
+
if (kind)
|
|
73
|
+
return { ident, kind };
|
|
74
|
+
if (rest.length === 2 && rest[0] === constants_1.CHANNEL.ALARM)
|
|
75
|
+
return { ident, kind: exports.MESSAGE_KIND.ALARM, key: rest[1] };
|
|
76
|
+
if (rest.length >= 2 && rest[0] === constants_1.CHANNEL.REPORT)
|
|
77
|
+
return { ident, kind: exports.MESSAGE_KIND.REPORT, key: rest.slice(1).join('/') };
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The machine broker (Mosquitto), rendered from the line. Machines speak
|
|
3
|
+
* MQTT 5 to it over TLS, each with its own client certificate (the CN is
|
|
4
|
+
* its username). The ACL lets a machine publish only its own channels and
|
|
5
|
+
* read only its own commands, and lets the line controller command every
|
|
6
|
+
* machine on the line. A bridge carries the line's topics out to the site
|
|
7
|
+
* NATS, where JetStream keeps them.
|
|
8
|
+
*/
|
|
9
|
+
import { type LineCodec } from '../protocol/codec';
|
|
10
|
+
import { type LineDef } from '../line';
|
|
11
|
+
export interface MosquittoConfOptions {
|
|
12
|
+
root: string;
|
|
13
|
+
/** Certificate directory as the broker sees it. */
|
|
14
|
+
certDir: string;
|
|
15
|
+
/** Where the site NATS MQTT listener is, host:port. */
|
|
16
|
+
siteNats: string;
|
|
17
|
+
aclFile: string;
|
|
18
|
+
maxPacketBytes?: number;
|
|
19
|
+
}
|
|
20
|
+
export declare function renderMosquittoConf(line: LineDef, opts: MosquittoConfOptions): string;
|
|
21
|
+
/**
|
|
22
|
+
* The codec's channels decide the topics: a machine writes its events and telemetry and reads its commands.
|
|
23
|
+
* A station whose address the line learns has no user of its own: `pattern` rules give every certificate
|
|
24
|
+
* the channels under its own name (%u), so its certificate's common name must be its address. On a line
|
|
25
|
+
* that also declares addresses, a certificate named for a declared address is denied that station's channels.
|
|
26
|
+
*/
|
|
27
|
+
export declare function renderMosquittoAcl(line: LineDef, codec: Pick<LineCodec, 'root' | 'channels' | 'topicFor'>): string;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The machine broker (Mosquitto), rendered from the line. Machines speak
|
|
4
|
+
* MQTT 5 to it over TLS, each with its own client certificate (the CN is
|
|
5
|
+
* its username). The ACL lets a machine publish only its own channels and
|
|
6
|
+
* read only its own commands, and lets the line controller command every
|
|
7
|
+
* machine on the line. A bridge carries the line's topics out to the site
|
|
8
|
+
* NATS, where JetStream keeps them.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.renderMosquittoConf = renderMosquittoConf;
|
|
12
|
+
exports.renderMosquittoAcl = renderMosquittoAcl;
|
|
13
|
+
const envelope_1 = require("../protocol/envelope");
|
|
14
|
+
const codec_1 = require("../protocol/codec");
|
|
15
|
+
const line_1 = require("../line");
|
|
16
|
+
const site_conf_1 = require("./site-conf");
|
|
17
|
+
function renderMosquittoConf(line, opts) {
|
|
18
|
+
return `# Rendered from the line definition by site/setup.ts. Do not edit by hand.
|
|
19
|
+
persistence true
|
|
20
|
+
persistence_location /mosquitto/data/
|
|
21
|
+
log_dest stdout
|
|
22
|
+
log_type error
|
|
23
|
+
log_type warning
|
|
24
|
+
log_type notice
|
|
25
|
+
|
|
26
|
+
listener 8883
|
|
27
|
+
cafile ${opts.certDir}/ca.pem
|
|
28
|
+
certfile ${opts.certDir}/broker.pem
|
|
29
|
+
keyfile ${opts.certDir}/broker.key
|
|
30
|
+
require_certificate true
|
|
31
|
+
use_identity_as_username true
|
|
32
|
+
acl_file ${opts.aclFile}
|
|
33
|
+
max_packet_size ${opts.maxPacketBytes ?? 262_144}
|
|
34
|
+
|
|
35
|
+
connection ${line.siteId}-${line.lineId}-site-nats
|
|
36
|
+
address ${opts.siteNats}
|
|
37
|
+
bridge_protocol_version mqttv311
|
|
38
|
+
bridge_cafile ${opts.certDir}/ca.pem
|
|
39
|
+
bridge_certfile ${opts.certDir}/${(0, site_conf_1.bridgeId)(line)}.pem
|
|
40
|
+
bridge_keyfile ${opts.certDir}/${(0, site_conf_1.bridgeId)(line)}.key
|
|
41
|
+
remote_clientid ${(0, site_conf_1.bridgeId)(line)}
|
|
42
|
+
cleansession false
|
|
43
|
+
try_private false
|
|
44
|
+
notifications false
|
|
45
|
+
topic ${opts.root}/${line.siteId}/${line.lineId}/# out 1
|
|
46
|
+
`;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The codec's channels decide the topics: a machine writes its events and telemetry and reads its commands.
|
|
50
|
+
* A station whose address the line learns has no user of its own: `pattern` rules give every certificate
|
|
51
|
+
* the channels under its own name (%u), so its certificate's common name must be its address. On a line
|
|
52
|
+
* that also declares addresses, a certificate named for a declared address is denied that station's channels.
|
|
53
|
+
*/
|
|
54
|
+
function renderMosquittoAcl(line, codec) {
|
|
55
|
+
const { siteId, lineId } = line;
|
|
56
|
+
const { events, telemetry, commands } = codec.channels;
|
|
57
|
+
const own = (address, channels) => channels.map((c) => (0, codec_1.topicOf)(codec, { siteId, lineId, address }, c));
|
|
58
|
+
const rules = (prefix, address) => [
|
|
59
|
+
...own(address, [...events, ...telemetry]).map((t) => `${prefix}write ${t}`),
|
|
60
|
+
...own(address, commands).map((t) => `${prefix}read ${t}`),
|
|
61
|
+
];
|
|
62
|
+
const declared = line.machines.filter((m) => !(0, line_1.learnsAddress)(line, m.machineId)).map((m) => (0, line_1.identOf)(line, m.machineId));
|
|
63
|
+
const machines = declared.map((ident) => [`user ${(0, envelope_1.clientIdOf)(ident)}`, ...rules('topic ', (0, envelope_1.addressOf)(ident))].join('\n'));
|
|
64
|
+
const learns = line.machines.some((m) => (0, line_1.learnsAddress)(line, m.machineId));
|
|
65
|
+
const learned = learns ? [['# Stations that learn their address: each certificate, under its own common name.', ...rules('pattern ', '%u')].join('\n')] : [];
|
|
66
|
+
const every = [...events, ...telemetry, ...commands];
|
|
67
|
+
const shadows = learns ? declared.map((ident) => shadowRules(line, (0, envelope_1.addressOf)(ident), own((0, envelope_1.addressOf)(ident), every))) : [];
|
|
68
|
+
const controller = [
|
|
69
|
+
`user ${(0, line_1.controllerId)(line)}`,
|
|
70
|
+
...commands.map((c) => `topic write ${(0, codec_1.topicOf)(codec, { siteId, lineId }, c)}`),
|
|
71
|
+
`topic read ${codec.root}/${siteId}/${lineId}/#`,
|
|
72
|
+
].join('\n');
|
|
73
|
+
return `# Rendered from the line definition by site/setup.ts. Do not edit by hand.\n\n${[...learned, ...shadows, ...machines, controller].join('\n\n')}\n`;
|
|
74
|
+
}
|
|
75
|
+
/** Deny a declared station's channels to a certificate named for its address, which the patterns would grant. */
|
|
76
|
+
function shadowRules(line, address, topics) {
|
|
77
|
+
const users = [(0, line_1.controllerId)(line), ...line.machines.filter((m) => !(0, line_1.learnsAddress)(line, m.machineId)).map((m) => (0, envelope_1.clientIdOf)((0, line_1.identOf)(line, m.machineId)))];
|
|
78
|
+
if (users.includes(address))
|
|
79
|
+
throw new Error(`address "${address}" is also a broker user name; a declared address on a learning line must differ from every user`);
|
|
80
|
+
return [`user ${address}`, ...topics.map((t) => `topic deny ${t}`)].join('\n');
|
|
81
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The site's NATS server, rendered from the line. It runs JetStream and an
|
|
3
|
+
* MQTT listener that only the machine broker's bridge logs in to (by client
|
|
4
|
+
* certificate; the CN is its user, and it may publish only under the line's
|
|
5
|
+
* topics). The gateway logs in by password and may only read: the JetStream
|
|
6
|
+
* API, acks and its inbox, and the line's subjects. The site NATS stands
|
|
7
|
+
* alone; Long Tail's escalation events stay inside Long Tail.
|
|
8
|
+
*/
|
|
9
|
+
import type { LineDef } from '../line';
|
|
10
|
+
export interface SiteConfOptions {
|
|
11
|
+
root: string;
|
|
12
|
+
gatewayUser: string;
|
|
13
|
+
gatewayPassword: string;
|
|
14
|
+
certDir: string;
|
|
15
|
+
storeDir: string;
|
|
16
|
+
mqttPort?: number;
|
|
17
|
+
}
|
|
18
|
+
/** The machine broker's bridge identity for a line. */
|
|
19
|
+
export declare function bridgeId(line: LineDef): string;
|
|
20
|
+
export declare function renderSiteConf(line: LineDef, opts: SiteConfOptions): string;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The site's NATS server, rendered from the line. It runs JetStream and an
|
|
4
|
+
* MQTT listener that only the machine broker's bridge logs in to (by client
|
|
5
|
+
* certificate; the CN is its user, and it may publish only under the line's
|
|
6
|
+
* topics). The gateway logs in by password and may only read: the JetStream
|
|
7
|
+
* API, acks and its inbox, and the line's subjects. The site NATS stands
|
|
8
|
+
* alone; Long Tail's escalation events stay inside Long Tail.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.bridgeId = bridgeId;
|
|
12
|
+
exports.renderSiteConf = renderSiteConf;
|
|
13
|
+
const protocol_1 = require("../protocol");
|
|
14
|
+
const q = JSON.stringify;
|
|
15
|
+
/** The machine broker's bridge identity for a line. */
|
|
16
|
+
function bridgeId(line) {
|
|
17
|
+
return `${line.siteId}-${line.lineId}-bridge`;
|
|
18
|
+
}
|
|
19
|
+
/** What the gateway may publish: JetStream requests, acks and flow control, never a machine subject. */
|
|
20
|
+
const GATEWAY_PUBLISH = ['$JS.API.>', '$JS.ACK.>', '$JS.FC.>'];
|
|
21
|
+
function renderSiteConf(line, opts) {
|
|
22
|
+
const base = (0, protocol_1.toSubject)(`${opts.root}/${line.siteId}/${line.lineId}`);
|
|
23
|
+
const bridge = [
|
|
24
|
+
` { user: ${q(`CN=${bridgeId(line)}`)}, allowed_connection_types: ["MQTT"],`,
|
|
25
|
+
` permissions: { publish: [${q(`${base}.>`)}], subscribe: { deny: [">"] } } }`,
|
|
26
|
+
].join('\n');
|
|
27
|
+
const gateway = [
|
|
28
|
+
` { user: ${q(opts.gatewayUser)}, password: ${q(opts.gatewayPassword)}, allowed_connection_types: ["STANDARD"],`,
|
|
29
|
+
` permissions: { publish: ${q(GATEWAY_PUBLISH)}, subscribe: ${q(['_INBOX.>', `${base}.>`])} } }`,
|
|
30
|
+
].join('\n');
|
|
31
|
+
return `# Rendered from the line definition by site/setup.ts. Do not edit by hand.
|
|
32
|
+
server_name: ${q(`${line.siteId}-${line.lineId}`)}
|
|
33
|
+
port: 4222
|
|
34
|
+
http: 8222
|
|
35
|
+
jetstream { store_dir: ${q(opts.storeDir)} }
|
|
36
|
+
|
|
37
|
+
authorization {
|
|
38
|
+
users: [
|
|
39
|
+
${bridge}
|
|
40
|
+
${gateway}
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
mqtt {
|
|
45
|
+
port: ${opts.mqttPort ?? 8883}
|
|
46
|
+
tls {
|
|
47
|
+
cert_file: ${q(`${opts.certDir}/server.pem`)}
|
|
48
|
+
key_file: ${q(`${opts.certDir}/server.key`)}
|
|
49
|
+
ca_file: ${q(`${opts.certDir}/ca.pem`)}
|
|
50
|
+
verify_and_map: true
|
|
51
|
+
timeout: 5
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
`;
|
|
55
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escalation events from Long Tail's in-process event adapter. With a
|
|
3
|
+
* cross-container bus configured, the adapter also receives events other
|
|
4
|
+
* containers publish. Nothing is replayed: the gateway reconciles open rows
|
|
5
|
+
* when it starts leading a line.
|
|
6
|
+
*/
|
|
7
|
+
import type { CallbackEventAdapter } from '../../events/callback';
|
|
8
|
+
import { type EscalationEvent, type EscalationFeed } from './delivery';
|
|
9
|
+
import type { MachineDisposition } from '../../../types/machines';
|
|
10
|
+
export declare class CallbackEscalationFeed implements EscalationFeed {
|
|
11
|
+
private readonly adapter;
|
|
12
|
+
private off?;
|
|
13
|
+
private readonly redelivery;
|
|
14
|
+
constructor(adapter: CallbackEventAdapter, nakDelayMs?: number);
|
|
15
|
+
connect(): Promise<void>;
|
|
16
|
+
consume(handler: (event: EscalationEvent) => Promise<MachineDisposition>): Promise<void>;
|
|
17
|
+
close(): Promise<void>;
|
|
18
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Escalation events from Long Tail's in-process event adapter. With a
|
|
4
|
+
* cross-container bus configured, the adapter also receives events other
|
|
5
|
+
* containers publish. Nothing is replayed: the gateway reconciles open rows
|
|
6
|
+
* when it starts leading a line.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.CallbackEscalationFeed = void 0;
|
|
10
|
+
const delivery_1 = require("./delivery");
|
|
11
|
+
const serial_1 = require("./serial");
|
|
12
|
+
const ESCALATION_EVENTS = 'system.escalation.>';
|
|
13
|
+
class CallbackEscalationFeed {
|
|
14
|
+
constructor(adapter, nakDelayMs = 1_000) {
|
|
15
|
+
this.adapter = adapter;
|
|
16
|
+
this.redelivery = new serial_1.LocalRedelivery(new serial_1.SerialQueue(), nakDelayMs);
|
|
17
|
+
}
|
|
18
|
+
async connect() { }
|
|
19
|
+
async consume(handler) {
|
|
20
|
+
this.off = this.adapter.on(ESCALATION_EVENTS, (raw) => {
|
|
21
|
+
const event = (0, delivery_1.toEscalationEvent)(raw);
|
|
22
|
+
if (event)
|
|
23
|
+
void this.redelivery.deliver(() => handler(event));
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
async close() {
|
|
27
|
+
this.off?.();
|
|
28
|
+
await this.redelivery.stop();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
exports.CallbackEscalationFeed = CallbackEscalationFeed;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Two sides of the machine fabric. A machine holds a MachineLink: one MQTT
|
|
3
|
+
* client with a will, publishing under its own subtree and hearing only its
|
|
4
|
+
* own commands. The gateway holds a MachineBus: durable, ordered delivery of
|
|
5
|
+
* every machine message on the line, QoS 1 command egress, and retained reads.
|
|
6
|
+
*/
|
|
7
|
+
import type { EscalationEvent, EscalationFeed, InboundMessage, LinkConnectOptions, LinkWill, MachineBus, MachineLink, PublishOptions } from '../../../types/machines';
|
|
8
|
+
export type { EscalationEvent, EscalationFeed, InboundMessage, LinkConnectOptions, LinkWill, MachineBus, MachineLink, PublishOptions };
|
|
9
|
+
export declare const DISPOSITION: {
|
|
10
|
+
/** Handled; do not deliver again. */
|
|
11
|
+
readonly ACK: "ack";
|
|
12
|
+
/** Not yet actionable (its row does not exist yet); deliver again later. */
|
|
13
|
+
readonly NAK: "nak";
|
|
14
|
+
/** Never actionable; drop it. */
|
|
15
|
+
readonly TERM: "term";
|
|
16
|
+
};
|
|
17
|
+
export type Disposition = typeof DISPOSITION[keyof typeof DISPOSITION];
|
|
18
|
+
/** MQTT filter match with `+` (one level) and `#` (the rest). */
|
|
19
|
+
export declare function topicMatches(filter: string, topic: string): boolean;
|
|
20
|
+
/** Read an LTEvent body into an EscalationEvent; null for other families. */
|
|
21
|
+
export declare function toEscalationEvent(body: Record<string, any>): EscalationEvent | null;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Two sides of the machine fabric. A machine holds a MachineLink: one MQTT
|
|
4
|
+
* client with a will, publishing under its own subtree and hearing only its
|
|
5
|
+
* own commands. The gateway holds a MachineBus: durable, ordered delivery of
|
|
6
|
+
* every machine message on the line, QoS 1 command egress, and retained reads.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.DISPOSITION = void 0;
|
|
10
|
+
exports.topicMatches = topicMatches;
|
|
11
|
+
exports.toEscalationEvent = toEscalationEvent;
|
|
12
|
+
exports.DISPOSITION = {
|
|
13
|
+
/** Handled; do not deliver again. */
|
|
14
|
+
ACK: 'ack',
|
|
15
|
+
/** Not yet actionable (its row does not exist yet); deliver again later. */
|
|
16
|
+
NAK: 'nak',
|
|
17
|
+
/** Never actionable; drop it. */
|
|
18
|
+
TERM: 'term',
|
|
19
|
+
};
|
|
20
|
+
/** MQTT filter match with `+` (one level) and `#` (the rest). */
|
|
21
|
+
function topicMatches(filter, topic) {
|
|
22
|
+
const f = filter.split('/');
|
|
23
|
+
const t = topic.split('/');
|
|
24
|
+
for (let i = 0; i < f.length; i++) {
|
|
25
|
+
if (f[i] === '#')
|
|
26
|
+
return true;
|
|
27
|
+
if (i >= t.length)
|
|
28
|
+
return false;
|
|
29
|
+
if (f[i] !== '+' && f[i] !== t[i])
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
return f.length === t.length;
|
|
33
|
+
}
|
|
34
|
+
const ESCALATION_SUBJECT = /^(?:.*\.)?system\.escalation\.([^.]+)\.([^.]+)\.([^.]+)$/;
|
|
35
|
+
/** Read an LTEvent body into an EscalationEvent; null for other families. */
|
|
36
|
+
function toEscalationEvent(body) {
|
|
37
|
+
const match = typeof body?.type === 'string' ? ESCALATION_SUBJECT.exec(body.type) : null;
|
|
38
|
+
if (!match)
|
|
39
|
+
return null;
|
|
40
|
+
const [, role, escalationId, verb] = match;
|
|
41
|
+
return { type: body.type, escalationId: body.escalationId ?? escalationId, role: body.role ?? role, verb, data: body.data ?? {} };
|
|
42
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from './delivery';
|
|
2
|
+
export * from './memory-broker';
|
|
3
|
+
export * from './memory';
|
|
4
|
+
export * from './serial';
|
|
5
|
+
export * from './streams';
|
|
6
|
+
export * from './mqtt-session';
|
|
7
|
+
export * from './mqtt-link';
|
|
8
|
+
export * from './mqtt-bus';
|
|
9
|
+
export * from './nats-bus';
|
|
10
|
+
export * from './nats-feed';
|
|
11
|
+
export * from './callback-feed';
|