@hotmeshio/long-tail 0.26.0 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/index.d.ts +4 -0
- package/build/index.js +30 -8
- package/build/lib/events/nats.d.ts +11 -0
- package/build/lib/events/nats.js +33 -2
- package/build/lib/machines/index.d.ts +1 -0
- package/build/lib/machines/index.js +1 -0
- package/build/lib/machines/protocol/codec.d.ts +16 -0
- package/build/lib/machines/protocol/codec.js +1 -0
- package/build/lib/machines/protocol/contract.js +10 -1
- package/build/lib/machines/site/edge-account.d.ts +11 -0
- package/build/lib/machines/site/edge-account.js +24 -0
- package/build/lib/machines/site/site-conf.d.ts +7 -4
- package/build/lib/machines/site/site-conf.js +13 -6
- package/build/lib/machines/transport/delivery.d.ts +3 -4
- package/build/lib/machines/transport/delivery.js +3 -8
- package/build/lib/machines/transport/index.d.ts +2 -0
- package/build/lib/machines/transport/index.js +2 -0
- package/build/lib/machines/transport/nats-bus.d.ts +7 -13
- package/build/lib/machines/transport/nats-bus.js +23 -43
- package/build/lib/machines/transport/nats-feed.js +2 -2
- package/build/lib/machines/transport/pubsub-feed.d.ts +17 -0
- package/build/lib/machines/transport/pubsub-feed.js +39 -0
- package/build/lib/machines/transport/stream-bus.d.ts +40 -0
- package/build/lib/machines/transport/stream-bus.js +57 -0
- package/build/lib/machines/transport/streams.d.ts +10 -14
- package/build/lib/machines/transport/streams.js +18 -61
- package/build/lib/messaging/config.d.ts +25 -0
- package/build/lib/messaging/config.js +55 -0
- package/build/lib/messaging/constants.d.ts +26 -0
- package/build/lib/messaging/constants.js +26 -0
- package/build/lib/messaging/index.d.ts +24 -0
- package/build/lib/messaging/index.js +65 -0
- package/build/lib/messaging/memory.d.ts +16 -0
- package/build/lib/messaging/memory.js +147 -0
- package/build/lib/messaging/nats/permissions.d.ts +13 -0
- package/build/lib/messaging/nats/permissions.js +22 -0
- package/build/lib/messaging/nats/provider.d.ts +30 -0
- package/build/lib/messaging/nats/provider.js +77 -0
- package/build/lib/messaging/nats/render-conf.d.ts +68 -0
- package/build/lib/messaging/nats/render-conf.js +58 -0
- package/build/lib/messaging/nats/streams.d.ts +26 -0
- package/build/lib/messaging/nats/streams.js +97 -0
- package/build/lib/messaging/subjects.d.ts +8 -0
- package/build/lib/messaging/subjects.js +25 -0
- package/build/scripts/bambu-boat.d.ts +17 -0
- package/build/scripts/bambu-boat.js +134 -0
- package/build/scripts/bambu-print.d.ts +18 -0
- package/build/scripts/bambu-print.js +114 -0
- package/build/services/machines/config.d.ts +6 -0
- package/build/services/machines/config.js +16 -6
- package/build/services/machines/contracts.d.ts +19 -2
- package/build/services/machines/contracts.js +12 -3
- package/build/services/machines/gateway/actions.d.ts +1 -1
- package/build/services/machines/gateway/dispatcher.d.ts +16 -3
- package/build/services/machines/gateway/dispatcher.js +50 -10
- package/build/services/machines/gateway/executor.d.ts +7 -2
- package/build/services/machines/gateway/executor.js +35 -9
- package/build/services/machines/gateway/gateway.d.ts +6 -0
- package/build/services/machines/gateway/gateway.js +33 -10
- package/build/services/machines/gateway/lifecycle.d.ts +21 -0
- package/build/services/machines/gateway/lifecycle.js +89 -0
- package/build/services/machines/gateway/pacing.d.ts +14 -0
- package/build/services/machines/gateway/pacing.js +26 -2
- package/build/services/machines/gateway/plan-escalation.js +13 -4
- package/build/services/machines/gateway/plan-machine.d.ts +5 -4
- package/build/services/machines/gateway/plan-machine.js +55 -14
- package/build/services/machines/gateway/reconcile.d.ts +2 -0
- package/build/services/machines/gateway/reconcile.js +13 -0
- package/build/services/machines/gateway/state.d.ts +21 -1
- package/build/services/machines/gateway/state.js +16 -3
- package/build/services/machines/leadership.d.ts +17 -4
- package/build/services/machines/leadership.js +43 -18
- package/build/services/machines/rows.js +17 -2
- package/build/services/machines/select.d.ts +9 -2
- package/build/services/machines/select.js +20 -7
- package/build/services/machines/surface.js +8 -2
- package/build/services/machines/testing/fake-long-tail.d.ts +1 -0
- package/build/services/machines/testing/fake-long-tail.js +6 -3
- package/build/start/adapters.js +11 -2
- package/build/start/index.js +7 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/build/types/index.d.ts +1 -0
- package/build/types/machines.d.ts +2 -1
- package/build/types/messaging.d.ts +72 -0
- package/build/types/messaging.js +11 -0
- package/build/types/startup.d.ts +26 -1
- package/docs/events.md +50 -4
- package/docs/machines.md +38 -9
- package/package.json +3 -1
- package/build/lib/machines/transport/nats-durable.d.ts +0 -11
- package/build/lib/machines/transport/nats-durable.js +0 -28
package/build/index.d.ts
CHANGED
|
@@ -21,6 +21,10 @@ export { NatsEventAdapter } from './lib/events/nats';
|
|
|
21
21
|
export { InMemoryEventAdapter } from './lib/events/memory';
|
|
22
22
|
export { SocketIOEventAdapter, type SocketIOAuthenticator } from './lib/events/socketio';
|
|
23
23
|
export { publishMilestoneEvent, publishTaskEvent, publishEscalationEvent, publishActivityEvent, publishWorkflowEvent, publishAgentEvent, publishKnowledgeEvent } from './lib/events/publish';
|
|
24
|
+
export { messagingRegistry, MemoryMessagingProvider, DELIVERY } from './lib/messaging';
|
|
25
|
+
export { NatsMessagingProvider } from './lib/messaging/nats/provider';
|
|
26
|
+
export { renderNatsConf, type NatsConfOptions, type EdgeAccount, type NatsUser } from './lib/messaging/nats/render-conf';
|
|
27
|
+
export { jsPermissions } from './lib/messaging/nats/permissions';
|
|
24
28
|
export { telemetryRegistry } from './lib/telemetry';
|
|
25
29
|
export { HoneycombTelemetryAdapter } from './lib/telemetry/honeycomb';
|
|
26
30
|
export { loggerRegistry } from './lib/logger';
|
package/build/index.js
CHANGED
|
@@ -15,9 +15,6 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (
|
|
|
15
15
|
}) : function(o, v) {
|
|
16
16
|
o["default"] = v;
|
|
17
17
|
});
|
|
18
|
-
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
19
|
-
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
20
|
-
};
|
|
21
18
|
var __importStar = (this && this.__importStar) || (function () {
|
|
22
19
|
var ownKeys = function(o) {
|
|
23
20
|
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
@@ -35,12 +32,16 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
32
|
return result;
|
|
36
33
|
};
|
|
37
34
|
})();
|
|
35
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
36
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
37
|
+
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.
|
|
40
|
-
exports.LTExpressAdapter = exports.Machines = exports.api = exports.KnowledgeApi = exports.resolveCredential = exports.mimeFromPath = exports.getStorageBackend = exports.listOAuthConnections = exports.getFreshAccessToken = exports.createClient = exports.CallbackEventAdapter = exports.builtinMcpServerFactories = exports.getSystemAgents = exports.getSystemWorkers = exports.registerMcpTool = exports.getToolContext = exports.getActivityIdentity = exports.McpEscalationStrategy = exports.DefaultEscalationStrategy = exports.escalationStrategyRegistry = exports.McpVisionServer = exports.McpTranslationServer = exports.McpServer = void 0;
|
|
39
|
+
exports.PinoLoggerAdapter = exports.loggerRegistry = exports.HoneycombTelemetryAdapter = exports.telemetryRegistry = exports.jsPermissions = exports.renderNatsConf = exports.NatsMessagingProvider = exports.DELIVERY = exports.MemoryMessagingProvider = exports.messagingRegistry = exports.publishKnowledgeEvent = exports.publishAgentEvent = exports.publishWorkflowEvent = exports.publishActivityEvent = exports.publishEscalationEvent = exports.publishTaskEvent = exports.publishMilestoneEvent = exports.SocketIOEventAdapter = exports.InMemoryEventAdapter = exports.NatsEventAdapter = exports.eventRegistry = exports.ltConfig = exports.seedConfigTopics = exports.seedSystemTopics = exports.TopicService = exports.displayGtin = exports.normalizeGtin = exports.isValidGtin = exports.assertValidScanIdentityRule = exports.assertValidScanSteps = exports.assertValidScanScheme = exports.UserService = exports.ConfigService = exports.EscalationService = exports.TaskService = exports.setAuthAdapter = exports.signToken = exports.requireAdmin = exports.requireAuth = exports.createAuthMiddleware = exports.JwtAuthAdapter = exports.conditionalAccumulator = exports.conditionLT = exports.conditional = exports.executeLT = exports.createLTActivityInterceptor = exports.createLTInterceptor = exports.registerLT = exports.midnightTheme = exports.start = void 0;
|
|
40
|
+
exports.LTExpressAdapter = exports.Machines = exports.api = exports.KnowledgeApi = exports.resolveCredential = exports.mimeFromPath = exports.getStorageBackend = exports.listOAuthConnections = exports.getFreshAccessToken = exports.createClient = exports.CallbackEventAdapter = exports.builtinMcpServerFactories = exports.getSystemAgents = exports.getSystemWorkers = exports.registerMcpTool = exports.getToolContext = exports.getActivityIdentity = exports.McpEscalationStrategy = exports.DefaultEscalationStrategy = exports.escalationStrategyRegistry = exports.McpVisionServer = exports.McpTranslationServer = exports.McpServer = exports.McpClient = exports.McpService = exports.BuiltInMcpAdapter = exports.mcpRegistry = exports.defaultMaintenanceConfig = exports.maintenanceRegistry = void 0;
|
|
41
41
|
const config_1 = require("./modules/config");
|
|
42
42
|
const logger_1 = require("./lib/logger");
|
|
43
43
|
const midnight_1 = require("./themes/midnight");
|
|
44
|
+
const path = __importStar(require("path"));
|
|
44
45
|
const start_1 = require("./start");
|
|
45
46
|
const line_1 = require("./lib/machines/line");
|
|
46
47
|
// ─── Package Exports ─────────────────────────────────────────────────────────
|
|
@@ -100,6 +101,16 @@ Object.defineProperty(exports, "publishActivityEvent", { enumerable: true, get:
|
|
|
100
101
|
Object.defineProperty(exports, "publishWorkflowEvent", { enumerable: true, get: function () { return publish_1.publishWorkflowEvent; } });
|
|
101
102
|
Object.defineProperty(exports, "publishAgentEvent", { enumerable: true, get: function () { return publish_1.publishAgentEvent; } });
|
|
102
103
|
Object.defineProperty(exports, "publishKnowledgeEvent", { enumerable: true, get: function () { return publish_1.publishKnowledgeEvent; } });
|
|
104
|
+
var messaging_1 = require("./lib/messaging");
|
|
105
|
+
Object.defineProperty(exports, "messagingRegistry", { enumerable: true, get: function () { return messaging_1.messagingRegistry; } });
|
|
106
|
+
Object.defineProperty(exports, "MemoryMessagingProvider", { enumerable: true, get: function () { return messaging_1.MemoryMessagingProvider; } });
|
|
107
|
+
Object.defineProperty(exports, "DELIVERY", { enumerable: true, get: function () { return messaging_1.DELIVERY; } });
|
|
108
|
+
var provider_1 = require("./lib/messaging/nats/provider");
|
|
109
|
+
Object.defineProperty(exports, "NatsMessagingProvider", { enumerable: true, get: function () { return provider_1.NatsMessagingProvider; } });
|
|
110
|
+
var render_conf_1 = require("./lib/messaging/nats/render-conf");
|
|
111
|
+
Object.defineProperty(exports, "renderNatsConf", { enumerable: true, get: function () { return render_conf_1.renderNatsConf; } });
|
|
112
|
+
var permissions_1 = require("./lib/messaging/nats/permissions");
|
|
113
|
+
Object.defineProperty(exports, "jsPermissions", { enumerable: true, get: function () { return permissions_1.jsPermissions; } });
|
|
103
114
|
var telemetry_1 = require("./lib/telemetry");
|
|
104
115
|
Object.defineProperty(exports, "telemetryRegistry", { enumerable: true, get: function () { return telemetry_1.telemetryRegistry; } });
|
|
105
116
|
var honeycomb_1 = require("./lib/telemetry/honeycomb");
|
|
@@ -257,7 +268,7 @@ async function main() {
|
|
|
257
268
|
// Machines resolve rows over MQTT when a line and a transport are configured
|
|
258
269
|
// (docker-compose.machines.yml sets both).
|
|
259
270
|
machines: process.env.MACHINE_TRANSPORT && process.env.MACHINE_LINE_CONFIG
|
|
260
|
-
?
|
|
271
|
+
? machinesFromEnv(process.env.MACHINE_LINE_CONFIG, process.env.MACHINE_CODEC_MODULE)
|
|
261
272
|
: undefined,
|
|
262
273
|
branding: {
|
|
263
274
|
...(process.env.WHITE_LABEL ? { appName: process.env.WHITE_LABEL } : {}),
|
|
@@ -266,11 +277,22 @@ async function main() {
|
|
|
266
277
|
themes: [midnight_1.midnightTheme],
|
|
267
278
|
},
|
|
268
279
|
telemetry: honeycombKey ? { honeycomb: { apiKey: honeycombKey } } : undefined,
|
|
269
|
-
|
|
270
|
-
|
|
280
|
+
// Messaging from LT_MESSAGING / LT_NATS_* when set; otherwise the older EVENT_TRANSPORT=nats with NATS_*.
|
|
281
|
+
messaging: !process.env.LT_MESSAGING && config_1.config.EVENT_TRANSPORT === 'nats'
|
|
282
|
+
? { nats: { url: config_1.config.NATS_URL, token: config_1.config.NATS_TOKEN || undefined, ws: { url: config_1.config.NATS_WS_URL || undefined } } }
|
|
271
283
|
: undefined,
|
|
272
284
|
});
|
|
273
285
|
}
|
|
286
|
+
/** The configured line, spoken to by the codec MACHINE_CODEC_MODULE names (its `createCodec(line)`), or the generic one. */
|
|
287
|
+
function machinesFromEnv(lineConfig, codecModule) {
|
|
288
|
+
const line = (0, line_1.loadLine)(lineConfig);
|
|
289
|
+
if (!codecModule)
|
|
290
|
+
return { lines: [line] };
|
|
291
|
+
const { createCodec } = require(path.resolve(codecModule));
|
|
292
|
+
if (typeof createCodec !== 'function')
|
|
293
|
+
throw new Error(`MACHINE_CODEC_MODULE=${codecModule} exports no createCodec(line)`);
|
|
294
|
+
return { lines: [line], codec: createCodec(line) };
|
|
295
|
+
}
|
|
274
296
|
// Run server when executed directly
|
|
275
297
|
if (require.main === module) {
|
|
276
298
|
require('dotenv').config();
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { LTEvent, LTEventAdapter } from '../../types';
|
|
2
|
+
import type { LTMessagingProvider } from '../../types/messaging';
|
|
3
|
+
/** The part of Long Tail's messaging the adapter uses. */
|
|
4
|
+
export type EventMessaging = Pick<LTMessagingProvider, 'connected' | 'pubsub'>;
|
|
2
5
|
/**
|
|
3
6
|
* NATS event adapter — publish AND subscribe.
|
|
4
7
|
*
|
|
@@ -11,6 +14,9 @@ import type { LTEvent, LTEventAdapter } from '../../types';
|
|
|
11
14
|
* An `_originId` field prevents the publishing container from
|
|
12
15
|
* re-dispatching its own events.
|
|
13
16
|
*
|
|
17
|
+
* Given `messaging` (Long Tail's messaging, `start({ messaging })`), it publishes
|
|
18
|
+
* and bridges over that connection; otherwise it opens a connection of its own.
|
|
19
|
+
*
|
|
14
20
|
* Usage:
|
|
15
21
|
* ```typescript
|
|
16
22
|
* import { eventRegistry, NatsEventAdapter } from '@hotmeshio/long-tail';
|
|
@@ -22,6 +28,8 @@ import type { LTEvent, LTEventAdapter } from '../../types';
|
|
|
22
28
|
export declare class NatsEventAdapter implements LTEventAdapter {
|
|
23
29
|
private nc;
|
|
24
30
|
private sub;
|
|
31
|
+
private readonly messaging;
|
|
32
|
+
private bridge;
|
|
25
33
|
private url;
|
|
26
34
|
private _wsUrl;
|
|
27
35
|
private _wsProxyTarget;
|
|
@@ -36,6 +44,7 @@ export declare class NatsEventAdapter implements LTEventAdapter {
|
|
|
36
44
|
wsProxy?: string;
|
|
37
45
|
subjectPrefix?: string;
|
|
38
46
|
token?: string;
|
|
47
|
+
messaging?: EventMessaging;
|
|
39
48
|
});
|
|
40
49
|
/**
|
|
41
50
|
* Public NATS WebSocket URL for browser connections.
|
|
@@ -59,6 +68,8 @@ export declare class NatsEventAdapter implements LTEventAdapter {
|
|
|
59
68
|
*/
|
|
60
69
|
setCallbackBridge(adapter: LTEventAdapter): void;
|
|
61
70
|
connect(): Promise<void>;
|
|
71
|
+
/** An event from another container, handed to the local callback adapter. */
|
|
72
|
+
private bridgeIn;
|
|
62
73
|
publish(event: LTEvent): Promise<void>;
|
|
63
74
|
disconnect(): Promise<void>;
|
|
64
75
|
}
|
package/build/lib/events/nats.js
CHANGED
|
@@ -18,6 +18,9 @@ const sc = (0, nats_1.StringCodec)();
|
|
|
18
18
|
* An `_originId` field prevents the publishing container from
|
|
19
19
|
* re-dispatching its own events.
|
|
20
20
|
*
|
|
21
|
+
* Given `messaging` (Long Tail's messaging, `start({ messaging })`), it publishes
|
|
22
|
+
* and bridges over that connection; otherwise it opens a connection of its own.
|
|
23
|
+
*
|
|
21
24
|
* Usage:
|
|
22
25
|
* ```typescript
|
|
23
26
|
* import { eventRegistry, NatsEventAdapter } from '@hotmeshio/long-tail';
|
|
@@ -30,9 +33,11 @@ class NatsEventAdapter {
|
|
|
30
33
|
constructor(options) {
|
|
31
34
|
this.nc = null;
|
|
32
35
|
this.sub = null;
|
|
36
|
+
this.bridge = null;
|
|
33
37
|
this._wsProxyBasePath = '';
|
|
34
38
|
this.originId = (0, crypto_1.randomUUID)();
|
|
35
39
|
this.callbackAdapter = null;
|
|
40
|
+
this.messaging = options?.messaging ?? null;
|
|
36
41
|
this.url = options?.url || config_1.config.NATS_URL;
|
|
37
42
|
this._wsUrl = options?.wsUrl || config_1.config.NATS_WS_URL || null;
|
|
38
43
|
this._wsProxyTarget = options?.wsProxy || null;
|
|
@@ -75,6 +80,11 @@ class NatsEventAdapter {
|
|
|
75
80
|
this.callbackAdapter = adapter;
|
|
76
81
|
}
|
|
77
82
|
async connect() {
|
|
83
|
+
if (this.messaging) {
|
|
84
|
+
if (this.callbackAdapter)
|
|
85
|
+
this.bridge = this.messaging.pubsub.subscribe(`${this.subjectPrefix}.>`, (m) => this.bridgeIn(m));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
78
88
|
this.nc = await (0, nats_1.connect)({
|
|
79
89
|
servers: this.url,
|
|
80
90
|
token: this.token,
|
|
@@ -102,15 +112,36 @@ class NatsEventAdapter {
|
|
|
102
112
|
})();
|
|
103
113
|
}
|
|
104
114
|
}
|
|
115
|
+
/** An event from another container, handed to the local callback adapter. */
|
|
116
|
+
bridgeIn(m) {
|
|
117
|
+
try {
|
|
118
|
+
const event = JSON.parse(m.payload);
|
|
119
|
+
if (event._originId === this.originId)
|
|
120
|
+
return;
|
|
121
|
+
this.callbackAdapter?.publish(event);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// Malformed message, skip
|
|
125
|
+
}
|
|
126
|
+
}
|
|
105
127
|
async publish(event) {
|
|
106
|
-
if (!this.nc)
|
|
107
|
-
return;
|
|
108
128
|
const subject = `${this.subjectPrefix}.${event.type}`;
|
|
109
129
|
const enriched = { ...event, _originId: this.originId };
|
|
130
|
+
if (this.messaging) {
|
|
131
|
+
if (this.messaging.connected)
|
|
132
|
+
await this.messaging.pubsub.publish(subject, JSON.stringify(enriched));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (!this.nc)
|
|
136
|
+
return;
|
|
110
137
|
this.nc.publish(subject, sc.encode(JSON.stringify(enriched)));
|
|
111
138
|
logger_1.loggerRegistry.info(`[lt-events:nats] published: ${subject} (origin=${this.originId.slice(0, 8)})`);
|
|
112
139
|
}
|
|
113
140
|
async disconnect() {
|
|
141
|
+
if (this.bridge) {
|
|
142
|
+
await this.bridge.close();
|
|
143
|
+
this.bridge = null;
|
|
144
|
+
}
|
|
114
145
|
if (this.sub) {
|
|
115
146
|
this.sub.unsubscribe();
|
|
116
147
|
this.sub = null;
|
|
@@ -18,4 +18,5 @@ __exportStar(require("./protocol"), exports);
|
|
|
18
18
|
__exportStar(require("./transport"), exports);
|
|
19
19
|
__exportStar(require("./line"), exports);
|
|
20
20
|
__exportStar(require("./site/site-conf"), exports);
|
|
21
|
+
__exportStar(require("./site/edge-account"), exports);
|
|
21
22
|
__exportStar(require("./site/mosquitto-conf"), exports);
|
|
@@ -16,6 +16,10 @@ export interface MachineCommand {
|
|
|
16
16
|
cmdId: string;
|
|
17
17
|
op: string;
|
|
18
18
|
args: Record<string, unknown>;
|
|
19
|
+
/** How many times this gateway has sent this cmdId, this time included. */
|
|
20
|
+
attempt?: number;
|
|
21
|
+
/** The command may have reached the machine before: sent earlier, or its row predates this gateway's takeover. */
|
|
22
|
+
retry?: boolean;
|
|
19
23
|
}
|
|
20
24
|
/** MQTT filters under each machine's topic, e.g. `job` or `alarm/+`. */
|
|
21
25
|
export interface CodecChannels {
|
|
@@ -55,6 +59,18 @@ export interface LineCodec {
|
|
|
55
59
|
* to a person. Otherwise (the default) known fields are acted on and the violations are logged.
|
|
56
60
|
*/
|
|
57
61
|
readonly strict?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* The machines keep no retained state and report only what changes: the gateway asks each one for
|
|
64
|
+
* a snapshot when it takes the line, so it knows every machine's job before acting on a change.
|
|
65
|
+
*/
|
|
66
|
+
readonly snapshotOnStart?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Active alarms stay retained on the broker until they clear, as in the line protocol. A takeover's
|
|
69
|
+
* retained read is then every active alarm, and an alarm row whose alarm is missing from it cleared
|
|
70
|
+
* while no gateway listened. Otherwise a machine's snapshot lists its active alarms, and an alarm it
|
|
71
|
+
* no longer lists has cleared.
|
|
72
|
+
*/
|
|
73
|
+
readonly retainedAlarms?: boolean;
|
|
58
74
|
/** One wire message as zero or more line-protocol messages; empty when the topic is not a machine's. */
|
|
59
75
|
decode(topic: string, payload: string, retained?: boolean): Inspection[];
|
|
60
76
|
/** The command as wire messages; none when the machines have no such command, and the gateway then sends nothing. */
|
|
@@ -66,6 +66,7 @@ function genericCodec(opts) {
|
|
|
66
66
|
channels: exports.GENERIC_CHANNELS,
|
|
67
67
|
unitsPerDispatch: opts.unitsPerDispatch ?? null,
|
|
68
68
|
dispatchOn: opts.dispatchOn ?? exports.DISPATCH_ON.CREATED,
|
|
69
|
+
retainedAlarms: true,
|
|
69
70
|
decode(topic, payload, retained) {
|
|
70
71
|
const seen = atMachine((0, contract_1.inspectMessage)(opts.root, topic, payload, retained), ids);
|
|
71
72
|
if (!seen.parsed)
|
|
@@ -51,7 +51,8 @@ const BODY = {
|
|
|
51
51
|
},
|
|
52
52
|
materials: { type: 'array', items: shape({ material: STR, lot: STR }) },
|
|
53
53
|
startedAt: TS, endedAt: TS, durationMs: INT0, failure: oneOf(constants_1.TRANSFER_FAILURE),
|
|
54
|
-
|
|
54
|
+
percent: { type: 'number', minimum: 0, maximum: 100 }, remainingSec: INT0, stage: STR, detail: { type: 'object' },
|
|
55
|
+
}, ['jobId', 'unitId', 'peer', 'result', 'reason', 'measurements', 'materials', 'startedAt', 'endedAt', 'durationMs', 'failure', 'percent', 'remainingSec', 'stage', 'detail']),
|
|
55
56
|
[topics_1.MESSAGE_KIND.ALARM]: shape({
|
|
56
57
|
alarmId: STR, code: STR, level: oneOf(constants_1.LEVEL), text: STR, active: BOOL,
|
|
57
58
|
raisedAt: TS, clearedAt: TS_N, clearedBy: STR_N, ackedBy: STR_N, ackedAt: TS_N,
|
|
@@ -92,6 +93,8 @@ function inspectMessage(root, topic, raw, retained) {
|
|
|
92
93
|
violations.push(...crossFieldRules(parsed, body, retained));
|
|
93
94
|
return { parsed, body, violations };
|
|
94
95
|
}
|
|
96
|
+
/** Job events a receiver may report naming only the carrier it scanned. */
|
|
97
|
+
const CARRIER_EVENTS = [constants_1.JOB_EVENT.ARRIVED, constants_1.JOB_EVENT.TRANSFER_FAILED];
|
|
95
98
|
function crossFieldRules(parsed, env, retained) {
|
|
96
99
|
const out = [];
|
|
97
100
|
const b = env.body ?? {};
|
|
@@ -102,6 +105,12 @@ function crossFieldRules(parsed, env, retained) {
|
|
|
102
105
|
if (kind === topics_1.MESSAGE_KIND.ALARM && b.alarmId !== key)
|
|
103
106
|
out.push('alarmId disagrees with the topic');
|
|
104
107
|
if (kind === topics_1.MESSAGE_KIND.JOB) {
|
|
108
|
+
// A scan at a receiver may read only the carrier; every other job event names its unit and job.
|
|
109
|
+
const byCarrier = CARRIER_EVENTS.includes(b.event) && typeof b.carrierId === 'string';
|
|
110
|
+
if (b.unitId === undefined && !byCarrier)
|
|
111
|
+
out.push(`${b.event} without a unitId`);
|
|
112
|
+
if (b.jobId === undefined && !byCarrier)
|
|
113
|
+
out.push(`${b.event} without a jobId`);
|
|
105
114
|
if (b.event === constants_1.JOB_EVENT.FINISHED) {
|
|
106
115
|
for (const field of ['result', 'measurements', 'materials', 'startedAt', 'endedAt']) {
|
|
107
116
|
if (!(field in b))
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A line's edge account on Long Tail's NATS: the machine broker's bridge logs
|
|
3
|
+
* in by client certificate (the CN is its user), over MQTT only, and may
|
|
4
|
+
* publish under the line's topics and subscribe to nothing. Long Tail imports
|
|
5
|
+
* exactly those subjects.
|
|
6
|
+
*/
|
|
7
|
+
import type { EdgeAccount } from '../../messaging/nats/render-conf';
|
|
8
|
+
import type { LineDef } from '../line';
|
|
9
|
+
/** An account name for a line: letters, digits, `-` and `_`. */
|
|
10
|
+
export declare function edgeAccountName(line: LineDef): string;
|
|
11
|
+
export declare function edgeAccountFor(line: LineDef, root: string, bridgeUser?: string): EdgeAccount;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* A line's edge account on Long Tail's NATS: the machine broker's bridge logs
|
|
4
|
+
* in by client certificate (the CN is its user), over MQTT only, and may
|
|
5
|
+
* publish under the line's topics and subscribe to nothing. Long Tail imports
|
|
6
|
+
* exactly those subjects.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.edgeAccountName = edgeAccountName;
|
|
10
|
+
exports.edgeAccountFor = edgeAccountFor;
|
|
11
|
+
const protocol_1 = require("../protocol");
|
|
12
|
+
const site_conf_1 = require("./site-conf");
|
|
13
|
+
/** An account name for a line: letters, digits, `-` and `_`. */
|
|
14
|
+
function edgeAccountName(line) {
|
|
15
|
+
return `EDGE_${line.siteId}_${line.lineId}`.replace(/[^A-Za-z0-9_-]/g, '_');
|
|
16
|
+
}
|
|
17
|
+
function edgeAccountFor(line, root, bridgeUser = `CN=${(0, site_conf_1.bridgeId)(line)}`) {
|
|
18
|
+
const subjects = `${(0, protocol_1.toSubject)(`${root}/${line.siteId}/${line.lineId}`)}.>`;
|
|
19
|
+
return {
|
|
20
|
+
name: edgeAccountName(line),
|
|
21
|
+
subjects,
|
|
22
|
+
users: [{ user: bridgeUser, allowed_connection_types: ['MQTT'], permissions: { publish: [subjects], subscribe: { deny: ['>'] } } }],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
* The site's NATS server, rendered from the line. It runs JetStream and an
|
|
3
3
|
* MQTT listener that only the machine broker's bridge logs in to (by client
|
|
4
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
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* topics). The gateway logs in by password and may only keep and read the
|
|
6
|
+
* line's streams through its own consumer (it can neither publish a machine
|
|
7
|
+
* subject nor delete a stream), and subscribe to its inbox and the line. The
|
|
8
|
+
* site NATS stands alone; Long Tail's escalation events stay inside Long Tail.
|
|
8
9
|
*/
|
|
9
|
-
import type
|
|
10
|
+
import { type LineDef } from '../line';
|
|
10
11
|
export interface SiteConfOptions {
|
|
11
12
|
root: string;
|
|
12
13
|
gatewayUser: string;
|
|
@@ -17,4 +18,6 @@ export interface SiteConfOptions {
|
|
|
17
18
|
}
|
|
18
19
|
/** The machine broker's bridge identity for a line. */
|
|
19
20
|
export declare function bridgeId(line: LineDef): string;
|
|
21
|
+
/** What the gateway may publish: its streams' and its consumer's JetStream requests and acks, never a machine subject. */
|
|
22
|
+
export declare function gatewayPublish(line: LineDef): string[];
|
|
20
23
|
export declare function renderSiteConf(line: LineDef, opts: SiteConfOptions): string;
|
|
@@ -3,21 +3,28 @@
|
|
|
3
3
|
* The site's NATS server, rendered from the line. It runs JetStream and an
|
|
4
4
|
* MQTT listener that only the machine broker's bridge logs in to (by client
|
|
5
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
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* topics). The gateway logs in by password and may only keep and read the
|
|
7
|
+
* line's streams through its own consumer (it can neither publish a machine
|
|
8
|
+
* subject nor delete a stream), and subscribe to its inbox and the line. The
|
|
9
|
+
* site NATS stands alone; Long Tail's escalation events stay inside Long Tail.
|
|
9
10
|
*/
|
|
10
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
12
|
exports.bridgeId = bridgeId;
|
|
13
|
+
exports.gatewayPublish = gatewayPublish;
|
|
12
14
|
exports.renderSiteConf = renderSiteConf;
|
|
15
|
+
const line_1 = require("../line");
|
|
13
16
|
const protocol_1 = require("../protocol");
|
|
17
|
+
const permissions_1 = require("../../messaging/nats/permissions");
|
|
18
|
+
const streams_1 = require("../transport/streams");
|
|
14
19
|
const q = JSON.stringify;
|
|
15
20
|
/** The machine broker's bridge identity for a line. */
|
|
16
21
|
function bridgeId(line) {
|
|
17
22
|
return `${line.siteId}-${line.lineId}-bridge`;
|
|
18
23
|
}
|
|
19
|
-
/** What the gateway may publish: JetStream requests
|
|
20
|
-
|
|
24
|
+
/** What the gateway may publish: its streams' and its consumer's JetStream requests and acks, never a machine subject. */
|
|
25
|
+
function gatewayPublish(line) {
|
|
26
|
+
return (0, permissions_1.jsPermissions)({ streams: Object.values(streams_1.LINE_STREAM), durable: `${(0, line_1.controllerId)(line)}-events` }).publish;
|
|
27
|
+
}
|
|
21
28
|
function renderSiteConf(line, opts) {
|
|
22
29
|
const base = (0, protocol_1.toSubject)(`${opts.root}/${line.siteId}/${line.lineId}`);
|
|
23
30
|
const bridge = [
|
|
@@ -26,7 +33,7 @@ function renderSiteConf(line, opts) {
|
|
|
26
33
|
].join('\n');
|
|
27
34
|
const gateway = [
|
|
28
35
|
` { user: ${q(opts.gatewayUser)}, password: ${q(opts.gatewayPassword)}, allowed_connection_types: ["STANDARD"],`,
|
|
29
|
-
` permissions: { publish: ${q(
|
|
36
|
+
` permissions: { publish: ${q(gatewayPublish(line))}, subscribe: ${q(['_INBOX.>', `${base}.>`])} } }`,
|
|
30
37
|
].join('\n');
|
|
31
38
|
return `# Rendered from the line definition by site/setup.ts. Do not edit by hand.
|
|
32
39
|
server_name: ${q(`${line.siteId}-${line.lineId}`)}
|
|
@@ -4,17 +4,16 @@
|
|
|
4
4
|
* own commands. The gateway holds a MachineBus: durable, ordered delivery of
|
|
5
5
|
* every machine message on the line, QoS 1 command egress, and retained reads.
|
|
6
6
|
*/
|
|
7
|
+
import type { Delivery } from '../../../types/messaging';
|
|
7
8
|
import type { EscalationEvent, EscalationFeed, InboundMessage, LinkConnectOptions, LinkWill, MachineBus, MachineLink, PublishOptions } from '../../../types/machines';
|
|
8
9
|
export type { EscalationEvent, EscalationFeed, InboundMessage, LinkConnectOptions, LinkWill, MachineBus, MachineLink, PublishOptions };
|
|
10
|
+
/** What the gateway does with a machine message: the messaging capability's deliveries (nak: its row does not exist yet). */
|
|
9
11
|
export declare const DISPOSITION: {
|
|
10
|
-
/** Handled; do not deliver again. */
|
|
11
12
|
readonly ACK: "ack";
|
|
12
|
-
/** Not yet actionable (its row does not exist yet); deliver again later. */
|
|
13
13
|
readonly NAK: "nak";
|
|
14
|
-
/** Never actionable; drop it. */
|
|
15
14
|
readonly TERM: "term";
|
|
16
15
|
};
|
|
17
|
-
export type Disposition =
|
|
16
|
+
export type Disposition = Delivery;
|
|
18
17
|
/** MQTT filter match with `+` (one level) and `#` (the rest). */
|
|
19
18
|
export declare function topicMatches(filter: string, topic: string): boolean;
|
|
20
19
|
/** Read an LTEvent body into an EscalationEvent; null for other families. */
|
|
@@ -9,14 +9,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
9
9
|
exports.DISPOSITION = void 0;
|
|
10
10
|
exports.topicMatches = topicMatches;
|
|
11
11
|
exports.toEscalationEvent = toEscalationEvent;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
};
|
|
12
|
+
const constants_1 = require("../../messaging/constants");
|
|
13
|
+
/** What the gateway does with a machine message: the messaging capability's deliveries (nak: its row does not exist yet). */
|
|
14
|
+
exports.DISPOSITION = constants_1.DELIVERY;
|
|
20
15
|
/** MQTT filter match with `+` (one level) and `#` (the rest). */
|
|
21
16
|
function topicMatches(filter, topic) {
|
|
22
17
|
const f = filter.split('/');
|
|
@@ -6,6 +6,8 @@ export * from './streams';
|
|
|
6
6
|
export * from './mqtt-session';
|
|
7
7
|
export * from './mqtt-link';
|
|
8
8
|
export * from './mqtt-bus';
|
|
9
|
+
export * from './stream-bus';
|
|
9
10
|
export * from './nats-bus';
|
|
10
11
|
export * from './nats-feed';
|
|
12
|
+
export * from './pubsub-feed';
|
|
11
13
|
export * from './callback-feed';
|
|
@@ -22,6 +22,8 @@ __exportStar(require("./streams"), exports);
|
|
|
22
22
|
__exportStar(require("./mqtt-session"), exports);
|
|
23
23
|
__exportStar(require("./mqtt-link"), exports);
|
|
24
24
|
__exportStar(require("./mqtt-bus"), exports);
|
|
25
|
+
__exportStar(require("./stream-bus"), exports);
|
|
25
26
|
__exportStar(require("./nats-bus"), exports);
|
|
26
27
|
__exportStar(require("./nats-feed"), exports);
|
|
28
|
+
__exportStar(require("./pubsub-feed"), exports);
|
|
27
29
|
__exportStar(require("./callback-feed"), exports);
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The gateway on
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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.
|
|
2
|
+
* The gateway on a NATS server of the site's own (MACHINE_SITE_NATS_*): the
|
|
3
|
+
* stream bus on a NATS messaging provider this bus connects and closes itself.
|
|
4
|
+
* With Long Tail's messaging configured, the stream bus runs on that instead.
|
|
7
5
|
*/
|
|
8
6
|
import type { MqttEndpoint } from '../../../services/machines/config';
|
|
9
|
-
import {
|
|
7
|
+
import type { StreamCodec } from './streams';
|
|
10
8
|
import type { Disposition, InboundMessage, MachineBus } from './delivery';
|
|
11
9
|
export interface NatsBusOptions {
|
|
12
10
|
natsUrl: string;
|
|
@@ -23,11 +21,8 @@ export interface NatsBusOptions {
|
|
|
23
21
|
}
|
|
24
22
|
export declare class NatsMachineBus implements MachineBus {
|
|
25
23
|
private readonly opts;
|
|
26
|
-
private
|
|
27
|
-
private
|
|
28
|
-
private messages?;
|
|
29
|
-
private telemetry;
|
|
30
|
-
private readonly queue;
|
|
24
|
+
private readonly provider;
|
|
25
|
+
private bus?;
|
|
31
26
|
constructor(opts: NatsBusOptions);
|
|
32
27
|
get durableName(): string;
|
|
33
28
|
connect(): Promise<void>;
|
|
@@ -37,6 +32,5 @@ export declare class NatsMachineBus implements MachineBus {
|
|
|
37
32
|
}): Promise<void>;
|
|
38
33
|
retained(filter: string): Promise<InboundMessage[]>;
|
|
39
34
|
close(): Promise<void>;
|
|
40
|
-
private
|
|
41
|
-
private mqtt;
|
|
35
|
+
private stream;
|
|
42
36
|
}
|
|
@@ -1,67 +1,47 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* The gateway on
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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.
|
|
3
|
+
* The gateway on a NATS server of the site's own (MACHINE_SITE_NATS_*): the
|
|
4
|
+
* stream bus on a NATS messaging provider this bus connects and closes itself.
|
|
5
|
+
* With Long Tail's messaging configured, the stream bus runs on that instead.
|
|
8
6
|
*/
|
|
9
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
8
|
exports.NatsMachineBus = void 0;
|
|
11
|
-
const
|
|
12
|
-
const
|
|
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");
|
|
9
|
+
const provider_1 = require("../../messaging/nats/provider");
|
|
10
|
+
const stream_bus_1 = require("./stream-bus");
|
|
17
11
|
class NatsMachineBus {
|
|
18
12
|
constructor(opts) {
|
|
19
13
|
this.opts = opts;
|
|
20
|
-
this.
|
|
21
|
-
this.queue = new serial_1.SerialQueue();
|
|
14
|
+
this.provider = new provider_1.NatsMessagingProvider({ url: opts.natsUrl, user: opts.user, password: opts.password, name: opts.controllerId });
|
|
22
15
|
}
|
|
23
16
|
get durableName() {
|
|
24
17
|
return `${this.opts.controllerId}-events`;
|
|
25
18
|
}
|
|
26
19
|
async connect() {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
this.
|
|
20
|
+
await this.provider.connect();
|
|
21
|
+
const streams = this.provider.streams;
|
|
22
|
+
if (!streams)
|
|
23
|
+
throw new Error(`the site NATS at ${this.opts.natsUrl} has no JetStream for the gateway`);
|
|
24
|
+
const { natsUrl: _url, user: _user, password: _password, ...rest } = this.opts;
|
|
25
|
+
this.bus = new stream_bus_1.StreamMachineBus({ ...rest, streams, pubsub: this.provider.pubsub });
|
|
26
|
+
await this.bus.connect();
|
|
33
27
|
}
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
}));
|
|
28
|
+
consume(handler) {
|
|
29
|
+
return this.stream().consume(handler);
|
|
41
30
|
}
|
|
42
|
-
|
|
43
|
-
|
|
31
|
+
publish(topic, payload, opts) {
|
|
32
|
+
return this.stream().publish(topic, payload, opts);
|
|
44
33
|
}
|
|
45
34
|
retained(filter) {
|
|
46
|
-
return this.
|
|
35
|
+
return this.stream().retained(filter);
|
|
47
36
|
}
|
|
48
37
|
async close() {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
await this.messages?.close();
|
|
52
|
-
await this.queue.drained();
|
|
53
|
-
await this.session?.close(true);
|
|
54
|
-
await this.nc?.drain();
|
|
38
|
+
await this.bus?.close();
|
|
39
|
+
await this.provider.disconnect();
|
|
55
40
|
}
|
|
56
|
-
|
|
57
|
-
if (!this.
|
|
41
|
+
stream() {
|
|
42
|
+
if (!this.bus)
|
|
58
43
|
throw new Error('NATS bus used before connect()');
|
|
59
|
-
return this.
|
|
60
|
-
}
|
|
61
|
-
mqtt() {
|
|
62
|
-
if (!this.session)
|
|
63
|
-
throw new Error('NATS bus used before connect()');
|
|
64
|
-
return this.session;
|
|
44
|
+
return this.bus;
|
|
65
45
|
}
|
|
66
46
|
}
|
|
67
47
|
exports.NatsMachineBus = NatsMachineBus;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
exports.CoreEscalationFeed = exports.LT_ESCALATION_SUBJECTS = void 0;
|
|
10
10
|
const nats_1 = require("nats");
|
|
11
|
-
const
|
|
11
|
+
const streams_1 = require("../../messaging/nats/streams");
|
|
12
12
|
const serial_1 = require("./serial");
|
|
13
13
|
const delivery_1 = require("./delivery");
|
|
14
14
|
/** Long Tail publishes every LTEvent as `lt.events.<type>`. */
|
|
@@ -34,7 +34,7 @@ class CoreEscalationFeed {
|
|
|
34
34
|
throw new Error('escalation feed used before connect()');
|
|
35
35
|
this.sub = this.nc.subscribe(exports.LT_ESCALATION_SUBJECTS, {
|
|
36
36
|
callback: (_err, m) => {
|
|
37
|
-
const event = decode((0,
|
|
37
|
+
const event = decode((0, streams_1.payloadOf)(m));
|
|
38
38
|
if (event)
|
|
39
39
|
void this.redelivery.deliver(() => handler(event));
|
|
40
40
|
},
|