@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.
Files changed (91) hide show
  1. package/build/index.d.ts +4 -0
  2. package/build/index.js +30 -8
  3. package/build/lib/events/nats.d.ts +11 -0
  4. package/build/lib/events/nats.js +33 -2
  5. package/build/lib/machines/index.d.ts +1 -0
  6. package/build/lib/machines/index.js +1 -0
  7. package/build/lib/machines/protocol/codec.d.ts +16 -0
  8. package/build/lib/machines/protocol/codec.js +1 -0
  9. package/build/lib/machines/protocol/contract.js +10 -1
  10. package/build/lib/machines/site/edge-account.d.ts +11 -0
  11. package/build/lib/machines/site/edge-account.js +24 -0
  12. package/build/lib/machines/site/site-conf.d.ts +7 -4
  13. package/build/lib/machines/site/site-conf.js +13 -6
  14. package/build/lib/machines/transport/delivery.d.ts +3 -4
  15. package/build/lib/machines/transport/delivery.js +3 -8
  16. package/build/lib/machines/transport/index.d.ts +2 -0
  17. package/build/lib/machines/transport/index.js +2 -0
  18. package/build/lib/machines/transport/nats-bus.d.ts +7 -13
  19. package/build/lib/machines/transport/nats-bus.js +23 -43
  20. package/build/lib/machines/transport/nats-feed.js +2 -2
  21. package/build/lib/machines/transport/pubsub-feed.d.ts +17 -0
  22. package/build/lib/machines/transport/pubsub-feed.js +39 -0
  23. package/build/lib/machines/transport/stream-bus.d.ts +40 -0
  24. package/build/lib/machines/transport/stream-bus.js +57 -0
  25. package/build/lib/machines/transport/streams.d.ts +10 -14
  26. package/build/lib/machines/transport/streams.js +18 -61
  27. package/build/lib/messaging/config.d.ts +25 -0
  28. package/build/lib/messaging/config.js +55 -0
  29. package/build/lib/messaging/constants.d.ts +26 -0
  30. package/build/lib/messaging/constants.js +26 -0
  31. package/build/lib/messaging/index.d.ts +24 -0
  32. package/build/lib/messaging/index.js +65 -0
  33. package/build/lib/messaging/memory.d.ts +16 -0
  34. package/build/lib/messaging/memory.js +147 -0
  35. package/build/lib/messaging/nats/permissions.d.ts +13 -0
  36. package/build/lib/messaging/nats/permissions.js +22 -0
  37. package/build/lib/messaging/nats/provider.d.ts +30 -0
  38. package/build/lib/messaging/nats/provider.js +77 -0
  39. package/build/lib/messaging/nats/render-conf.d.ts +68 -0
  40. package/build/lib/messaging/nats/render-conf.js +58 -0
  41. package/build/lib/messaging/nats/streams.d.ts +26 -0
  42. package/build/lib/messaging/nats/streams.js +97 -0
  43. package/build/lib/messaging/subjects.d.ts +8 -0
  44. package/build/lib/messaging/subjects.js +25 -0
  45. package/build/scripts/bambu-boat.d.ts +17 -0
  46. package/build/scripts/bambu-boat.js +134 -0
  47. package/build/scripts/bambu-print.d.ts +18 -0
  48. package/build/scripts/bambu-print.js +114 -0
  49. package/build/services/machines/config.d.ts +6 -0
  50. package/build/services/machines/config.js +16 -6
  51. package/build/services/machines/contracts.d.ts +19 -2
  52. package/build/services/machines/contracts.js +12 -3
  53. package/build/services/machines/gateway/actions.d.ts +1 -1
  54. package/build/services/machines/gateway/dispatcher.d.ts +16 -3
  55. package/build/services/machines/gateway/dispatcher.js +50 -10
  56. package/build/services/machines/gateway/executor.d.ts +7 -2
  57. package/build/services/machines/gateway/executor.js +35 -9
  58. package/build/services/machines/gateway/gateway.d.ts +6 -0
  59. package/build/services/machines/gateway/gateway.js +33 -10
  60. package/build/services/machines/gateway/lifecycle.d.ts +21 -0
  61. package/build/services/machines/gateway/lifecycle.js +89 -0
  62. package/build/services/machines/gateway/pacing.d.ts +14 -0
  63. package/build/services/machines/gateway/pacing.js +26 -2
  64. package/build/services/machines/gateway/plan-escalation.js +13 -4
  65. package/build/services/machines/gateway/plan-machine.d.ts +5 -4
  66. package/build/services/machines/gateway/plan-machine.js +55 -14
  67. package/build/services/machines/gateway/reconcile.d.ts +2 -0
  68. package/build/services/machines/gateway/reconcile.js +13 -0
  69. package/build/services/machines/gateway/state.d.ts +21 -1
  70. package/build/services/machines/gateway/state.js +16 -3
  71. package/build/services/machines/leadership.d.ts +17 -4
  72. package/build/services/machines/leadership.js +43 -18
  73. package/build/services/machines/rows.js +17 -2
  74. package/build/services/machines/select.d.ts +9 -2
  75. package/build/services/machines/select.js +20 -7
  76. package/build/services/machines/surface.js +8 -2
  77. package/build/services/machines/testing/fake-long-tail.d.ts +1 -0
  78. package/build/services/machines/testing/fake-long-tail.js +6 -3
  79. package/build/start/adapters.js +11 -2
  80. package/build/start/index.js +7 -1
  81. package/build/tsconfig.tsbuildinfo +1 -1
  82. package/build/types/index.d.ts +1 -0
  83. package/build/types/machines.d.ts +2 -1
  84. package/build/types/messaging.d.ts +72 -0
  85. package/build/types/messaging.js +11 -0
  86. package/build/types/startup.d.ts +26 -1
  87. package/docs/events.md +50 -4
  88. package/docs/machines.md +38 -9
  89. package/package.json +3 -1
  90. package/build/lib/machines/transport/nats-durable.d.ts +0 -11
  91. 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.McpClient = exports.McpService = exports.BuiltInMcpAdapter = exports.mcpRegistry = exports.defaultMaintenanceConfig = exports.maintenanceRegistry = exports.PinoLoggerAdapter = exports.loggerRegistry = exports.HoneycombTelemetryAdapter = exports.telemetryRegistry = 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 = 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
