@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
@@ -0,0 +1,147 @@
1
+ "use strict";
2
+ /**
3
+ * Messaging in memory, for tests and single-process development. Pub/sub fans
4
+ * out to every matching subscriber. Streams keep each message until it ages
5
+ * out, and deliver to each consumer one at a time in order; a nak'd message
6
+ * comes back after its delay while later messages go on, as JetStream does.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.MemoryMessagingProvider = void 0;
10
+ const constants_1 = require("./constants");
11
+ const subjects_1 = require("./subjects");
12
+ const DEFAULT_NAK_DELAY_MS = 1_000;
13
+ class MemoryPubSub {
14
+ constructor() {
15
+ this.subs = new Set();
16
+ }
17
+ async publish(subject, payload) {
18
+ for (const sub of [...this.subs])
19
+ if ((0, subjects_1.subjectMatches)(sub.filter, subject))
20
+ sub.handler({ subject, payload });
21
+ }
22
+ subscribe(filter, handler) {
23
+ const sub = { filter, handler };
24
+ this.subs.add(sub);
25
+ return { close: async () => { this.subs.delete(sub); } };
26
+ }
27
+ }
28
+ class MemoryStreams {
29
+ constructor() {
30
+ this.streams = new Map();
31
+ }
32
+ async ensureStream(spec) {
33
+ const existing = this.streams.get(spec.name);
34
+ if (existing)
35
+ existing.spec = spec;
36
+ else
37
+ this.streams.set(spec.name, { spec, messages: [], seq: 0, consumers: new Map(), wake: new Set() });
38
+ }
39
+ async ensureConsumer(stream, spec) {
40
+ const s = this.stream(stream);
41
+ const existing = s.consumers.get(spec.name);
42
+ if (existing) {
43
+ existing.spec = { ...existing.spec, filterSubjects: spec.filterSubjects };
44
+ return;
45
+ }
46
+ const floor = (spec.startAt ?? constants_1.START_AT.NEW) === constants_1.START_AT.ALL ? 0 : s.seq;
47
+ s.consumers.set(spec.name, { spec, floor, settled: new Set(), deliveries: new Map(), retryAt: new Map() });
48
+ }
49
+ async append(subject, payload) {
50
+ const now = Date.now();
51
+ for (const s of this.streams.values()) {
52
+ if (!s.spec.subjects.some((f) => (0, subjects_1.subjectMatches)(f, subject)))
53
+ continue;
54
+ s.messages.push({ seq: ++s.seq, subject, payload, at: now });
55
+ s.messages = s.messages.filter((m) => now - m.at <= s.spec.maxAgeMs);
56
+ for (const wake of [...s.wake])
57
+ wake();
58
+ }
59
+ }
60
+ async consume(stream, consumer, handler, opts = {}) {
61
+ const s = this.stream(stream);
62
+ const c = s.consumers.get(consumer);
63
+ if (!c)
64
+ throw new Error(`stream ${stream} has no consumer ${consumer}; ensure it first`);
65
+ const nakDelayMs = opts.nakDelayMs ?? DEFAULT_NAK_DELAY_MS;
66
+ let open = true;
67
+ let running = false;
68
+ let timer;
69
+ const next = () => {
70
+ const now = Date.now();
71
+ let waitMs;
72
+ for (const m of s.messages) {
73
+ if (m.seq <= c.floor || c.settled.has(m.seq))
74
+ continue;
75
+ if (!c.spec.filterSubjects.some((f) => (0, subjects_1.subjectMatches)(f, m.subject)))
76
+ continue;
77
+ const at = c.retryAt.get(m.seq);
78
+ if (at === undefined || at <= now)
79
+ return { message: m };
80
+ waitMs = Math.min(waitMs ?? Infinity, at - now);
81
+ }
82
+ return { waitMs };
83
+ };
84
+ const pump = async () => {
85
+ if (running || !open)
86
+ return;
87
+ running = true;
88
+ try {
89
+ for (let step = next(); open && step.message; step = next()) {
90
+ const m = step.message;
91
+ const deliveries = (c.deliveries.get(m.seq) ?? 0) + 1;
92
+ c.deliveries.set(m.seq, deliveries);
93
+ const outcome = await handler({ subject: m.subject, payload: m.payload, deliveries }).catch(() => constants_1.DELIVERY.NAK);
94
+ if (outcome === constants_1.DELIVERY.NAK) {
95
+ c.retryAt.set(m.seq, Date.now() + nakDelayMs);
96
+ }
97
+ else {
98
+ c.settled.add(m.seq);
99
+ c.retryAt.delete(m.seq);
100
+ }
101
+ }
102
+ const { waitMs } = next();
103
+ if (open && waitMs !== undefined) {
104
+ clearTimeout(timer);
105
+ timer = setTimeout(() => void pump(), Math.max(0, waitMs));
106
+ }
107
+ }
108
+ finally {
109
+ running = false;
110
+ }
111
+ };
112
+ const wake = () => void pump();
113
+ s.wake.add(wake);
114
+ void pump();
115
+ return {
116
+ close: async () => {
117
+ open = false;
118
+ clearTimeout(timer);
119
+ s.wake.delete(wake);
120
+ },
121
+ };
122
+ }
123
+ stream(name) {
124
+ const s = this.streams.get(name);
125
+ if (!s)
126
+ throw new Error(`no stream ${name}; ensure it first`);
127
+ return s;
128
+ }
129
+ }
130
+ class MemoryMessagingProvider {
131
+ constructor() {
132
+ this.name = 'memory';
133
+ this.pubsub = new MemoryPubSub();
134
+ this.streams = new MemoryStreams();
135
+ this.live = false;
136
+ }
137
+ get connected() {
138
+ return this.live;
139
+ }
140
+ async connect() {
141
+ this.live = true;
142
+ }
143
+ async disconnect() {
144
+ this.live = false;
145
+ }
146
+ }
147
+ exports.MemoryMessagingProvider = MemoryMessagingProvider;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * JetStream rights scoped to named streams and one durable consumer: look up,
3
+ * create and update those streams, create, read and acknowledge through that
4
+ * consumer, and nothing else. Deleting or purging a stream is not granted.
5
+ */
6
+ export interface JsScope {
7
+ streams: string[];
8
+ durable: string;
9
+ }
10
+ export declare function jsPermissions({ streams, durable }: JsScope): {
11
+ publish: string[];
12
+ subscribe: string[];
13
+ };
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ /**
3
+ * JetStream rights scoped to named streams and one durable consumer: look up,
4
+ * create and update those streams, create, read and acknowledge through that
5
+ * consumer, and nothing else. Deleting or purging a stream is not granted.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.jsPermissions = jsPermissions;
9
+ function jsPermissions({ streams, durable }) {
10
+ const perStream = streams.flatMap((s) => [
11
+ `$JS.API.STREAM.INFO.${s}`,
12
+ `$JS.API.STREAM.CREATE.${s}`,
13
+ `$JS.API.STREAM.UPDATE.${s}`,
14
+ `$JS.API.CONSUMER.INFO.${s}.${durable}`,
15
+ `$JS.API.CONSUMER.CREATE.${s}.${durable}`,
16
+ `$JS.API.CONSUMER.CREATE.${s}.${durable}.>`,
17
+ `$JS.API.CONSUMER.DURABLE.CREATE.${s}.${durable}`,
18
+ `$JS.API.CONSUMER.MSG.NEXT.${s}.${durable}`,
19
+ `$JS.ACK.${s}.${durable}.>`,
20
+ ]);
21
+ return { publish: ['$JS.API.INFO', ...perStream], subscribe: ['_INBOX.>'] };
22
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Messaging on NATS. One connection carries pub/sub (core NATS subjects) and,
3
+ * when the server runs JetStream for this account, durable streams. Without
4
+ * JetStream the provider still serves pub/sub and reports no streams.
5
+ */
6
+ import type { LTMessagingProvider, LTPubSub, LTStreams } from '../../../types/messaging';
7
+ export interface NatsMessagingOptions {
8
+ url: string;
9
+ /** Token auth, or user and password; neither for an open server. */
10
+ token?: string;
11
+ user?: string;
12
+ password?: string;
13
+ /** The connection's name on the server. */
14
+ name?: string;
15
+ }
16
+ export declare class NatsMessagingProvider implements LTMessagingProvider {
17
+ private readonly opts;
18
+ readonly name = "nats";
19
+ readonly pubsub: LTPubSub;
20
+ private nc;
21
+ private durable;
22
+ constructor(opts: NatsMessagingOptions);
23
+ get connected(): boolean;
24
+ get streams(): LTStreams | null;
25
+ connect(): Promise<void>;
26
+ disconnect(): Promise<void>;
27
+ /** Streams when the account has JetStream; null otherwise, and pub/sub still serves. */
28
+ private jetStream;
29
+ private connection;
30
+ }
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ /**
3
+ * Messaging on NATS. One connection carries pub/sub (core NATS subjects) and,
4
+ * when the server runs JetStream for this account, durable streams. Without
5
+ * JetStream the provider still serves pub/sub and reports no streams.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.NatsMessagingProvider = void 0;
9
+ const nats_1 = require("nats");
10
+ const logger_1 = require("../../logger");
11
+ const constants_1 = require("../constants");
12
+ const streams_1 = require("./streams");
13
+ class NatsPubSub {
14
+ constructor(nc) {
15
+ this.nc = nc;
16
+ }
17
+ async publish(subject, payload) {
18
+ this.nc().publish(subject, payload);
19
+ }
20
+ subscribe(filter, handler) {
21
+ const sub = this.nc().subscribe(filter, {
22
+ callback: (err, m) => { if (!err)
23
+ handler({ subject: m.subject, payload: (0, streams_1.payloadOf)(m) }); },
24
+ });
25
+ return { close: async () => { sub.unsubscribe(); } };
26
+ }
27
+ }
28
+ class NatsMessagingProvider {
29
+ constructor(opts) {
30
+ this.opts = opts;
31
+ this.name = 'nats';
32
+ this.pubsub = new NatsPubSub(() => this.connection());
33
+ this.nc = null;
34
+ this.durable = null;
35
+ }
36
+ get connected() {
37
+ return !!this.nc && !this.nc.isClosed();
38
+ }
39
+ get streams() {
40
+ return this.durable;
41
+ }
42
+ async connect() {
43
+ if (this.connected)
44
+ return;
45
+ const { url, token, user, password, name } = this.opts;
46
+ this.nc = await (0, nats_1.connect)({ servers: url, token, user, pass: password, name });
47
+ this.durable = await this.jetStream(this.nc);
48
+ }
49
+ async disconnect() {
50
+ if (!this.nc)
51
+ return;
52
+ await this.nc.drain();
53
+ this.nc = null;
54
+ this.durable = null;
55
+ }
56
+ /** Streams when the account has JetStream; null otherwise, and pub/sub still serves. */
57
+ async jetStream(nc) {
58
+ try {
59
+ const jsm = await nc.jetstreamManager();
60
+ await jsm.getAccountInfo();
61
+ return new streams_1.NatsStreams(jsm, nc.jetstream());
62
+ }
63
+ catch (err) {
64
+ const reason = String(err?.message ?? err?.code ?? err);
65
+ if (!/jetstream not enabled|503|no responders/i.test(reason)) {
66
+ logger_1.loggerRegistry.warn(`${constants_1.MESSAGING_LOG.STREAMS_UNAVAILABLE} provider=${this.name} reason=${reason}`);
67
+ }
68
+ return null;
69
+ }
70
+ }
71
+ connection() {
72
+ if (!this.nc)
73
+ throw new Error('NATS messaging is not connected; call connect() first');
74
+ return this.nc;
75
+ }
76
+ }
77
+ exports.NatsMessagingProvider = NatsMessagingProvider;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Long Tail's NATS server, rendered. Accounts keep traffic apart:
3
+ *
4
+ * - LT: Long Tail itself. It owns the durable streams and imports each edge's
5
+ * subjects, which its streams can then keep.
6
+ * - one edge account per line: the site broker's bridge logs in here (MQTT
7
+ * only) and may publish its line's subjects and nothing else. The account
8
+ * exports exactly those subjects, so nothing of Long Tail's is reachable.
9
+ * - SYS: the server's own monitoring.
10
+ *
11
+ * NATS configuration accepts JSON, so accounts are written as JSON.
12
+ */
13
+ export interface NatsPermissions {
14
+ publish?: string[] | {
15
+ allow?: string[];
16
+ deny?: string[];
17
+ };
18
+ subscribe?: string[] | {
19
+ allow?: string[];
20
+ deny?: string[];
21
+ };
22
+ }
23
+ export interface NatsUser {
24
+ user: string;
25
+ password?: string;
26
+ permissions?: NatsPermissions;
27
+ allowed_connection_types?: string[];
28
+ }
29
+ /** A line's edge: who logs in for it, and the subjects it may publish (e.g. `line.v1.site-1.line-1.>`). */
30
+ export interface EdgeAccount {
31
+ name: string;
32
+ subjects: string;
33
+ users: NatsUser[];
34
+ }
35
+ export interface NatsConfOptions {
36
+ serverName: string;
37
+ port?: number;
38
+ httpPort?: number;
39
+ jetstream?: {
40
+ storeDir: string;
41
+ };
42
+ /** WebSocket listener for browsers; `noAuthUser` maps a connection without credentials to that LT user. */
43
+ websocket?: {
44
+ port: number;
45
+ noTls?: boolean;
46
+ noAuthUser?: string;
47
+ };
48
+ /** MQTT listener for edge bridges; with `tls`, certificates map to users by their subject. */
49
+ mqtt?: {
50
+ port: number;
51
+ tls?: {
52
+ certFile: string;
53
+ keyFile: string;
54
+ caFile: string;
55
+ };
56
+ };
57
+ lt: {
58
+ users: NatsUser[];
59
+ };
60
+ edges: EdgeAccount[];
61
+ system?: {
62
+ user: string;
63
+ password: string;
64
+ };
65
+ }
66
+ export declare const LT_ACCOUNT = "LT";
67
+ export declare const SYS_ACCOUNT = "SYS";
68
+ export declare function renderNatsConf(opts: NatsConfOptions): string;
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ /**
3
+ * Long Tail's NATS server, rendered. Accounts keep traffic apart:
4
+ *
5
+ * - LT: Long Tail itself. It owns the durable streams and imports each edge's
6
+ * subjects, which its streams can then keep.
7
+ * - one edge account per line: the site broker's bridge logs in here (MQTT
8
+ * only) and may publish its line's subjects and nothing else. The account
9
+ * exports exactly those subjects, so nothing of Long Tail's is reachable.
10
+ * - SYS: the server's own monitoring.
11
+ *
12
+ * NATS configuration accepts JSON, so accounts are written as JSON.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.SYS_ACCOUNT = exports.LT_ACCOUNT = void 0;
16
+ exports.renderNatsConf = renderNatsConf;
17
+ exports.LT_ACCOUNT = 'LT';
18
+ exports.SYS_ACCOUNT = 'SYS';
19
+ function accounts(opts) {
20
+ const out = {
21
+ [exports.LT_ACCOUNT]: {
22
+ jetstream: opts.jetstream ? 'enabled' : 'disabled',
23
+ users: opts.lt.users,
24
+ imports: opts.edges.map((e) => ({ stream: { account: e.name, subject: e.subjects } })),
25
+ },
26
+ };
27
+ for (const edge of opts.edges) {
28
+ // The edge account has JetStream because NATS keeps MQTT session state there.
29
+ out[edge.name] = { jetstream: opts.jetstream ? 'enabled' : 'disabled', users: edge.users, exports: [{ stream: edge.subjects }] };
30
+ }
31
+ if (opts.system)
32
+ out[exports.SYS_ACCOUNT] = { users: [opts.system] };
33
+ return out;
34
+ }
35
+ function renderNatsConf(opts) {
36
+ const lines = [
37
+ '# Rendered by Long Tail (lib/messaging/nats/render-conf.ts). Do not edit by hand.',
38
+ `server_name: ${JSON.stringify(opts.serverName)}`,
39
+ `port: ${opts.port ?? 4222}`,
40
+ `http: ${opts.httpPort ?? 8222}`,
41
+ ];
42
+ if (opts.jetstream)
43
+ lines.push(`jetstream { store_dir: ${JSON.stringify(opts.jetstream.storeDir)} }`);
44
+ if (opts.websocket) {
45
+ const ws = opts.websocket;
46
+ lines.push(`websocket { port: ${ws.port}${ws.noTls ? ', no_tls: true, same_origin: false' : ''}${ws.noAuthUser ? `, no_auth_user: ${JSON.stringify(ws.noAuthUser)}` : ''} }`);
47
+ }
48
+ if (opts.mqtt) {
49
+ const tls = opts.mqtt.tls;
50
+ lines.push(tls
51
+ ? `mqtt { port: ${opts.mqtt.port}, tls { cert_file: ${JSON.stringify(tls.certFile)}, key_file: ${JSON.stringify(tls.keyFile)}, ca_file: ${JSON.stringify(tls.caFile)}, verify_and_map: true, timeout: 5 } }`
52
+ : `mqtt { port: ${opts.mqtt.port} }`);
53
+ }
54
+ lines.push(`accounts: ${JSON.stringify(accounts(opts), null, 2)}`);
55
+ if (opts.system)
56
+ lines.push(`system_account: ${exports.SYS_ACCOUNT}`);
57
+ return `${lines.join('\n')}\n`;
58
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Durable streams on JetStream. A spec becomes a file-backed stream or an
3
+ * explicit-ack durable consumer; ensuring either is safe on every boot, and an
4
+ * existing consumer keeps its position while its subjects follow the spec.
5
+ */
6
+ import { type ConsumerConfig, type JetStreamClient, type JetStreamManager, type StreamConfig } from 'nats';
7
+ import type { ConsumerSpec, Delivery, LTConsumeOptions, LTStreamMessage, LTStreams, LTSubscription, StreamSpec, Subject } from '../../../types/messaging';
8
+ export declare function streamConfig(spec: StreamSpec): Partial<StreamConfig>;
9
+ export declare function consumerConfig(spec: ConsumerSpec): Partial<ConsumerConfig>;
10
+ /** Create the stream, or update an existing one to the spec. */
11
+ export declare function ensureStream(jsm: JetStreamManager, spec: StreamSpec): Promise<void>;
12
+ /** Create the consumer, or move an existing one to the spec's subjects without losing its place. */
13
+ export declare function ensureConsumer(jsm: JetStreamManager, stream: string, spec: ConsumerSpec): Promise<void>;
14
+ export declare function payloadOf(m: {
15
+ data: Uint8Array;
16
+ }): string;
17
+ export declare class NatsStreams implements LTStreams {
18
+ private readonly jsm;
19
+ private readonly js;
20
+ constructor(jsm: JetStreamManager, js: JetStreamClient);
21
+ ensureStream(spec: StreamSpec): Promise<void>;
22
+ ensureConsumer(stream: string, spec: ConsumerSpec): Promise<void>;
23
+ append(subject: Subject, payload: string): Promise<void>;
24
+ /** One message at a time: the next is not taken until the handler settles this one. */
25
+ consume(stream: string, consumer: string, handler: (m: LTStreamMessage) => Promise<Delivery>, opts?: LTConsumeOptions): Promise<LTSubscription>;
26
+ }
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ /**
3
+ * Durable streams on JetStream. A spec becomes a file-backed stream or an
4
+ * explicit-ack durable consumer; ensuring either is safe on every boot, and an
5
+ * existing consumer keeps its position while its subjects follow the spec.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.NatsStreams = void 0;
9
+ exports.streamConfig = streamConfig;
10
+ exports.consumerConfig = consumerConfig;
11
+ exports.ensureStream = ensureStream;
12
+ exports.ensureConsumer = ensureConsumer;
13
+ exports.payloadOf = payloadOf;
14
+ const nats_1 = require("nats");
15
+ const constants_1 = require("../constants");
16
+ const DEFAULT_ACK_WAIT_MS = 30_000;
17
+ const DEFAULT_NAK_DELAY_MS = 1_000;
18
+ function streamConfig(spec) {
19
+ return { name: spec.name, subjects: spec.subjects, max_age: (0, nats_1.nanos)(spec.maxAgeMs), storage: nats_1.StorageType.File };
20
+ }
21
+ function consumerConfig(spec) {
22
+ return {
23
+ durable_name: spec.name,
24
+ ack_policy: nats_1.AckPolicy.Explicit,
25
+ deliver_policy: (spec.startAt ?? constants_1.START_AT.NEW) === constants_1.START_AT.ALL ? nats_1.DeliverPolicy.All : nats_1.DeliverPolicy.New,
26
+ ack_wait: (0, nats_1.nanos)(spec.ackWaitMs ?? DEFAULT_ACK_WAIT_MS),
27
+ filter_subjects: spec.filterSubjects,
28
+ };
29
+ }
30
+ /** Create the stream, or update an existing one to the spec. */
31
+ async function ensureStream(jsm, spec) {
32
+ const config = streamConfig(spec);
33
+ try {
34
+ await jsm.streams.info(spec.name);
35
+ }
36
+ catch (err) {
37
+ if (!/stream not found/i.test(err?.message ?? ''))
38
+ throw err;
39
+ await jsm.streams.add(config);
40
+ return;
41
+ }
42
+ await jsm.streams.update(spec.name, config);
43
+ }
44
+ /** Create the consumer, or move an existing one to the spec's subjects without losing its place. */
45
+ async function ensureConsumer(jsm, stream, spec) {
46
+ let existing;
47
+ try {
48
+ existing = await jsm.consumers.info(stream, spec.name);
49
+ }
50
+ catch (err) {
51
+ if (!/consumer not found/i.test(err?.message ?? ''))
52
+ throw err;
53
+ await jsm.consumers.add(stream, consumerConfig(spec));
54
+ return;
55
+ }
56
+ const want = [...spec.filterSubjects].sort();
57
+ const have = [...(existing.config.filter_subjects ?? [])].sort();
58
+ if (want.join('|') !== have.join('|'))
59
+ await jsm.consumers.update(stream, spec.name, { filter_subjects: spec.filterSubjects });
60
+ }
61
+ function payloadOf(m) {
62
+ return Buffer.from(m.data).toString('utf8');
63
+ }
64
+ class NatsStreams {
65
+ constructor(jsm, js) {
66
+ this.jsm = jsm;
67
+ this.js = js;
68
+ }
69
+ ensureStream(spec) {
70
+ return ensureStream(this.jsm, spec);
71
+ }
72
+ ensureConsumer(stream, spec) {
73
+ return ensureConsumer(this.jsm, stream, spec);
74
+ }
75
+ async append(subject, payload) {
76
+ await this.js.publish(subject, payload);
77
+ }
78
+ /** One message at a time: the next is not taken until the handler settles this one. */
79
+ async consume(stream, consumer, handler, opts = {}) {
80
+ const nakDelayMs = opts.nakDelayMs ?? DEFAULT_NAK_DELAY_MS;
81
+ const messages = await (await this.js.consumers.get(stream, consumer)).consume();
82
+ void (async () => {
83
+ for await (const m of messages) {
84
+ const delivery = await handler({ subject: m.subject, payload: payloadOf(m), deliveries: m.info.deliveryCount })
85
+ .catch(() => constants_1.DELIVERY.NAK);
86
+ if (delivery === constants_1.DELIVERY.ACK)
87
+ m.ack();
88
+ else if (delivery === constants_1.DELIVERY.NAK)
89
+ m.nak(nakDelayMs);
90
+ else
91
+ m.term();
92
+ }
93
+ })();
94
+ return { close: async () => { await messages.close(); } };
95
+ }
96
+ }
97
+ exports.NatsStreams = NatsStreams;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Subject filters. Subjects are dot-separated tokens; in a filter `*` matches
3
+ * exactly one token and `>` matches one or more trailing tokens.
4
+ */
5
+ import type { Subject } from '../../types/messaging';
6
+ export declare function subjectMatches(filter: Subject, subject: Subject): boolean;
7
+ /** Whether `subject` is a valid concrete subject: nonempty tokens, no wildcards or whitespace. */
8
+ export declare function isSubject(subject: string): boolean;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ /**
3
+ * Subject filters. Subjects are dot-separated tokens; in a filter `*` matches
4
+ * exactly one token and `>` matches one or more trailing tokens.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.subjectMatches = subjectMatches;
8
+ exports.isSubject = isSubject;
9
+ function subjectMatches(filter, subject) {
10
+ const f = filter.split('.');
11
+ const s = subject.split('.');
12
+ for (let i = 0; i < f.length; i++) {
13
+ if (f[i] === '>')
14
+ return i === f.length - 1 && s.length > i;
15
+ if (i >= s.length)
16
+ return false;
17
+ if (f[i] !== '*' && f[i] !== s[i])
18
+ return false;
19
+ }
20
+ return f.length === s.length;
21
+ }
22
+ /** Whether `subject` is a valid concrete subject: nonempty tokens, no wildcards or whitespace. */
23
+ function isSubject(subject) {
24
+ return !!subject && subject.split('.').every((t) => !!t && !/[\s*>]/.test(t));
25
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Make the example's first print: a small 3DBenchy (by Creative Tools), sliced
3
+ * for the printer with OrcaSlicer, written to examples/workflows/bambu-printer/files/boat.gcode.3mf.
4
+ * Run it on the host, where OrcaSlicer is installed:
5
+ *
6
+ * npm run bambu:boat
7
+ *
8
+ * 1. Download the model once into files/.
9
+ * 2. Scale it (BAMBU_BOAT_SCALE, 0.5 by default: about ten minutes on a P1S).
10
+ * 3. Slice it with the printer, process and filament presets. Each is an
11
+ * OrcaSlicer system preset name or a path to a preset JSON, which may
12
+ * inherit a system preset by name.
13
+ *
14
+ * Env: BAMBU_BOAT_PRINTER, BAMBU_BOAT_PROCESS, BAMBU_BOAT_FILAMENT, BAMBU_BOAT_SCALE,
15
+ * ORCA_SLICER (the OrcaSlicer executable) and ORCA_PROFILES (its BBL profiles directory).
16
+ */
17
+ export {};