@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,27 @@
|
|
|
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("./delivery"), exports);
|
|
18
|
+
__exportStar(require("./memory-broker"), exports);
|
|
19
|
+
__exportStar(require("./memory"), exports);
|
|
20
|
+
__exportStar(require("./serial"), exports);
|
|
21
|
+
__exportStar(require("./streams"), exports);
|
|
22
|
+
__exportStar(require("./mqtt-session"), exports);
|
|
23
|
+
__exportStar(require("./mqtt-link"), exports);
|
|
24
|
+
__exportStar(require("./mqtt-bus"), exports);
|
|
25
|
+
__exportStar(require("./nats-bus"), exports);
|
|
26
|
+
__exportStar(require("./nats-feed"), exports);
|
|
27
|
+
__exportStar(require("./callback-feed"), exports);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An in-process broker with the semantics the line relies on: retained
|
|
3
|
+
* messages, wills, persistent sessions that queue QoS 1 messages while a
|
|
4
|
+
* client is away, and a durable consumer log with ack, nak and replay (the
|
|
5
|
+
* JetStream stand-in). Used by tests and single-process runs.
|
|
6
|
+
*/
|
|
7
|
+
import { type Disposition, type InboundMessage, type LinkWill } from './delivery';
|
|
8
|
+
export interface MemoryBrokerOptions {
|
|
9
|
+
/** Delay before a nak'd message is offered again. */
|
|
10
|
+
redeliverMs?: number;
|
|
11
|
+
maxDeliver?: number;
|
|
12
|
+
}
|
|
13
|
+
export declare class MemoryBroker {
|
|
14
|
+
private sessions;
|
|
15
|
+
private retainedStore;
|
|
16
|
+
private log;
|
|
17
|
+
private durables;
|
|
18
|
+
private readonly redeliverMs;
|
|
19
|
+
private readonly maxDeliver;
|
|
20
|
+
constructor(opts?: MemoryBrokerOptions);
|
|
21
|
+
connect(clientId: string, will?: LinkWill): void;
|
|
22
|
+
disconnect(clientId: string, graceful: boolean): void;
|
|
23
|
+
isConnected(clientId: string): boolean;
|
|
24
|
+
subscribe(clientId: string, filter: string, handler: (msg: InboundMessage) => void): void;
|
|
25
|
+
publish(topic: string, payload: string, retain?: boolean): void;
|
|
26
|
+
retained(filter: string): InboundMessage[];
|
|
27
|
+
/** Attach a handler to a named durable consumer; it resumes from its cursor. */
|
|
28
|
+
consume(name: string, filter: string, handler: (msg: InboundMessage) => Promise<Disposition>): void;
|
|
29
|
+
/** Detach the handler; unacked and unread messages wait for the next consume. */
|
|
30
|
+
detach(name: string): void;
|
|
31
|
+
shutdown(): void;
|
|
32
|
+
private deliverToSession;
|
|
33
|
+
private pump;
|
|
34
|
+
private offer;
|
|
35
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* An in-process broker with the semantics the line relies on: retained
|
|
4
|
+
* messages, wills, persistent sessions that queue QoS 1 messages while a
|
|
5
|
+
* client is away, and a durable consumer log with ack, nak and replay (the
|
|
6
|
+
* JetStream stand-in). Used by tests and single-process runs.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.MemoryBroker = void 0;
|
|
10
|
+
const delivery_1 = require("./delivery");
|
|
11
|
+
class MemoryBroker {
|
|
12
|
+
constructor(opts = {}) {
|
|
13
|
+
this.sessions = new Map();
|
|
14
|
+
this.retainedStore = new Map();
|
|
15
|
+
this.log = [];
|
|
16
|
+
this.durables = new Map();
|
|
17
|
+
this.redeliverMs = opts.redeliverMs ?? 20;
|
|
18
|
+
this.maxDeliver = opts.maxDeliver ?? 50;
|
|
19
|
+
}
|
|
20
|
+
connect(clientId, will) {
|
|
21
|
+
const session = this.sessions.get(clientId) ?? { connected: false, subs: new Map(), queued: [] };
|
|
22
|
+
session.connected = true;
|
|
23
|
+
session.will = will;
|
|
24
|
+
this.sessions.set(clientId, session);
|
|
25
|
+
const queued = session.queued.splice(0);
|
|
26
|
+
for (const msg of queued)
|
|
27
|
+
this.deliverToSession(session, msg);
|
|
28
|
+
}
|
|
29
|
+
disconnect(clientId, graceful) {
|
|
30
|
+
const session = this.sessions.get(clientId);
|
|
31
|
+
if (!session?.connected)
|
|
32
|
+
return;
|
|
33
|
+
session.connected = false;
|
|
34
|
+
const will = session.will;
|
|
35
|
+
session.will = undefined;
|
|
36
|
+
if (!graceful && will)
|
|
37
|
+
this.publish(will.topic, will.payload, will.retain);
|
|
38
|
+
}
|
|
39
|
+
isConnected(clientId) {
|
|
40
|
+
return this.sessions.get(clientId)?.connected ?? false;
|
|
41
|
+
}
|
|
42
|
+
subscribe(clientId, filter, handler) {
|
|
43
|
+
const session = this.sessions.get(clientId);
|
|
44
|
+
if (!session)
|
|
45
|
+
throw new Error(`subscribe before connect: ${clientId}`);
|
|
46
|
+
session.subs.set(filter, handler);
|
|
47
|
+
for (const [topic, payload] of this.retainedStore) {
|
|
48
|
+
if ((0, delivery_1.topicMatches)(filter, topic))
|
|
49
|
+
queueMicrotask(() => handler({ topic, payload, retained: true, deliveries: 1 }));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
publish(topic, payload, retain = false) {
|
|
53
|
+
if (retain) {
|
|
54
|
+
if (payload === '')
|
|
55
|
+
this.retainedStore.delete(topic);
|
|
56
|
+
else
|
|
57
|
+
this.retainedStore.set(topic, payload);
|
|
58
|
+
}
|
|
59
|
+
for (const session of this.sessions.values()) {
|
|
60
|
+
this.deliverToSession(session, { topic, payload, retained: false, deliveries: 1 });
|
|
61
|
+
}
|
|
62
|
+
this.log.push({ seq: this.log.length, topic, payload, retained: retain });
|
|
63
|
+
for (const durable of this.durables.values())
|
|
64
|
+
void this.pump(durable);
|
|
65
|
+
}
|
|
66
|
+
retained(filter) {
|
|
67
|
+
return [...this.retainedStore]
|
|
68
|
+
.filter(([topic]) => (0, delivery_1.topicMatches)(filter, topic))
|
|
69
|
+
.map(([topic, payload]) => ({ topic, payload, retained: true, deliveries: 1 }));
|
|
70
|
+
}
|
|
71
|
+
/** Attach a handler to a named durable consumer; it resumes from its cursor. */
|
|
72
|
+
consume(name, filter, handler) {
|
|
73
|
+
const durable = this.durables.get(name) ?? { filter, cursor: 0, pumping: false, retries: new Map(), timers: new Set() };
|
|
74
|
+
durable.handler = handler;
|
|
75
|
+
this.durables.set(name, durable);
|
|
76
|
+
void this.pump(durable);
|
|
77
|
+
}
|
|
78
|
+
/** Detach the handler; unacked and unread messages wait for the next consume. */
|
|
79
|
+
detach(name) {
|
|
80
|
+
const durable = this.durables.get(name);
|
|
81
|
+
if (!durable)
|
|
82
|
+
return;
|
|
83
|
+
durable.handler = undefined;
|
|
84
|
+
for (const t of durable.timers)
|
|
85
|
+
clearTimeout(t);
|
|
86
|
+
durable.timers.clear();
|
|
87
|
+
if (durable.retries.size)
|
|
88
|
+
durable.cursor = Math.min(durable.cursor, ...durable.retries.keys());
|
|
89
|
+
durable.retries.clear();
|
|
90
|
+
}
|
|
91
|
+
shutdown() {
|
|
92
|
+
for (const name of this.durables.keys())
|
|
93
|
+
this.detach(name);
|
|
94
|
+
}
|
|
95
|
+
deliverToSession(session, msg) {
|
|
96
|
+
for (const [filter, handler] of session.subs) {
|
|
97
|
+
if (!(0, delivery_1.topicMatches)(filter, msg.topic))
|
|
98
|
+
continue;
|
|
99
|
+
if (session.connected)
|
|
100
|
+
queueMicrotask(() => handler(msg));
|
|
101
|
+
else
|
|
102
|
+
session.queued.push(msg);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async pump(durable) {
|
|
106
|
+
if (durable.pumping)
|
|
107
|
+
return;
|
|
108
|
+
durable.pumping = true;
|
|
109
|
+
try {
|
|
110
|
+
while (durable.handler && durable.cursor < this.log.length) {
|
|
111
|
+
const entry = this.log[durable.cursor++];
|
|
112
|
+
if ((0, delivery_1.topicMatches)(durable.filter, entry.topic))
|
|
113
|
+
await this.offer(durable, entry.seq);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
durable.pumping = false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
async offer(durable, seq) {
|
|
121
|
+
const handler = durable.handler;
|
|
122
|
+
if (!handler)
|
|
123
|
+
return;
|
|
124
|
+
const entry = this.log[seq];
|
|
125
|
+
const deliveries = (durable.retries.get(seq) ?? 0) + 1;
|
|
126
|
+
durable.retries.set(seq, deliveries);
|
|
127
|
+
let disposition;
|
|
128
|
+
try {
|
|
129
|
+
disposition = await handler({ topic: entry.topic, payload: entry.payload, retained: entry.retained, deliveries });
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
disposition = delivery_1.DISPOSITION.NAK;
|
|
133
|
+
}
|
|
134
|
+
if (disposition !== delivery_1.DISPOSITION.NAK || deliveries >= this.maxDeliver) {
|
|
135
|
+
durable.retries.delete(seq);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const timer = setTimeout(() => {
|
|
139
|
+
durable.timers.delete(timer);
|
|
140
|
+
void this.offer(durable, seq);
|
|
141
|
+
}, this.redeliverMs);
|
|
142
|
+
durable.timers.add(timer);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
exports.MemoryBroker = MemoryBroker;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory adapters over one MemoryBroker: a MachineLink per mock machine, the
|
|
3
|
+
* gateway's MachineBus, and an EscalationFeed that tests publish into the
|
|
4
|
+
* way Long Tail publishes escalation events.
|
|
5
|
+
*/
|
|
6
|
+
import { MemoryBroker } from './memory-broker';
|
|
7
|
+
import { type Disposition, type EscalationEvent, type EscalationFeed, type InboundMessage, type LinkConnectOptions, type MachineBus, type MachineLink, type PublishOptions } from './delivery';
|
|
8
|
+
export declare class MemoryMachineLink implements MachineLink {
|
|
9
|
+
private readonly broker;
|
|
10
|
+
private clientId?;
|
|
11
|
+
constructor(broker: MemoryBroker);
|
|
12
|
+
get connected(): boolean;
|
|
13
|
+
connect(opts: LinkConnectOptions): Promise<void>;
|
|
14
|
+
publish(topic: string, payload: string, opts: PublishOptions): Promise<void>;
|
|
15
|
+
subscribe(filter: string, handler: (msg: InboundMessage) => void): Promise<void>;
|
|
16
|
+
disconnect(graceful: boolean): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
export declare class MemoryMachineBus implements MachineBus {
|
|
19
|
+
private readonly broker;
|
|
20
|
+
private readonly filter;
|
|
21
|
+
private readonly durableName;
|
|
22
|
+
constructor(broker: MemoryBroker, filter: string, durableName?: string);
|
|
23
|
+
connect(): Promise<void>;
|
|
24
|
+
consume(handler: (msg: InboundMessage) => Promise<Disposition>): Promise<void>;
|
|
25
|
+
publish(topic: string, payload: string, opts?: {
|
|
26
|
+
retain?: boolean;
|
|
27
|
+
}): Promise<void>;
|
|
28
|
+
retained(filter: string): Promise<InboundMessage[]>;
|
|
29
|
+
close(): Promise<void>;
|
|
30
|
+
}
|
|
31
|
+
export declare class MemoryEscalationFeed implements EscalationFeed {
|
|
32
|
+
private readonly broker;
|
|
33
|
+
private readonly durableName;
|
|
34
|
+
constructor(broker: MemoryBroker, durableName?: string);
|
|
35
|
+
/** Publish an LTEvent body as Long Tail would. */
|
|
36
|
+
emit(body: Record<string, any>): void;
|
|
37
|
+
connect(): Promise<void>;
|
|
38
|
+
consume(handler: (event: EscalationEvent) => Promise<Disposition>): Promise<void>;
|
|
39
|
+
close(): Promise<void>;
|
|
40
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Memory adapters over one MemoryBroker: a MachineLink per mock machine, the
|
|
4
|
+
* gateway's MachineBus, and an EscalationFeed that tests publish into the
|
|
5
|
+
* way Long Tail publishes escalation events.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.MemoryEscalationFeed = exports.MemoryMachineBus = exports.MemoryMachineLink = void 0;
|
|
9
|
+
const delivery_1 = require("./delivery");
|
|
10
|
+
class MemoryMachineLink {
|
|
11
|
+
constructor(broker) {
|
|
12
|
+
this.broker = broker;
|
|
13
|
+
}
|
|
14
|
+
get connected() {
|
|
15
|
+
return !!this.clientId && this.broker.isConnected(this.clientId);
|
|
16
|
+
}
|
|
17
|
+
async connect(opts) {
|
|
18
|
+
this.clientId = opts.clientId;
|
|
19
|
+
this.broker.connect(opts.clientId, opts.will);
|
|
20
|
+
}
|
|
21
|
+
async publish(topic, payload, opts) {
|
|
22
|
+
if (!this.connected)
|
|
23
|
+
throw new Error(`publish while disconnected: ${topic}`);
|
|
24
|
+
this.broker.publish(topic, payload, opts.retain ?? false);
|
|
25
|
+
}
|
|
26
|
+
async subscribe(filter, handler) {
|
|
27
|
+
if (!this.clientId)
|
|
28
|
+
throw new Error('subscribe before connect');
|
|
29
|
+
this.broker.subscribe(this.clientId, filter, handler);
|
|
30
|
+
}
|
|
31
|
+
async disconnect(graceful) {
|
|
32
|
+
if (this.clientId)
|
|
33
|
+
this.broker.disconnect(this.clientId, graceful);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
exports.MemoryMachineLink = MemoryMachineLink;
|
|
37
|
+
class MemoryMachineBus {
|
|
38
|
+
constructor(broker, filter, durableName = 'line-gateway') {
|
|
39
|
+
this.broker = broker;
|
|
40
|
+
this.filter = filter;
|
|
41
|
+
this.durableName = durableName;
|
|
42
|
+
}
|
|
43
|
+
async connect() { }
|
|
44
|
+
async consume(handler) {
|
|
45
|
+
this.broker.consume(this.durableName, this.filter, handler);
|
|
46
|
+
}
|
|
47
|
+
async publish(topic, payload, opts = {}) {
|
|
48
|
+
this.broker.publish(topic, payload, opts.retain ?? false);
|
|
49
|
+
}
|
|
50
|
+
async retained(filter) {
|
|
51
|
+
return this.broker.retained(filter);
|
|
52
|
+
}
|
|
53
|
+
async close() {
|
|
54
|
+
this.broker.detach(this.durableName);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
exports.MemoryMachineBus = MemoryMachineBus;
|
|
58
|
+
const FEED_TOPIC = 'lt/events';
|
|
59
|
+
class MemoryEscalationFeed {
|
|
60
|
+
constructor(broker, durableName = 'line-gateway-escalations') {
|
|
61
|
+
this.broker = broker;
|
|
62
|
+
this.durableName = durableName;
|
|
63
|
+
}
|
|
64
|
+
/** Publish an LTEvent body as Long Tail would. */
|
|
65
|
+
emit(body) {
|
|
66
|
+
this.broker.publish(FEED_TOPIC, JSON.stringify(body));
|
|
67
|
+
}
|
|
68
|
+
async connect() { }
|
|
69
|
+
async consume(handler) {
|
|
70
|
+
this.broker.consume(this.durableName, FEED_TOPIC, async (msg) => {
|
|
71
|
+
const event = (0, delivery_1.toEscalationEvent)(JSON.parse(msg.payload));
|
|
72
|
+
return event ? handler(event) : delivery_1.DISPOSITION.TERM;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
async close() {
|
|
76
|
+
this.broker.detach(this.durableName);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
exports.MemoryEscalationFeed = MemoryEscalationFeed;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gateway over plain MQTT, for any broker. The persistent session keeps
|
|
3
|
+
* QoS 1 messages while the gateway is away; there is no stream to replay
|
|
4
|
+
* beyond that, and a message that is not yet actionable is retried in this
|
|
5
|
+
* process.
|
|
6
|
+
*/
|
|
7
|
+
import type { MqttEndpoint } from '../../../services/machines/config';
|
|
8
|
+
import type { Disposition, InboundMessage, MachineBus } from './delivery';
|
|
9
|
+
export declare class MqttMachineBus implements MachineBus {
|
|
10
|
+
private readonly endpoint;
|
|
11
|
+
private readonly clientId;
|
|
12
|
+
private readonly filter;
|
|
13
|
+
private session?;
|
|
14
|
+
private readonly redelivery;
|
|
15
|
+
constructor(endpoint: MqttEndpoint, clientId: string, filter: string, nakDelayMs?: number);
|
|
16
|
+
connect(): Promise<void>;
|
|
17
|
+
consume(handler: (msg: InboundMessage) => Promise<Disposition>): Promise<void>;
|
|
18
|
+
publish(topic: string, payload: string, opts?: {
|
|
19
|
+
retain?: boolean;
|
|
20
|
+
}): Promise<void>;
|
|
21
|
+
retained(filter: string): Promise<InboundMessage[]>;
|
|
22
|
+
close(): Promise<void>;
|
|
23
|
+
private connected;
|
|
24
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The gateway over plain MQTT, for any broker. The persistent session keeps
|
|
4
|
+
* QoS 1 messages while the gateway is away; there is no stream to replay
|
|
5
|
+
* beyond that, and a message that is not yet actionable is retried in this
|
|
6
|
+
* process.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.MqttMachineBus = void 0;
|
|
10
|
+
const mqtt_session_1 = require("./mqtt-session");
|
|
11
|
+
const serial_1 = require("./serial");
|
|
12
|
+
class MqttMachineBus {
|
|
13
|
+
constructor(endpoint, clientId, filter, nakDelayMs = 1_000) {
|
|
14
|
+
this.endpoint = endpoint;
|
|
15
|
+
this.clientId = clientId;
|
|
16
|
+
this.filter = filter;
|
|
17
|
+
this.redelivery = new serial_1.LocalRedelivery(new serial_1.SerialQueue(), nakDelayMs);
|
|
18
|
+
}
|
|
19
|
+
async connect() {
|
|
20
|
+
this.session = await mqtt_session_1.MqttSession.open(this.endpoint, this.clientId);
|
|
21
|
+
}
|
|
22
|
+
async consume(handler) {
|
|
23
|
+
await this.connected().subscribe(this.filter, (msg) => {
|
|
24
|
+
void this.redelivery.deliver((deliveries) => handler({ ...msg, deliveries }));
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
async publish(topic, payload, opts = {}) {
|
|
28
|
+
await this.connected().publish(topic, payload, 1, opts.retain ?? false);
|
|
29
|
+
}
|
|
30
|
+
retained(filter) {
|
|
31
|
+
return this.connected().retained(filter);
|
|
32
|
+
}
|
|
33
|
+
async close() {
|
|
34
|
+
await this.redelivery.stop();
|
|
35
|
+
await this.session?.close(true);
|
|
36
|
+
}
|
|
37
|
+
connected() {
|
|
38
|
+
if (!this.session)
|
|
39
|
+
throw new Error('MQTT bus used before connect()');
|
|
40
|
+
return this.session;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
exports.MqttMachineBus = MqttMachineBus;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The machine side over MQTT: one persistent session per machine, logged in
|
|
3
|
+
* with its own credentials, with a will that marks it offline.
|
|
4
|
+
*/
|
|
5
|
+
import type { MqttEndpoint } from '../../../services/machines/config';
|
|
6
|
+
import type { InboundMessage, LinkConnectOptions, MachineLink, PublishOptions } from './delivery';
|
|
7
|
+
export declare class MqttMachineLink implements MachineLink {
|
|
8
|
+
private readonly endpoint;
|
|
9
|
+
private session?;
|
|
10
|
+
constructor(endpoint: MqttEndpoint);
|
|
11
|
+
get connected(): boolean;
|
|
12
|
+
connect(opts: LinkConnectOptions): Promise<void>;
|
|
13
|
+
publish(topic: string, payload: string, opts: PublishOptions): Promise<void>;
|
|
14
|
+
subscribe(filter: string, handler: (msg: InboundMessage) => void): Promise<void>;
|
|
15
|
+
disconnect(graceful: boolean): Promise<void>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The machine side over MQTT: one persistent session per machine, logged in
|
|
4
|
+
* with its own credentials, with a will that marks it offline.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.MqttMachineLink = void 0;
|
|
8
|
+
const mqtt_session_1 = require("./mqtt-session");
|
|
9
|
+
class MqttMachineLink {
|
|
10
|
+
constructor(endpoint) {
|
|
11
|
+
this.endpoint = endpoint;
|
|
12
|
+
}
|
|
13
|
+
get connected() {
|
|
14
|
+
return !!this.session?.connected;
|
|
15
|
+
}
|
|
16
|
+
async connect(opts) {
|
|
17
|
+
if (this.session)
|
|
18
|
+
await this.session.close(false);
|
|
19
|
+
this.session = await mqtt_session_1.MqttSession.open(this.endpoint, opts.clientId, { will: opts.will, onRestored: opts.onRestored });
|
|
20
|
+
}
|
|
21
|
+
async publish(topic, payload, opts) {
|
|
22
|
+
if (!this.session)
|
|
23
|
+
throw new Error(`publish before connect: ${topic}`);
|
|
24
|
+
await this.session.publish(topic, payload, opts.qos, opts.retain ?? false);
|
|
25
|
+
}
|
|
26
|
+
async subscribe(filter, handler) {
|
|
27
|
+
if (!this.session)
|
|
28
|
+
throw new Error('subscribe before connect');
|
|
29
|
+
await this.session.subscribe(filter, handler);
|
|
30
|
+
}
|
|
31
|
+
async disconnect(graceful) {
|
|
32
|
+
const session = this.session;
|
|
33
|
+
this.session = undefined;
|
|
34
|
+
await session?.close(graceful);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
exports.MqttMachineLink = MqttMachineLink;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One MQTT client with a persistent session (clean start off), shared by the
|
|
3
|
+
* machine link and the controller buses. A session resumed after a restart
|
|
4
|
+
* delivers queued QoS 1 messages right after the connect acknowledgement,
|
|
5
|
+
* before anyone has subscribed again; those are held in the process until a
|
|
6
|
+
* handler for their topic registers, so a queued command is not dropped.
|
|
7
|
+
*/
|
|
8
|
+
import type { IClientOptions, MqttClient } from 'mqtt';
|
|
9
|
+
import type { MqttEndpoint } from '../../../services/machines/config';
|
|
10
|
+
import { type InboundMessage, type LinkWill } from './delivery';
|
|
11
|
+
type Handler = (msg: InboundMessage) => void;
|
|
12
|
+
export interface SessionOptions {
|
|
13
|
+
will?: LinkWill;
|
|
14
|
+
onRestored?: () => void;
|
|
15
|
+
}
|
|
16
|
+
/** Client options for one identity; `{clientId}` in a path or username picks that identity's credentials. */
|
|
17
|
+
export declare function mqttOptions(endpoint: MqttEndpoint, clientId: string, will?: LinkWill): IClientOptions;
|
|
18
|
+
export declare class MqttSession {
|
|
19
|
+
readonly client: MqttClient;
|
|
20
|
+
private handlers;
|
|
21
|
+
private held;
|
|
22
|
+
private constructor();
|
|
23
|
+
/** Connect and wait for the broker's acknowledgement; a refusal rejects. */
|
|
24
|
+
static open(endpoint: MqttEndpoint, clientId: string, opts?: SessionOptions): Promise<MqttSession>;
|
|
25
|
+
get connected(): boolean;
|
|
26
|
+
subscribe(filter: string, handler: Handler): Promise<void>;
|
|
27
|
+
publish(topic: string, payload: string, qos: 0 | 1, retain?: boolean): Promise<void>;
|
|
28
|
+
/** Retained messages under `filter`, as the broker sends them on subscribe. */
|
|
29
|
+
retained(filter: string, settleMs?: number): Promise<InboundMessage[]>;
|
|
30
|
+
/** Graceful sends DISCONNECT, so the broker drops the will; otherwise the socket just closes. */
|
|
31
|
+
close(graceful: boolean): Promise<void>;
|
|
32
|
+
private route;
|
|
33
|
+
}
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* One MQTT client with a persistent session (clean start off), shared by the
|
|
4
|
+
* machine link and the controller buses. A session resumed after a restart
|
|
5
|
+
* delivers queued QoS 1 messages right after the connect acknowledgement,
|
|
6
|
+
* before anyone has subscribed again; those are held in the process until a
|
|
7
|
+
* handler for their topic registers, so a queued command is not dropped.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.MqttSession = void 0;
|
|
11
|
+
exports.mqttOptions = mqttOptions;
|
|
12
|
+
const fs_1 = require("fs");
|
|
13
|
+
const delivery_1 = require("./delivery");
|
|
14
|
+
const HELD_LIMIT = 1_000;
|
|
15
|
+
const MQTT_MISSING = 'the MQTT transport needs the "mqtt" package (npm install mqtt)';
|
|
16
|
+
/** mqtt is an optional peer dependency, loaded only when an MQTT transport is used. */
|
|
17
|
+
function loadMqtt() {
|
|
18
|
+
try {
|
|
19
|
+
return require('mqtt');
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
const error = new Error(MQTT_MISSING);
|
|
23
|
+
error.cause = err;
|
|
24
|
+
throw error;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const SESSION_EXPIRY_SEC = 86_400;
|
|
28
|
+
const RECONNECT_MS = 1_000;
|
|
29
|
+
const CONNECT_TIMEOUT_MS = 10_000;
|
|
30
|
+
/** Client options for one identity; `{clientId}` in a path or username picks that identity's credentials. */
|
|
31
|
+
function mqttOptions(endpoint, clientId, will) {
|
|
32
|
+
const fill = (value) => value?.split('{clientId}').join(clientId);
|
|
33
|
+
const file = (path) => (path ? (0, fs_1.readFileSync)(fill(path)) : undefined);
|
|
34
|
+
return {
|
|
35
|
+
clientId,
|
|
36
|
+
protocolVersion: endpoint.protocolVersion,
|
|
37
|
+
clean: false,
|
|
38
|
+
...(endpoint.protocolVersion === 5 ? { properties: { sessionExpiryInterval: SESSION_EXPIRY_SEC } } : {}),
|
|
39
|
+
ca: file(endpoint.caFile),
|
|
40
|
+
cert: file(endpoint.certFile),
|
|
41
|
+
key: file(endpoint.keyFile),
|
|
42
|
+
username: fill(endpoint.username),
|
|
43
|
+
password: endpoint.password,
|
|
44
|
+
reconnectPeriod: RECONNECT_MS,
|
|
45
|
+
connectTimeout: CONNECT_TIMEOUT_MS,
|
|
46
|
+
will: will ? { topic: will.topic, payload: Buffer.from(will.payload), qos: 1, retain: will.retain } : undefined,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
class MqttSession {
|
|
50
|
+
constructor(client) {
|
|
51
|
+
this.client = client;
|
|
52
|
+
this.handlers = [];
|
|
53
|
+
this.held = [];
|
|
54
|
+
client.on('message', (topic, payload, packet) => {
|
|
55
|
+
this.route({ topic, payload: payload.toString(), retained: packet.retain, deliveries: packet.dup ? 2 : 1 });
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/** Connect and wait for the broker's acknowledgement; a refusal rejects. */
|
|
59
|
+
static open(endpoint, clientId, opts = {}) {
|
|
60
|
+
const client = loadMqtt().connect(endpoint.url, mqttOptions(endpoint, clientId, opts.will));
|
|
61
|
+
const session = new MqttSession(client);
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
const fail = (err) => {
|
|
64
|
+
client.end(true);
|
|
65
|
+
reject(new Error(`MQTT ${clientId} could not connect to ${endpoint.url}: ${err.message}`));
|
|
66
|
+
};
|
|
67
|
+
client.once('error', fail);
|
|
68
|
+
client.once('connect', () => {
|
|
69
|
+
client.off('error', fail);
|
|
70
|
+
client.on('error', () => undefined);
|
|
71
|
+
client.on('connect', () => opts.onRestored?.());
|
|
72
|
+
resolve(session);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
get connected() {
|
|
77
|
+
return this.client.connected;
|
|
78
|
+
}
|
|
79
|
+
async subscribe(filter, handler) {
|
|
80
|
+
this.handlers.push({ filter, handler });
|
|
81
|
+
const ready = this.held.filter((m) => (0, delivery_1.topicMatches)(filter, m.topic));
|
|
82
|
+
this.held = this.held.filter((m) => !(0, delivery_1.topicMatches)(filter, m.topic));
|
|
83
|
+
await this.client.subscribeAsync(filter, { qos: 1 });
|
|
84
|
+
for (const m of ready)
|
|
85
|
+
handler(m);
|
|
86
|
+
}
|
|
87
|
+
async publish(topic, payload, qos, retain = false) {
|
|
88
|
+
await this.client.publishAsync(topic, payload, { qos, retain });
|
|
89
|
+
}
|
|
90
|
+
/** Retained messages under `filter`, as the broker sends them on subscribe. */
|
|
91
|
+
async retained(filter, settleMs = 250) {
|
|
92
|
+
const seen = [];
|
|
93
|
+
const collect = (topic, payload, packet) => {
|
|
94
|
+
if (packet.retain && (0, delivery_1.topicMatches)(filter, topic))
|
|
95
|
+
seen.push({ topic, payload: payload.toString(), retained: true, deliveries: 1 });
|
|
96
|
+
};
|
|
97
|
+
this.client.on('message', collect);
|
|
98
|
+
try {
|
|
99
|
+
await this.client.subscribeAsync(filter, { qos: 1 });
|
|
100
|
+
await new Promise((r) => setTimeout(r, settleMs));
|
|
101
|
+
await this.client.unsubscribeAsync(filter);
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
this.client.off('message', collect);
|
|
105
|
+
}
|
|
106
|
+
return seen;
|
|
107
|
+
}
|
|
108
|
+
/** Graceful sends DISCONNECT, so the broker drops the will; otherwise the socket just closes. */
|
|
109
|
+
async close(graceful) {
|
|
110
|
+
if (graceful)
|
|
111
|
+
await this.client.endAsync(false);
|
|
112
|
+
else
|
|
113
|
+
this.client.end(true);
|
|
114
|
+
}
|
|
115
|
+
route(msg) {
|
|
116
|
+
const matching = this.handlers.filter((h) => (0, delivery_1.topicMatches)(h.filter, msg.topic));
|
|
117
|
+
if (matching.length) {
|
|
118
|
+
for (const h of matching)
|
|
119
|
+
h.handler(msg);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
this.held.push(msg);
|
|
123
|
+
if (this.held.length > HELD_LIMIT)
|
|
124
|
+
this.held.shift();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
exports.MqttSession = MqttSession;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gateway on the site NATS. Machine events come from the line's durable
|
|
3
|
+
* consumer on LINE_EVENTS (replayed after a restart); telemetry is read live,
|
|
4
|
+
* since it only proves a machine is alive. Commands leave through an MQTT
|
|
5
|
+
* session at QoS 1: a NATS publish reaches MQTT subscribers at QoS 0 and
|
|
6
|
+
* would not wait for a machine that is away.
|
|
7
|
+
*/
|
|
8
|
+
import type { MqttEndpoint } from '../../../services/machines/config';
|
|
9
|
+
import { type StreamCodec } from './streams';
|
|
10
|
+
import type { Disposition, InboundMessage, MachineBus } from './delivery';
|
|
11
|
+
export interface NatsBusOptions {
|
|
12
|
+
natsUrl: string;
|
|
13
|
+
user?: string;
|
|
14
|
+
password?: string;
|
|
15
|
+
mqtt: MqttEndpoint;
|
|
16
|
+
/** The gateway's broker identity; also its MQTT client id. */
|
|
17
|
+
controllerId: string;
|
|
18
|
+
siteId: string;
|
|
19
|
+
lineId: string;
|
|
20
|
+
/** The codec's topics: which subjects are durable events and which are live telemetry. */
|
|
21
|
+
codec: StreamCodec;
|
|
22
|
+
nakDelayMs?: number;
|
|
23
|
+
}
|
|
24
|
+
export declare class NatsMachineBus implements MachineBus {
|
|
25
|
+
private readonly opts;
|
|
26
|
+
private nc?;
|
|
27
|
+
private session?;
|
|
28
|
+
private messages?;
|
|
29
|
+
private telemetry;
|
|
30
|
+
private readonly queue;
|
|
31
|
+
constructor(opts: NatsBusOptions);
|
|
32
|
+
get durableName(): string;
|
|
33
|
+
connect(): Promise<void>;
|
|
34
|
+
consume(handler: (msg: InboundMessage) => Promise<Disposition>): Promise<void>;
|
|
35
|
+
publish(topic: string, payload: string, opts?: {
|
|
36
|
+
retain?: boolean;
|
|
37
|
+
}): Promise<void>;
|
|
38
|
+
retained(filter: string): Promise<InboundMessage[]>;
|
|
39
|
+
close(): Promise<void>;
|
|
40
|
+
private nats;
|
|
41
|
+
private mqtt;
|
|
42
|
+
}
|