- ? { lines: [(0, line_1.loadLine)(process.env.MACHINE_LINE_CONFIG)] }
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
- events: config_1.config.EVENT_TRANSPORT === 'nats'
270
- ? { nats: { url: config_1.config.NATS_URL, wsUrl: config_1.config.NATS_WS_URL || undefined, token: config_1.config.NATS_TOKEN || undefined } }
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
  }
@@ -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;
@@ -2,4 +2,5 @@ export * from './protocol';
2
2
  export * from './transport';
3
3
  export * from './line';
4
4
  export * from './site/site-conf';
5
+ export * from './site/edge-account';
5
6
  export * from './site/mosquitto-conf';
@@ -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
- }, ['peer', 'result', 'reason', 'measurements', 'materials', 'startedAt', 'endedAt', 'durationMs', 'failure']),
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: the JetStream
6
- * API, acks and its inbox, and the line's subjects. The site NATS stands
7
- * alone; Long Tail's escalation events stay inside Long Tail.
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 { LineDef } from '../line';
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: the JetStream
7
- * API, acks and its inbox, and the line's subjects. The site NATS stands
8
- * alone; Long Tail's escalation events stay inside Long Tail.
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, acks and flow control, never a machine subject. */
20
- const GATEWAY_PUBLISH = ['$JS.API.>', '$JS.ACK.>', '$JS.FC.>'];
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(GATEWAY_PUBLISH)}, subscribe: ${q(['_INBOX.>', `${base}.>`])} } }`,
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 = typeof DISPOSITION[keyof typeof 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
- exports.DISPOSITION = {
13
- /** Handled; do not deliver again. */
14
- ACK: 'ack',
15
- /** Not yet actionable (its row does not exist yet); deliver again later. */
16
- NAK: 'nak',
17
- /** Never actionable; drop it. */
18
- TERM: 'term',
19
- };
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 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.
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 { type StreamCodec } from './streams';
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 nc?;
27
- private session?;
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 nats;
41
- private mqtt;
35
+ private stream;
42
36
  }
@@ -1,67 +1,47 @@
1
1
  "use strict";
2
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.
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 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");
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.telemetry = [];
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
- 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);
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
- 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
- }));
28
+ consume(handler) {
29
+ return this.stream().consume(handler);
41
30
  }
42
- async publish(topic, payload, opts = {}) {
43
- await this.mqtt().publish(topic, payload, 1, opts.retain ?? false);
31
+ publish(topic, payload, opts) {
32
+ return this.stream().publish(topic, payload, opts);
44
33
  }
45
34
  retained(filter) {
46
- return this.mqtt().retained(filter);
35
+ return this.stream().retained(filter);
47
36
  }
48
37
  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();
38
+ await this.bus?.close();
39
+ await this.provider.disconnect();
55
40
  }
56
- nats() {
57
- if (!this.nc)
41
+ stream() {
42
+ if (!this.bus)
58
43
  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;
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 nats_durable_1 = require("./nats-durable");
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, nats_durable_1.payloadOf)(m));
37
+ const event = decode((0, streams_1.payloadOf)(m));
38
38
  if (event)
39
39
  void this.redelivery.deliver(() => handler(event));
40
40
  },