@hotmeshio/long-tail 0.25.4 → 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/scripts/machine-line.d.ts +33 -0
- package/build/scripts/machine-line.js +301 -0
- package/build/services/machines/commands.d.ts +29 -0
- package/build/services/machines/commands.js +37 -0
- package/build/services/machines/config.d.ts +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/package.json +11 -1
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The gateway on the site NATS. Machine events come from the line's durable
|
|
4
|
+
* consumer on LINE_EVENTS (replayed after a restart); telemetry is read live,
|
|
5
|
+
* since it only proves a machine is alive. Commands leave through an MQTT
|
|
6
|
+
* session at QoS 1: a NATS publish reaches MQTT subscribers at QoS 0 and
|
|
7
|
+
* would not wait for a machine that is away.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.NatsMachineBus = void 0;
|
|
11
|
+
const nats_1 = require("nats");
|
|
12
|
+
const protocol_1 = require("../protocol");
|
|
13
|
+
const mqtt_session_1 = require("./mqtt-session");
|
|
14
|
+
const nats_durable_1 = require("./nats-durable");
|
|
15
|
+
const serial_1 = require("./serial");
|
|
16
|
+
const streams_1 = require("./streams");
|
|
17
|
+
class NatsMachineBus {
|
|
18
|
+
constructor(opts) {
|
|
19
|
+
this.opts = opts;
|
|
20
|
+
this.telemetry = [];
|
|
21
|
+
this.queue = new serial_1.SerialQueue();
|
|
22
|
+
}
|
|
23
|
+
get durableName() {
|
|
24
|
+
return `${this.opts.controllerId}-events`;
|
|
25
|
+
}
|
|
26
|
+
async connect() {
|
|
27
|
+
const { natsUrl, user, password, controllerId, siteId, lineId, codec } = this.opts;
|
|
28
|
+
this.nc = await (0, nats_1.connect)({ servers: natsUrl, user, pass: password, name: controllerId });
|
|
29
|
+
const jsm = await this.nc.jetstreamManager();
|
|
30
|
+
await (0, streams_1.ensureStreams)(jsm, (0, streams_1.lineStreamConfigs)(codec));
|
|
31
|
+
await (0, streams_1.ensureConsumer)(jsm, streams_1.LINE_STREAM.EVENTS, (0, streams_1.lineEventConsumer)(codec, siteId, lineId, this.durableName));
|
|
32
|
+
this.session = await mqtt_session_1.MqttSession.open(this.opts.mqtt, controllerId);
|
|
33
|
+
}
|
|
34
|
+
async consume(handler) {
|
|
35
|
+
const nc = this.nats();
|
|
36
|
+
const { codec, siteId, lineId, nakDelayMs = 1_000 } = this.opts;
|
|
37
|
+
this.messages = await (0, nats_durable_1.readDurable)(nc.jetstream(), streams_1.LINE_STREAM.EVENTS, this.durableName, this.queue, (m) => handler({ topic: (0, protocol_1.toTopic)(m.subject), payload: (0, nats_durable_1.payloadOf)(m), deliveries: m.info.deliveryCount }), nakDelayMs);
|
|
38
|
+
this.telemetry = (0, streams_1.telemetrySubjects)(codec, siteId, lineId).map((subject) => nc.subscribe(subject, {
|
|
39
|
+
callback: (_err, m) => void this.queue.run(() => handler({ topic: (0, protocol_1.toTopic)(m.subject), payload: (0, nats_durable_1.payloadOf)(m), deliveries: 1 })),
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
42
|
+
async publish(topic, payload, opts = {}) {
|
|
43
|
+
await this.mqtt().publish(topic, payload, 1, opts.retain ?? false);
|
|
44
|
+
}
|
|
45
|
+
retained(filter) {
|
|
46
|
+
return this.mqtt().retained(filter);
|
|
47
|
+
}
|
|
48
|
+
async close() {
|
|
49
|
+
for (const sub of this.telemetry)
|
|
50
|
+
sub.unsubscribe();
|
|
51
|
+
await this.messages?.close();
|
|
52
|
+
await this.queue.drained();
|
|
53
|
+
await this.session?.close(true);
|
|
54
|
+
await this.nc?.drain();
|
|
55
|
+
}
|
|
56
|
+
nats() {
|
|
57
|
+
if (!this.nc)
|
|
58
|
+
throw new Error('NATS bus used before connect()');
|
|
59
|
+
return this.nc;
|
|
60
|
+
}
|
|
61
|
+
mqtt() {
|
|
62
|
+
if (!this.session)
|
|
63
|
+
throw new Error('NATS bus used before connect()');
|
|
64
|
+
return this.session;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
exports.NatsMachineBus = NatsMachineBus;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read a durable JetStream consumer one message at a time and settle each
|
|
3
|
+
* with the handler's disposition: ack, nak with a delay, or term.
|
|
4
|
+
*/
|
|
5
|
+
import type { ConsumerMessages, JetStreamClient, JsMsg } from 'nats';
|
|
6
|
+
import type { SerialQueue } from './serial';
|
|
7
|
+
import { type Disposition } from './delivery';
|
|
8
|
+
export declare function readDurable(js: JetStreamClient, stream: string, durable: string, queue: SerialQueue, handle: (msg: JsMsg) => Promise<Disposition>, nakDelayMs: number): Promise<ConsumerMessages>;
|
|
9
|
+
export declare function payloadOf(m: {
|
|
10
|
+
data: Uint8Array;
|
|
11
|
+
}): string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Read a durable JetStream consumer one message at a time and settle each
|
|
4
|
+
* with the handler's disposition: ack, nak with a delay, or term.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.readDurable = readDurable;
|
|
8
|
+
exports.payloadOf = payloadOf;
|
|
9
|
+
const delivery_1 = require("./delivery");
|
|
10
|
+
async function readDurable(js, stream, durable, queue, handle, nakDelayMs) {
|
|
11
|
+
const consumer = await js.consumers.get(stream, durable);
|
|
12
|
+
const messages = await consumer.consume();
|
|
13
|
+
void (async () => {
|
|
14
|
+
for await (const m of messages) {
|
|
15
|
+
const disposition = await queue.run(() => handle(m)).catch(() => delivery_1.DISPOSITION.NAK);
|
|
16
|
+
if (disposition === delivery_1.DISPOSITION.ACK)
|
|
17
|
+
m.ack();
|
|
18
|
+
else if (disposition === delivery_1.DISPOSITION.NAK)
|
|
19
|
+
m.nak(nakDelayMs);
|
|
20
|
+
else
|
|
21
|
+
m.term();
|
|
22
|
+
}
|
|
23
|
+
})();
|
|
24
|
+
return messages;
|
|
25
|
+
}
|
|
26
|
+
function payloadOf(m) {
|
|
27
|
+
return Buffer.from(m.data).toString('utf8');
|
|
28
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Long Tail's escalation events for a gateway running outside Long Tail: a
|
|
3
|
+
* subscription to Long Tail's NATS. Nothing is replayed; a not-yet-actionable
|
|
4
|
+
* event is retried in this process, and the gateway reconciles open rows
|
|
5
|
+
* when it starts leading a line.
|
|
6
|
+
*/
|
|
7
|
+
import { type Disposition, type EscalationEvent, type EscalationFeed } from './delivery';
|
|
8
|
+
type Handler = (event: EscalationEvent) => Promise<Disposition>;
|
|
9
|
+
/** Long Tail publishes every LTEvent as `lt.events.<type>`. */
|
|
10
|
+
export declare const LT_ESCALATION_SUBJECTS = "lt.events.system.escalation.>";
|
|
11
|
+
export interface CoreFeedOptions {
|
|
12
|
+
natsUrl: string;
|
|
13
|
+
token?: string;
|
|
14
|
+
nakDelayMs?: number;
|
|
15
|
+
}
|
|
16
|
+
export declare class CoreEscalationFeed implements EscalationFeed {
|
|
17
|
+
private readonly opts;
|
|
18
|
+
private nc?;
|
|
19
|
+
private sub?;
|
|
20
|
+
private readonly redelivery;
|
|
21
|
+
constructor(opts: CoreFeedOptions);
|
|
22
|
+
connect(): Promise<void>;
|
|
23
|
+
consume(handler: Handler): Promise<void>;
|
|
24
|
+
close(): Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Long Tail's escalation events for a gateway running outside Long Tail: a
|
|
4
|
+
* subscription to Long Tail's NATS. Nothing is replayed; a not-yet-actionable
|
|
5
|
+
* event is retried in this process, and the gateway reconciles open rows
|
|
6
|
+
* when it starts leading a line.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.CoreEscalationFeed = exports.LT_ESCALATION_SUBJECTS = void 0;
|
|
10
|
+
const nats_1 = require("nats");
|
|
11
|
+
const nats_durable_1 = require("./nats-durable");
|
|
12
|
+
const serial_1 = require("./serial");
|
|
13
|
+
const delivery_1 = require("./delivery");
|
|
14
|
+
/** Long Tail publishes every LTEvent as `lt.events.<type>`. */
|
|
15
|
+
exports.LT_ESCALATION_SUBJECTS = 'lt.events.system.escalation.>';
|
|
16
|
+
function decode(payload) {
|
|
17
|
+
try {
|
|
18
|
+
return (0, delivery_1.toEscalationEvent)(JSON.parse(payload));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
class CoreEscalationFeed {
|
|
25
|
+
constructor(opts) {
|
|
26
|
+
this.opts = opts;
|
|
27
|
+
this.redelivery = new serial_1.LocalRedelivery(new serial_1.SerialQueue(), opts.nakDelayMs ?? 1_000);
|
|
28
|
+
}
|
|
29
|
+
async connect() {
|
|
30
|
+
this.nc = await (0, nats_1.connect)({ servers: this.opts.natsUrl, token: this.opts.token, name: 'line-gateway-escalations' });
|
|
31
|
+
}
|
|
32
|
+
async consume(handler) {
|
|
33
|
+
if (!this.nc)
|
|
34
|
+
throw new Error('escalation feed used before connect()');
|
|
35
|
+
this.sub = this.nc.subscribe(exports.LT_ESCALATION_SUBJECTS, {
|
|
36
|
+
callback: (_err, m) => {
|
|
37
|
+
const event = decode((0, nats_durable_1.payloadOf)(m));
|
|
38
|
+
if (event)
|
|
39
|
+
void this.redelivery.deliver(() => handler(event));
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
async close() {
|
|
44
|
+
this.sub?.unsubscribe();
|
|
45
|
+
await this.redelivery.stop();
|
|
46
|
+
await this.nc?.drain();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
exports.CoreEscalationFeed = CoreEscalationFeed;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type Disposition } from './delivery';
|
|
2
|
+
/** Runs async work one item at a time, in arrival order. */
|
|
3
|
+
export declare class SerialQueue {
|
|
4
|
+
private tail;
|
|
5
|
+
run<T>(work: () => Promise<T>): Promise<T>;
|
|
6
|
+
/** Resolves once everything queued so far has run. */
|
|
7
|
+
drained(): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Delivery through a SerialQueue that tries a not-yet-actionable item again
|
|
11
|
+
* after a delay, for transports without a broker-side redelivery.
|
|
12
|
+
*/
|
|
13
|
+
export declare class LocalRedelivery {
|
|
14
|
+
readonly queue: SerialQueue;
|
|
15
|
+
private readonly delayMs;
|
|
16
|
+
private readonly timers;
|
|
17
|
+
private stopped;
|
|
18
|
+
constructor(queue: SerialQueue, delayMs: number);
|
|
19
|
+
deliver(attempt: (deliveries: number) => Promise<Disposition>, deliveries?: number): Promise<void>;
|
|
20
|
+
stop(): Promise<void>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LocalRedelivery = exports.SerialQueue = void 0;
|
|
4
|
+
const delivery_1 = require("./delivery");
|
|
5
|
+
/** Runs async work one item at a time, in arrival order. */
|
|
6
|
+
class SerialQueue {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.tail = Promise.resolve();
|
|
9
|
+
}
|
|
10
|
+
run(work) {
|
|
11
|
+
const next = this.tail.then(work, work);
|
|
12
|
+
this.tail = next.catch(() => undefined);
|
|
13
|
+
return next;
|
|
14
|
+
}
|
|
15
|
+
/** Resolves once everything queued so far has run. */
|
|
16
|
+
drained() {
|
|
17
|
+
return this.tail.then(() => undefined);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
exports.SerialQueue = SerialQueue;
|
|
21
|
+
/**
|
|
22
|
+
* Delivery through a SerialQueue that tries a not-yet-actionable item again
|
|
23
|
+
* after a delay, for transports without a broker-side redelivery.
|
|
24
|
+
*/
|
|
25
|
+
class LocalRedelivery {
|
|
26
|
+
constructor(queue, delayMs) {
|
|
27
|
+
this.queue = queue;
|
|
28
|
+
this.delayMs = delayMs;
|
|
29
|
+
this.timers = new Set();
|
|
30
|
+
this.stopped = false;
|
|
31
|
+
}
|
|
32
|
+
deliver(attempt, deliveries = 1) {
|
|
33
|
+
return this.queue.run(async () => {
|
|
34
|
+
if (this.stopped)
|
|
35
|
+
return;
|
|
36
|
+
const disposition = await attempt(deliveries).catch(() => delivery_1.DISPOSITION.NAK);
|
|
37
|
+
if (disposition !== delivery_1.DISPOSITION.NAK)
|
|
38
|
+
return;
|
|
39
|
+
const timer = setTimeout(() => {
|
|
40
|
+
this.timers.delete(timer);
|
|
41
|
+
void this.deliver(attempt, deliveries + 1);
|
|
42
|
+
}, this.delayMs);
|
|
43
|
+
this.timers.add(timer);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async stop() {
|
|
47
|
+
this.stopped = true;
|
|
48
|
+
for (const t of this.timers)
|
|
49
|
+
clearTimeout(t);
|
|
50
|
+
this.timers.clear();
|
|
51
|
+
await this.queue.drained();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
exports.LocalRedelivery = LocalRedelivery;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The site's durable log. Machine events and telemetry each land in a
|
|
3
|
+
* JetStream stream; the gateway reads events through a durable consumer, so
|
|
4
|
+
* a restart or a link outage replays instead of losing.
|
|
5
|
+
*/
|
|
6
|
+
import { type ConsumerConfig, type JetStreamManager, type StreamConfig } from 'nats';
|
|
7
|
+
import { type LineCodec } from '../protocol/codec';
|
|
8
|
+
export declare const LINE_STREAM: {
|
|
9
|
+
readonly EVENTS: "LINE_EVENTS";
|
|
10
|
+
readonly TELEMETRY: "LINE_TELEMETRY";
|
|
11
|
+
};
|
|
12
|
+
/** What the streams need from a codec: its root, channels and topic layout. */
|
|
13
|
+
export type StreamCodec = Pick<LineCodec, 'root' | 'channels' | 'topicFor'>;
|
|
14
|
+
/** An MQTT filter as a NATS subject: levels become tokens, `+` becomes `*`, `#` becomes `>`. */
|
|
15
|
+
export declare function filterToSubject(filter: string): string;
|
|
16
|
+
/** The site's streams, for every line under the codec's root; a root alone means the generic layout. */
|
|
17
|
+
export declare function lineStreamConfigs(codec: StreamCodec | string): Partial<StreamConfig>[];
|
|
18
|
+
/** The telemetry subjects for one line, read live rather than replayed. */
|
|
19
|
+
export declare function telemetrySubjects(codec: StreamCodec | string, siteId: string, lineId: string): string[];
|
|
20
|
+
/**
|
|
21
|
+
* A durable consumer. It starts at new messages the first time (open rows
|
|
22
|
+
* are reconciled at boot); after that its position survives restarts.
|
|
23
|
+
*/
|
|
24
|
+
export declare function durableConsumer(name: string, filterSubjects: string[]): Partial<ConsumerConfig>;
|
|
25
|
+
export declare function lineEventConsumer(codec: StreamCodec | string, siteId: string, lineId: string, name: string): Partial<ConsumerConfig>;
|
|
26
|
+
/** Create or update each stream, then the consumer. Safe on every boot. */
|
|
27
|
+
export declare function ensureStreams(jsm: JetStreamManager, streams: Partial<StreamConfig>[]): Promise<void>;
|
|
28
|
+
export declare function ensureConsumer(jsm: JetStreamManager, stream: string, config: Partial<ConsumerConfig>): Promise<void>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The site's durable log. Machine events and telemetry each land in a
|
|
4
|
+
* JetStream stream; the gateway reads events through a durable consumer, so
|
|
5
|
+
* a restart or a link outage replays instead of losing.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.LINE_STREAM = void 0;
|
|
9
|
+
exports.filterToSubject = filterToSubject;
|
|
10
|
+
exports.lineStreamConfigs = lineStreamConfigs;
|
|
11
|
+
exports.telemetrySubjects = telemetrySubjects;
|
|
12
|
+
exports.durableConsumer = durableConsumer;
|
|
13
|
+
exports.lineEventConsumer = lineEventConsumer;
|
|
14
|
+
exports.ensureStreams = ensureStreams;
|
|
15
|
+
exports.ensureConsumer = ensureConsumer;
|
|
16
|
+
const nats_1 = require("nats");
|
|
17
|
+
const codec_1 = require("../protocol/codec");
|
|
18
|
+
exports.LINE_STREAM = {
|
|
19
|
+
EVENTS: 'LINE_EVENTS',
|
|
20
|
+
TELEMETRY: 'LINE_TELEMETRY',
|
|
21
|
+
};
|
|
22
|
+
const DAY_MS = 86_400_000;
|
|
23
|
+
const ACK_WAIT_MS = 30_000;
|
|
24
|
+
const GENERIC = (root) => ({ root, channels: codec_1.GENERIC_CHANNELS });
|
|
25
|
+
/** An MQTT filter as a NATS subject: levels become tokens, `+` becomes `*`, `#` becomes `>`. */
|
|
26
|
+
function filterToSubject(filter) {
|
|
27
|
+
return filter.split('/').map((level) => (level === '+' ? '*' : level === '#' ? '>' : level)).join('.');
|
|
28
|
+
}
|
|
29
|
+
function subjectsFor(codec, channels, siteId, lineId) {
|
|
30
|
+
return channels.map((c) => filterToSubject((0, codec_1.topicOf)(codec, { siteId, lineId }, c)));
|
|
31
|
+
}
|
|
32
|
+
/** The site's streams, for every line under the codec's root; a root alone means the generic layout. */
|
|
33
|
+
function lineStreamConfigs(codec) {
|
|
34
|
+
const c = typeof codec === 'string' ? GENERIC(codec) : codec;
|
|
35
|
+
return [
|
|
36
|
+
{ name: exports.LINE_STREAM.EVENTS, subjects: subjectsFor(c, c.channels.events), max_age: (0, nats_1.nanos)(30 * DAY_MS), storage: nats_1.StorageType.File },
|
|
37
|
+
{ name: exports.LINE_STREAM.TELEMETRY, subjects: subjectsFor(c, c.channels.telemetry), max_age: (0, nats_1.nanos)(3 * DAY_MS), storage: nats_1.StorageType.File },
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
/** The telemetry subjects for one line, read live rather than replayed. */
|
|
41
|
+
function telemetrySubjects(codec, siteId, lineId) {
|
|
42
|
+
const c = typeof codec === 'string' ? GENERIC(codec) : codec;
|
|
43
|
+
return subjectsFor(c, c.channels.telemetry, siteId, lineId);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A durable consumer. It starts at new messages the first time (open rows
|
|
47
|
+
* are reconciled at boot); after that its position survives restarts.
|
|
48
|
+
*/
|
|
49
|
+
function durableConsumer(name, filterSubjects) {
|
|
50
|
+
return {
|
|
51
|
+
durable_name: name,
|
|
52
|
+
ack_policy: nats_1.AckPolicy.Explicit,
|
|
53
|
+
deliver_policy: nats_1.DeliverPolicy.New,
|
|
54
|
+
ack_wait: (0, nats_1.nanos)(ACK_WAIT_MS),
|
|
55
|
+
filter_subjects: filterSubjects,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function lineEventConsumer(codec, siteId, lineId, name) {
|
|
59
|
+
const c = typeof codec === 'string' ? GENERIC(codec) : codec;
|
|
60
|
+
return durableConsumer(name, subjectsFor(c, c.channels.events, siteId, lineId));
|
|
61
|
+
}
|
|
62
|
+
/** Create or update each stream, then the consumer. Safe on every boot. */
|
|
63
|
+
async function ensureStreams(jsm, streams) {
|
|
64
|
+
for (const config of streams) {
|
|
65
|
+
try {
|
|
66
|
+
await jsm.streams.info(config.name);
|
|
67
|
+
await jsm.streams.update(config.name, config);
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
if (!/stream not found/i.test(err?.message ?? ''))
|
|
71
|
+
throw err;
|
|
72
|
+
await jsm.streams.add(config);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function ensureConsumer(jsm, stream, config) {
|
|
77
|
+
let existing;
|
|
78
|
+
try {
|
|
79
|
+
existing = await jsm.consumers.info(stream, config.durable_name);
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
if (!/consumer not found/i.test(err?.message ?? ''))
|
|
83
|
+
throw err;
|
|
84
|
+
await jsm.consumers.add(stream, config);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
// The consumer keeps its position; only its subjects follow the codec's channels.
|
|
88
|
+
const want = [...(config.filter_subjects ?? [])].sort();
|
|
89
|
+
const have = [...(existing.config.filter_subjects ?? [])].sort();
|
|
90
|
+
if (want.join('|') !== have.join('|'))
|
|
91
|
+
await jsm.consumers.update(stream, config.durable_name, { filter_subjects: config.filter_subjects });
|
|
92
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* machine-line: drive the machine line example end to end.
|
|
3
|
+
*
|
|
4
|
+
* Starts unit routes on the factory line and plays the people: at every
|
|
5
|
+
* station people work (and at the inspector, where every route ends) a
|
|
6
|
+
* person takes the units off the station before and receives them; a
|
|
7
|
+
* technician clears alarms; an operator decides each failed handoff. The
|
|
8
|
+
* machines and the gateway do the rest over MQTT. MIX sets the share of
|
|
9
|
+
* routes minted from the automated manifest; the rest run the manual one,
|
|
10
|
+
* through the same workflow. Rows are watched on Long Tail's NATS.
|
|
11
|
+
*
|
|
12
|
+
* Usage (after `docker compose -f docker-compose.yml -f docker-compose.nats.yml -f docker-compose.machines.yml up -d --build`):
|
|
13
|
+
* npm run machine:line
|
|
14
|
+
* MIX=50 UNITS=8 npm run machine:line # half automated, half manual, same factory
|
|
15
|
+
* UNITS=8 BATCH=2 npm run machine:line
|
|
16
|
+
* FAULT=jam npm run machine:line # first route jams leaving FAULT_AT; operator retries
|
|
17
|
+
* FAULT=reject DECIDE=scrap npm run machine:line
|
|
18
|
+
*
|
|
19
|
+
* Env vars:
|
|
20
|
+
* BASE_URL Long Tail API (default http://localhost:3000)
|
|
21
|
+
* SIM_URL mock line drill server (default http://localhost:8090)
|
|
22
|
+
* NATS_URL Long Tail NATS (default nats://localhost:4222), NATS_TOKEN (default dev_api_secret)
|
|
23
|
+
* UNITS units to run (default 6); BATCH units per route (default 2)
|
|
24
|
+
* MIX percent of routes on the automated manifest (default 100); the rest run manual
|
|
25
|
+
* MACHINE_LINE_CONFIG, MACHINE_MANIFESTS line and manifests (default the factory line and route-manifests.json)
|
|
26
|
+
* FAULT jam | incomplete | carrier_mismatch | unreadable | stall | reject | hold | estop | offline
|
|
27
|
+
* FAULT_AT machine the fault happens at (default the first machine)
|
|
28
|
+
* DECIDE operator decision on a failed handoff: retry | manual | scrap (default retry;
|
|
29
|
+
* manual where the row does not offer retry)
|
|
30
|
+
* OFFLINE_S how long FAULT=offline keeps the machine away (default 25)
|
|
31
|
+
* HANDOFF_TIMEOUT per-handoff timeout (default 5m); TIMEOUT_S whole run (default 300)
|
|
32
|
+
*/
|
|
33
|
+
export {};
|