@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
@@ -33,6 +33,14 @@ export interface HandoffRow extends HandoffFacets {
33
33
  trace?: TraceEntry[];
34
34
  /** Each unit's arrival event, as its receiver reported it (dispatch on arrival). */
35
35
  arrivals?: Record<string, Record<string, any>>;
36
+ /** Lifecycle item keys already on the row (lifecycle.ts). */
37
+ noted?: string[];
38
+ /** Parked with a limit on its items, which lifecycle notes must never use up. */
39
+ capped?: boolean;
40
+ /** When the row was created (epoch ms). */
41
+ createdAt?: number;
42
+ /** When its last unit arrived (epoch ms, dispatch on arrival); its dispatch windows count from here, else from createdAt. */
43
+ dueAt?: number;
36
44
  }
37
45
  export interface AlarmTrack {
38
46
  machineId: string;
@@ -42,6 +50,8 @@ export interface AlarmTrack {
42
50
  rowId?: string;
43
51
  /** Created and not yet resolved by anyone. */
44
52
  open: boolean;
53
+ /** Rows the alarm paused, noted again when it clears. */
54
+ noted?: string[];
45
55
  }
46
56
  export interface LinkTrack {
47
57
  online: boolean;
@@ -58,6 +68,16 @@ export declare class GatewayState {
58
68
  unitsPerDispatch: number | null;
59
69
  /** Jobs go to a receiver once its units arrive there, from the codec. */
60
70
  dispatchOnArrival: boolean;
71
+ /** Note each job's start, progress steps and pauses on the row that holds it (lifecycle.ts). */
72
+ lifecycle: boolean;
73
+ /** Active alarms are retained on the broker; otherwise snapshots list them (codec.ts retainedAlarms). */
74
+ retainedAlarms: boolean;
75
+ /** Reading the broker's retained messages on taking the line; a snapshot read back may be older than its alarms. */
76
+ readingRetained: boolean;
77
+ /** When this gateway took the line (epoch ms); a command for an older row may have been sent by an earlier one. */
78
+ takenAt: number;
79
+ /** With lifecycle notes, progress is noted each time it crosses this many percent; 0 notes none. */
80
+ progressStep: number;
61
81
  /** While reconciling, new handoffs wait so pacing sees the whole line before anything is sent. */
62
82
  holdDispatch: boolean;
63
83
  /** Units at each machine that arrived and have not left: machine → unit → job. */
@@ -102,7 +122,7 @@ export declare class GatewayState {
102
122
  rememberFinish(machineId: string, unitId: string, body: Record<string, any>): void;
103
123
  takeFinish(machineId: string, unitId: string): Record<string, any> | undefined;
104
124
  rememberDispatch(cmdId: string, rowId: string): void;
105
- /** The open row an accepted dispatch command belongs to. */
125
+ /** The row a dispatch command belongs to, open or recently closed. */
106
126
  rowForDispatch(cmdId: string): HandoffRow | undefined;
107
127
  /** Track a machine-command row; false when it is already tracked or has ended. */
108
128
  trackCommand(id: string): boolean;
@@ -23,6 +23,16 @@ class GatewayState {
23
23
  this.unitsPerDispatch = null;
24
24
  /** Jobs go to a receiver once its units arrive there, from the codec. */
25
25
  this.dispatchOnArrival = false;
26
+ /** Note each job's start, progress steps and pauses on the row that holds it (lifecycle.ts). */
27
+ this.lifecycle = false;
28
+ /** Active alarms are retained on the broker; otherwise snapshots list them (codec.ts retainedAlarms). */
29
+ this.retainedAlarms = false;
30
+ /** Reading the broker's retained messages on taking the line; a snapshot read back may be older than its alarms. */
31
+ this.readingRetained = false;
32
+ /** When this gateway took the line (epoch ms); a command for an older row may have been sent by an earlier one. */
33
+ this.takenAt = 0;
34
+ /** With lifecycle notes, progress is noted each time it crosses this many percent; 0 notes none. */
35
+ this.progressStep = 25;
26
36
  /** While reconciling, new handoffs wait so pacing sees the whole line before anything is sent. */
27
37
  this.holdDispatch = false;
28
38
  /** Units at each machine that arrived and have not left: machine → unit → job. */
@@ -55,7 +65,10 @@ class GatewayState {
55
65
  const existing = this.openRows.get(row.id);
56
66
  // An event may carry metadata older than what the gateway already wrote: keep both traces.
57
67
  this.openRows.set(row.id, existing
58
- ? { ...row, dispatch: existing.dispatch, trace: (0, trace_1.mergeTrace)(row.trace, existing.trace), arrivals: existing.arrivals }
68
+ ? {
69
+ ...row, dispatch: existing.dispatch, trace: (0, trace_1.mergeTrace)(row.trace, existing.trace), arrivals: existing.arrivals,
70
+ dueAt: existing.dueAt, noted: existing.noted ?? row.noted,
71
+ }
59
72
  : row);
60
73
  }
61
74
  get(id) {
@@ -135,10 +148,10 @@ class GatewayState {
135
148
  this.dispatches.delete(oldest);
136
149
  }
137
150
  }
138
- /** The open row an accepted dispatch command belongs to. */
151
+ /** The row a dispatch command belongs to, open or recently closed. */
139
152
  rowForDispatch(cmdId) {
140
153
  const rowId = this.dispatches.get(cmdId);
141
- return rowId ? this.openRows.get(rowId) : undefined;
154
+ return rowId ? this.openRows.get(rowId) ?? this.closedRows.get(rowId) : undefined;
142
155
  }
143
156
  /** Track a machine-command row; false when it is already tracked or has ended. */
144
157
  trackCommand(id) {
@@ -4,19 +4,32 @@
4
4
  * waits on the server, so standby replicas hold no timers and the next one
5
5
  * takes over the moment the leader's session ends.
6
6
  */
7
+ import type { PoolClient } from 'pg';
8
+ export interface LeadershipOptions {
9
+ /** A dedicated connection for the lock; the shared pool's by default. */
10
+ connect?: () => Promise<PoolClient>;
11
+ /** The pause before contending again. */
12
+ retryMs?: number;
13
+ }
7
14
  export declare class LineLeadership {
8
15
  readonly key: string;
9
16
  private client?;
10
17
  private stopped;
11
18
  private leading;
12
- constructor(key: string);
19
+ private endTurn?;
20
+ private readonly connect;
21
+ private readonly retryMs;
22
+ constructor(key: string, opts?: LeadershipOptions);
13
23
  /**
14
- * Wait for the lock, then call `lead`. When leading fails or the
15
- * connection drops, the lock is released, `lost` runs, and the line
16
- * contends again after a pause.
24
+ * Wait for the lock, then call `lead`. When the connection is refused,
25
+ * leading fails or the connection drops, `lost` runs before this replica lets
26
+ * the lock go, so no other replica leads alongside a gateway that is
27
+ * stopping; then the line contends again after a pause.
17
28
  */
18
29
  run(lead: () => Promise<void>, lost: (err: Error) => Promise<void>): Promise<void>;
19
30
  private leadOnce;
31
+ /** Lead while the lock is held; the error that ended it. */
32
+ private holdLock;
20
33
  /** A leader unlocks; a replica still waiting drops its connection, which ends the wait. */
21
34
  stop(): Promise<void>;
22
35
  }
@@ -12,49 +12,74 @@ const LOCK_SQL = 'SELECT pg_advisory_lock(hashtext($1))';
12
12
  const RETRY_MS = 5_000;
13
13
  const UNLOCK_SQL = 'SELECT pg_advisory_unlock(hashtext($1))';
14
14
  class LineLeadership {
15
- constructor(key) {
15
+ constructor(key, opts = {}) {
16
16
  this.key = key;
17
17
  this.stopped = false;
18
18
  this.leading = false;
19
+ this.connect = opts.connect ?? (() => (0, db_1.getPool)().connect());
20
+ this.retryMs = opts.retryMs ?? RETRY_MS;
19
21
  }
20
22
  /**
21
- * Wait for the lock, then call `lead`. When leading fails or the
22
- * connection drops, the lock is released, `lost` runs, and the line
23
- * contends again after a pause.
23
+ * Wait for the lock, then call `lead`. When the connection is refused,
24
+ * leading fails or the connection drops, `lost` runs before this replica lets
25
+ * the lock go, so no other replica leads alongside a gateway that is
26
+ * stopping; then the line contends again after a pause.
24
27
  */
25
28
  async run(lead, lost) {
26
29
  while (!this.stopped) {
27
- const err = await this.leadOnce(lead);
30
+ await this.leadOnce(lead, lost);
28
31
  if (this.stopped)
29
32
  return;
30
- await lost(err);
31
- await new Promise((r) => setTimeout(r, RETRY_MS));
33
+ await new Promise((r) => setTimeout(r, this.retryMs));
32
34
  }
33
35
  }
34
- async leadOnce(lead) {
35
- const client = await (0, db_1.getPool)().connect();
36
+ async leadOnce(lead, lost) {
37
+ let client;
38
+ try {
39
+ client = await this.connect();
40
+ }
41
+ catch (err) {
42
+ if (!this.stopped)
43
+ await lost(err);
44
+ return;
45
+ }
46
+ if (this.stopped) {
47
+ client.release(true);
48
+ return;
49
+ }
36
50
  this.client = client;
37
- const dropped = new Promise((resolve) => client.once('error', resolve));
51
+ const err = await this.holdLock(client, lead);
52
+ this.leading = false;
53
+ this.endTurn = undefined;
54
+ // stop() has let the connection go.
55
+ if (this.stopped)
56
+ return;
57
+ await lost(err);
58
+ this.client = undefined;
59
+ client.release(true);
60
+ }
61
+ /** Lead while the lock is held; the error that ended it. */
62
+ async holdLock(client, lead) {
63
+ const ended = new Promise((resolve) => {
64
+ client.once('error', resolve);
65
+ this.endTurn = () => resolve(new Error('leadership stopped'));
66
+ });
38
67
  try {
39
68
  await client.query(LOCK_SQL, [this.key]);
69
+ if (this.stopped)
70
+ return new Error('leadership stopped');
40
71
  this.leading = true;
41
72
  await lead();
42
- return await dropped;
73
+ return await ended;
43
74
  }
44
75
  catch (err) {
45
76
  return err;
46
77
  }
47
- finally {
48
- this.leading = false;
49
- if (!this.stopped) {
50
- this.client = undefined;
51
- client.release(true);
52
- }
53
- }
54
78
  }
55
79
  /** A leader unlocks; a replica still waiting drops its connection, which ends the wait. */
56
80
  async stop() {
57
81
  this.stopped = true;
82
+ this.endTurn?.();
58
83
  const client = this.client;
59
84
  this.client = undefined;
60
85
  if (!client)
@@ -16,15 +16,30 @@ function paramsOf(v) {
16
16
  return v && typeof v === 'object' && !Array.isArray(v) ? v : undefined;
17
17
  }
18
18
  function artifactOf(v) {
19
- return v && typeof v.url === 'string' && v.url && typeof v.sha256 === 'string' && v.sha256 ? { url: v.url, sha256: v.sha256 } : undefined;
19
+ if (!v || typeof v.url !== 'string' || !v.url || typeof v.sha256 !== 'string' || !v.sha256)
20
+ return undefined;
21
+ return {
22
+ url: v.url, sha256: v.sha256,
23
+ ...(typeof v.id === 'string' && v.id ? { id: v.id } : {}),
24
+ ...(typeof v.contentType === 'string' && v.contentType ? { contentType: v.contentType } : {}),
25
+ ...(Number.isInteger(v.sizeBytes) && v.sizeBytes >= 0 ? { sizeBytes: v.sizeBytes } : {}),
26
+ };
27
+ }
28
+ function artifactsOf(v) {
29
+ const list = Array.isArray(v) ? v.map(artifactOf).filter((a) => !!a) : [];
30
+ return list.length ? list : undefined;
20
31
  }
21
32
  function unitsOf(v) {
22
33
  if (!Array.isArray(v))
23
34
  return [];
24
35
  return v.filter((u) => u && u.unitId).map((u) => {
25
36
  const artifact = artifactOf(u.artifact);
37
+ const artifacts = artifactsOf(u.artifacts);
26
38
  const parameters = paramsOf(u.parameters);
27
- return { unitId: String(u.unitId), carrierId: str(u.carrierId), ...(artifact ? { artifact } : {}), ...(parameters ? { parameters } : {}) };
39
+ return {
40
+ unitId: String(u.unitId), carrierId: str(u.carrierId),
41
+ ...(artifact ? { artifact } : {}), ...(artifacts ? { artifacts } : {}), ...(parameters ? { parameters } : {}),
42
+ };
28
43
  });
29
44
  }
30
45
  /** Read standard handoff facets; null when the line, job or units are missing. */
@@ -6,12 +6,19 @@
6
6
  import { type MachineLineConfig } from './config';
7
7
  import { type LineDef } from '../../lib/machines/line';
8
8
  import { type LineCodec } from '../../lib/machines/protocol';
9
+ import { messagingRegistry } from '../../lib/messaging';
9
10
  import type { CallbackEventAdapter } from '../../lib/events/callback';
10
11
  import type { EscalationFeed, MachineBus, MachineLink } from '../../lib/machines/transport/delivery';
11
12
  /** The codec for a line when the host supplies none: the line protocol under MACHINE_TOPIC_ROOT. */
12
13
  export declare function defaultCodec(config: MachineLineConfig, line?: LineDef): LineCodec;
13
- /** The gateway's view of every machine on the line. */
14
- export declare function createMachineBus(config: MachineLineConfig, line: LineDef, codec?: LineCodec): MachineBus;
14
+ /** What the stream bus needs from Long Tail's messaging; the registry by default. */
15
+ export type LineMessaging = Pick<typeof messagingRegistry, 'streams' | 'pubsub'>;
16
+ /**
17
+ * The gateway's view of every machine on the line. With `nats`, events are read
18
+ * from Long Tail's messaging streams; MACHINE_SITE_NATS_URL instead names a NATS
19
+ * server of the site's own, which the bus connects to itself.
20
+ */
21
+ export declare function createMachineBus(config: MachineLineConfig, line: LineDef, codec?: LineCodec, messaging?: LineMessaging): MachineBus;
15
22
  /** Long Tail's escalation events for the gateway; `in-process` needs the running instance's callback adapter. */
16
23
  export declare function createEscalationFeed(config: MachineLineConfig, line: LineDef, callbacks?: CallbackEventAdapter): EscalationFeed;
17
24
  /** One MQTT link per simulated machine; machines speak MQTT in every networked mode. */
@@ -15,8 +15,11 @@ const protocol_1 = require("../../lib/machines/protocol");
15
15
  const mqtt_bus_1 = require("../../lib/machines/transport/mqtt-bus");
16
16
  const mqtt_link_1 = require("../../lib/machines/transport/mqtt-link");
17
17
  const nats_bus_1 = require("../../lib/machines/transport/nats-bus");
18
+ const stream_bus_1 = require("../../lib/machines/transport/stream-bus");
18
19
  const callback_feed_1 = require("../../lib/machines/transport/callback-feed");
19
20
  const nats_feed_1 = require("../../lib/machines/transport/nats-feed");
21
+ const pubsub_feed_1 = require("../../lib/machines/transport/pubsub-feed");
22
+ const messaging_1 = require("../../lib/messaging");
20
23
  const IN_PROCESS_ONLY = 'lives inside one process; build MemoryMachineBus and MemoryEscalationFeed directly';
21
24
  function mqttOf(config) {
22
25
  if (!config.mqtt)
@@ -27,14 +30,20 @@ function mqttOf(config) {
27
30
  function defaultCodec(config, line) {
28
31
  return (0, protocol_1.genericCodec)({ root: config.topicRoot, line });
29
32
  }
30
- /** The gateway's view of every machine on the line. */
31
- function createMachineBus(config, line, codec = defaultCodec(config, line)) {
33
+ /**
34
+ * The gateway's view of every machine on the line. With `nats`, events are read
35
+ * from Long Tail's messaging streams; MACHINE_SITE_NATS_URL instead names a NATS
36
+ * server of the site's own, which the bus connects to itself.
37
+ */
38
+ function createMachineBus(config, line, codec = defaultCodec(config, line), messaging = messaging_1.messagingRegistry) {
32
39
  switch (config.transport) {
33
- case config_1.MACHINE_TRANSPORT.NATS:
34
- return new nats_bus_1.NatsMachineBus({
35
- natsUrl: config.siteNatsUrl, user: config.siteNatsUser, password: config.siteNatsPassword,
36
- mqtt: mqttOf(config), controllerId: (0, line_1.controllerId)(line), siteId: line.siteId, lineId: line.lineId, codec,
37
- });
40
+ case config_1.MACHINE_TRANSPORT.NATS: {
41
+ const where = { mqtt: mqttOf(config), controllerId: (0, line_1.controllerId)(line), siteId: line.siteId, lineId: line.lineId, codec };
42
+ if (config.siteNatsUrl) {
43
+ return new nats_bus_1.NatsMachineBus({ natsUrl: config.siteNatsUrl, user: config.siteNatsUser, password: config.siteNatsPassword, ...where });
44
+ }
45
+ return new stream_bus_1.StreamMachineBus({ streams: messaging.streams, pubsub: messaging.pubsub, ...where });
46
+ }
38
47
  case config_1.MACHINE_TRANSPORT.MQTT:
39
48
  return new mqtt_bus_1.MqttMachineBus(mqttOf(config), (0, line_1.controllerId)(line), (0, protocol_1.lineFilter)(codec.root, line.siteId, line.lineId));
40
49
  default:
@@ -49,6 +58,10 @@ function createEscalationFeed(config, line, callbacks) {
49
58
  throw new Error('MACHINE_ESCALATION_FEED=in-process runs inside start({ machines })');
50
59
  return new callback_feed_1.CallbackEscalationFeed(callbacks);
51
60
  case config_1.ESCALATION_FEED.CORE:
61
+ if (messaging_1.messagingRegistry.hasProvider)
62
+ return new pubsub_feed_1.PubSubEscalationFeed(messaging_1.messagingRegistry.pubsub);
63
+ if (!config.ltNatsUrl)
64
+ throw new Error('MACHINE_ESCALATION_FEED=core needs Long Tail messaging (LT_MESSAGING=nats) or NATS_URL');
52
65
  return new nats_feed_1.CoreEscalationFeed({ natsUrl: config.ltNatsUrl, token: config.ltNatsToken });
53
66
  default:
54
67
  throw new Error(`MACHINE_ESCALATION_FEED=${config.feed} ${IN_PROCESS_ONLY}`);
@@ -30,24 +30,30 @@ class LineRun {
30
30
  this.config = config;
31
31
  this.opts = opts;
32
32
  this.log = log;
33
+ this.stopped = false;
33
34
  this.leadership = new leadership_1.LineLeadership(`lt-machine-line:${line.siteId}/${line.lineId}`);
34
35
  }
35
36
  start() {
36
37
  const where = { site: this.line.siteId, line: this.line.lineId };
37
38
  this.log.info(log_1.GATEWAY_LOG.STANDBY, where);
38
- this.leadership.run(() => this.lead(), async (err) => {
39
+ this.leadership.run(() => (this.leading = this.lead()), async (err) => {
39
40
  this.log.warn(log_1.GATEWAY_LOG.LEADERSHIP_LOST, { ...where, error: err.message });
40
41
  await this.gateway?.stop();
41
42
  this.gateway = undefined;
42
43
  }).catch((err) => this.log.error(log_1.GATEWAY_LOG.START_FAILED, { ...where, error: err.message }));
43
44
  }
45
+ /** A gateway still starting finishes first, so none is left running after stop. */
44
46
  async stop() {
47
+ this.stopped = true;
48
+ await this.leading?.catch(() => undefined);
45
49
  await this.gateway?.stop();
46
50
  await this.leadership.stop();
47
51
  }
48
52
  async lead() {
49
53
  // 1. Identities, 2. gateway on the line's bus and feed, 3. reconcile and consume (gateway.start)
50
54
  const ids = await (0, identities_1.ensureLineBots)(this.line);
55
+ if (this.stopped)
56
+ return;
51
57
  const userFor = (as) => {
52
58
  const id = ids.get(as);
53
59
  if (!id)
@@ -68,7 +74,7 @@ class LineRun {
68
74
  settings: {
69
75
  topicRoot: config.topicRoot, replyTimeoutMs: config.replyTimeoutMs, dispatchAttempts: config.dispatchAttempts,
70
76
  offlineDebounceMs: config.offlineDebounceMs, dispatchTtlMs: config.dispatchTtlMs, quietMs: config.quietMs,
71
- receiptDelayMs: config.receiptDelayMs,
77
+ receiptDelayMs: config.receiptDelayMs, lifecycle: config.lifecycle, progressStep: config.progressStep,
72
78
  },
73
79
  });
74
80
  await this.gateway.start();
@@ -26,6 +26,7 @@ export interface FakeRow {
26
26
  resolved_by?: string;
27
27
  envelope?: string;
28
28
  escalation_payload?: string;
29
+ created_at?: string;
29
30
  }
30
31
  export interface Call {
31
32
  verb: string;
@@ -43,7 +43,7 @@ class FakeLongTail {
43
43
  });
44
44
  }
45
45
  add(input) {
46
- const row = { ...input, id: `e${++this.seq}`, status: 'pending', items: [] };
46
+ const row = { ...input, id: `e${++this.seq}`, status: 'pending', items: [], created_at: new Date().toISOString() };
47
47
  this.rows.set(row.id, row);
48
48
  this.emit(row, 'created');
49
49
  return row;
@@ -132,7 +132,7 @@ class FakeLongTail {
132
132
  return null;
133
133
  return {
134
134
  id: row.id, type: row.type, subtype: row.subtype, role: row.role, status: row.status,
135
- assigned_to: row.assigned_to ?? null, resolved_by: row.resolved_by ?? null, metadata: { ...row.metadata },
135
+ assigned_to: row.assigned_to ?? null, resolved_by: row.resolved_by ?? null, metadata: { ...row.metadata }, created_at: row.created_at ?? null,
136
136
  // Stored serialized, as Long Tail returns it.
137
137
  resolver_payload: row.resolver_payload ? JSON.stringify(row.resolver_payload) : null,
138
138
  };
@@ -140,7 +140,10 @@ class FakeLongTail {
140
140
  async listOpen(query) {
141
141
  return [...this.rows.values()]
142
142
  .filter((r) => r.status === 'pending' && (!query.type || r.type === query.type) && (!query.role || r.role === query.role))
143
- .map((r) => ({ id: r.id, type: r.type, subtype: r.subtype, role: r.role, status: r.status, assigned_to: r.assigned_to ?? null, metadata: r.metadata }));
143
+ .map((r) => ({
144
+ id: r.id, type: r.type, subtype: r.subtype, role: r.role, status: r.status, assigned_to: r.assigned_to ?? null, metadata: r.metadata,
145
+ created_at: r.created_at ?? null,
146
+ }));
144
147
  }
145
148
  ofType(type) {
146
149
  return [...this.rows.values()].filter((r) => r.type === type);
@@ -6,6 +6,8 @@ const pino_1 = require("../lib/logger/pino");
6
6
  const telemetry_1 = require("../lib/telemetry");
7
7
  const honeycomb_1 = require("../lib/telemetry/honeycomb");
8
8
  const events_1 = require("../lib/events");
9
+ const messaging_1 = require("../lib/messaging");
10
+ const config_1 = require("../lib/messaging/config");
9
11
  const nats_1 = require("../lib/events/nats");
10
12
  const socketio_1 = require("../lib/events/socketio");
11
13
  const maintenance_1 = require("../services/maintenance");
@@ -44,6 +46,10 @@ function registerAdapters(startConfig) {
44
46
  else if (startConfig.telemetry?.honeycomb) {
45
47
  telemetry_1.telemetryRegistry.register(new honeycomb_1.HoneycombTelemetryAdapter(startConfig.telemetry.honeycomb));
46
48
  }
49
+ // Messaging: one provider shared by events and the machine surface.
50
+ const messaging = (0, config_1.resolveMessaging)(startConfig);
51
+ if (messaging.provider)
52
+ messaging_1.messagingRegistry.register(messaging.provider);
47
53
  // Events — register the configured transport.
48
54
  // Socket.IO is the default for single-container ("nothing but Postgres").
49
55
  // When NATS or custom adapters are configured, Socket.IO is NOT registered —
@@ -54,8 +60,11 @@ function registerAdapters(startConfig) {
54
60
  events_1.eventRegistry.register(adapter);
55
61
  }
56
62
  }
57
- else if (startConfig.events?.nats) {
58
- events_1.eventRegistry.register(new nats_1.NatsEventAdapter(startConfig.events.nats));
63
+ else if (messaging.provider?.name === config_1.MESSAGING_KIND.NATS) {
64
+ events_1.eventRegistry.register(new nats_1.NatsEventAdapter({
65
+ messaging: messaging.provider, wsUrl: messaging.ws.url, wsProxy: messaging.ws.proxy, token: messaging.ws.token,
66
+ subjectPrefix: startConfig.events?.nats?.subjectPrefix,
67
+ }));
59
68
  }
60
69
  else {
61
70
  events_1.eventRegistry.register(new socketio_1.SocketIOEventAdapter({
@@ -5,6 +5,7 @@ const hotmesh_1 = require("@hotmeshio/hotmesh");
5
5
  const logger_1 = require("../lib/logger");
6
6
  const telemetry_1 = require("../lib/telemetry");
7
7
  const events_1 = require("../lib/events");
8
+ const messaging_1 = require("../lib/messaging");
8
9
  const callback_1 = require("../lib/events/callback");
9
10
  const surface_1 = require("../services/machines/surface");
10
11
  const roles_1 = require("../services/machines/roles");
@@ -66,8 +67,12 @@ async function start(startConfig) {
66
67
  (0, config_1.applyDatabaseConfig)(startConfig.database);
67
68
  // 2. Apply server/auth config
68
69
  await (0, config_1.applyServerAuthConfig)(startConfig);
69
- // 3. Register adapters
70
+ // 3. Register adapters; messaging connects first, since events and machines use it.
71
+ // An unreachable NATS is logged and Long Tail starts, as events always have; what needs streams fails then.
70
72
  (0, adapters_1.registerAdapters)(startConfig);
73
+ await messaging_1.messagingRegistry.connect().catch((err) => {
74
+ logger_1.loggerRegistry.error(`${messaging_1.MESSAGING_LOG.CONNECT_FAILED} ${err instanceof Error ? err.message : String(err)}`);
75
+ });
71
76
  logger_1.loggerRegistry.info('[long-tail] starting...');
72
77
  // 4-5. Collect workers, run migrations, start workers, seed data
73
78
  if (startConfig.machines && startConfig.machines.declareRoles !== false) {
@@ -110,6 +115,7 @@ async function start(startConfig) {
110
115
  if (events_1.eventRegistry.hasAdapters) {
111
116
  await events_1.eventRegistry.disconnect();
112
117
  }
118
+ await messaging_1.messagingRegistry.disconnect();
113
119
  if (telemetry_1.telemetryRegistry.hasAdapter) {
114
120
  await telemetry_1.telemetryRegistry.disconnect();
115
121
  }