@yarkivaev/scada 2.3.115 → 2.4.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 (104) hide show
  1. package/README.md +158 -0
  2. package/charts/scada/templates/processes.yaml +22 -0
  3. package/charts/scada/values.schema.json +12 -0
  4. package/db/migrations/V0009__collector_task_state.sql +6 -0
  5. package/package.json +14 -5
  6. package/src/application/deploy/collectorProcesses.js +3 -2
  7. package/src/application/deploy/readDocument.js +4 -1
  8. package/src/application/plantServer.js +1 -1
  9. package/src/application/runtime/additionalAdapters.js +139 -0
  10. package/src/application/runtime/assignment.js +30 -0
  11. package/src/application/runtime/budgetedSink.js +17 -0
  12. package/src/application/runtime/collectorSignals.js +14 -0
  13. package/src/application/runtime/compositeLifecycle.js +49 -0
  14. package/src/application/runtime/createRegistry.js +20 -0
  15. package/src/application/runtime/deadline.js +8 -0
  16. package/src/application/runtime/index.js +10 -0
  17. package/src/application/runtime/manifest.js +38 -0
  18. package/src/application/runtime/pipelineAdapters.js +60 -0
  19. package/src/application/runtime/resolveSecrets.js +12 -0
  20. package/src/application/runtime/resourceClaims.js +21 -0
  21. package/src/application/runtime/runCollector.js +50 -0
  22. package/src/application/runtime/runtime.js +86 -0
  23. package/src/application/runtime/runtimeHealth.js +29 -0
  24. package/src/application/runtime/runtimeServices.js +15 -0
  25. package/src/application/runtime/stateDocument.js +22 -0
  26. package/src/application/runtime/streamPorts.js +42 -0
  27. package/src/application/runtime/streamTask.js +105 -0
  28. package/src/application/runtime/taskBudget.js +31 -0
  29. package/src/application/runtime/taskContext.js +18 -0
  30. package/src/application/runtime/taskQueue.js +21 -0
  31. package/src/application/runtime/taskSlot.js +46 -0
  32. package/src/application/runtime/taskState.js +73 -0
  33. package/src/application/siteCollection.js +14 -0
  34. package/src/application/siteServer.js +7 -6
  35. package/src/application/siteTelemetry.js +1 -1
  36. package/src/application/supervisorSink.js +9 -8
  37. package/src/application/sync/siteSyncBind.js +1 -1
  38. package/src/application/sync/siteSyncTargets.js +1 -1
  39. package/src/bin/collector.js +7 -0
  40. package/src/bin/modbus-to-mqtt.js +11 -9
  41. package/src/infrastructure/deploy/executeRelease.js +45 -3
  42. package/src/infrastructure/ingest/activity/activityTracking.js +15 -21
  43. package/src/infrastructure/ingest/activity/activityTransformer.js +16 -15
  44. package/src/infrastructure/ingest/durableConsume.js +2 -169
  45. package/src/infrastructure/ingest/ingestBatch.js +13 -0
  46. package/src/infrastructure/ingest/modbus/mx210Tcp.js +4 -4
  47. package/src/infrastructure/ingest/modbus/silentStreams.js +39 -16
  48. package/src/infrastructure/ingest/mqtt/metricsTransformer.js +4 -5
  49. package/src/infrastructure/ingest/mqtt/modbusMqtt.js +17 -19
  50. package/src/infrastructure/ingest/mqtt/modbusPublish.js +4 -0
  51. package/src/infrastructure/ingest/mqtt/mqttMetrics.js +17 -21
  52. package/src/infrastructure/ingest/mqtt/registerPublish.js +3 -3
  53. package/src/infrastructure/ingest/pipelines/alertPipeline.js +7 -6
  54. package/src/infrastructure/ingest/pipelines/decisionPipeline.js +7 -6
  55. package/src/infrastructure/ingest/pipelines/segmentPipeline.js +8 -17
  56. package/src/infrastructure/ingest/pipelines/stompOptions.js +7 -0
  57. package/src/infrastructure/ingest/silentOpenWatch.js +5 -42
  58. package/src/infrastructure/ingest/telemetry/amqpMetricsIngest.js +16 -28
  59. package/src/infrastructure/ingest/telemetry/amqpMqttRelay.js +17 -22
  60. package/src/infrastructure/ingest/telemetry/foldingSink.js +5 -2
  61. package/src/infrastructure/messaging/stomp/segmentRetags.js +1 -1
  62. package/src/infrastructure/messaging/stomp/userDecisions.js +1 -1
  63. package/src/infrastructure/opcua/certificates.js +11 -0
  64. package/src/infrastructure/opcua/config.js +38 -0
  65. package/src/infrastructure/opcua/delivery.js +38 -0
  66. package/src/infrastructure/opcua/endpoint.js +90 -0
  67. package/src/infrastructure/opcua/index.js +10 -0
  68. package/src/infrastructure/opcua/session.js +70 -0
  69. package/src/infrastructure/opcua/source.js +38 -0
  70. package/src/infrastructure/opcua/subscriptionHealth.js +28 -0
  71. package/src/infrastructure/operators/operatorsSync.js +5 -29
  72. package/src/infrastructure/persistence/clickhouse/streamHub.js +21 -15
  73. package/src/infrastructure/persistence/metricsSensor.js +9 -6
  74. package/src/infrastructure/persistence/runtime/taskStatePg.js +24 -0
  75. package/src/infrastructure/streams/README.md +291 -0
  76. package/src/infrastructure/streams/abortable.js +14 -0
  77. package/src/infrastructure/streams/amqpSink/publication.js +46 -0
  78. package/src/infrastructure/streams/amqpSink/session.js +96 -0
  79. package/src/infrastructure/streams/amqpSink.js +47 -0
  80. package/src/infrastructure/streams/amqpSource/session.js +127 -0
  81. package/src/infrastructure/streams/amqpSource.js +97 -0
  82. package/src/infrastructure/streams/brokerClose.js +19 -0
  83. package/src/infrastructure/streams/budgetAccept.js +6 -0
  84. package/src/infrastructure/streams/clickhouseSink.js +48 -0
  85. package/src/infrastructure/streams/deadline.js +1 -0
  86. package/src/infrastructure/streams/follow.js +41 -0
  87. package/src/infrastructure/streams/index.js +15 -0
  88. package/src/infrastructure/streams/lokiSource.js +88 -0
  89. package/src/infrastructure/streams/modbusConnection.js +173 -0
  90. package/src/infrastructure/streams/modbusRtuBusSource.js +124 -0
  91. package/src/infrastructure/streams/modbusSource.js +185 -0
  92. package/src/infrastructure/streams/mqttSink/session.js +51 -0
  93. package/src/infrastructure/streams/mqttSink.js +54 -0
  94. package/src/infrastructure/streams/mqttSource.js +66 -0
  95. package/src/infrastructure/streams/pollingSource.js +1 -0
  96. package/src/infrastructure/streams/positive.js +5 -0
  97. package/src/infrastructure/streams/postgresSink.js +124 -0
  98. package/src/infrastructure/streams/serialReceive.js +80 -0
  99. package/src/infrastructure/streams/serialSource.js +87 -0
  100. package/src/infrastructure/streams/sqliteSink.js +55 -0
  101. package/src/infrastructure/streams/stompBody.js +46 -0
  102. package/src/infrastructure/streams/stompSend.js +89 -0
  103. package/src/infrastructure/streams/stompSource.js +201 -0
  104. package/src/infrastructure/sync/operationSyncIngest.js +4 -16
@@ -0,0 +1,55 @@
1
+ import Database from 'better-sqlite3';
2
+
3
+ /**
4
+ * SQLite sink for batch record insertion.
5
+ *
6
+ * Opens a SQLite database in WAL mode and implements the Sink
7
+ * interface for use with batch collectors. Creates the target
8
+ * table with the given columns if it does not exist.
9
+ *
10
+ * @example
11
+ * const sink = sqliteSink('/tmp/metrics.db', 'metrics', ['topic', 'ts', 'value']);
12
+ * sink.write([{ topic: 'sensor/1', ts: 1700000000000, value: 42 }]);
13
+ *
14
+ * @param {string} path - Filesystem path to the SQLite database file
15
+ * @param {string} table - Target table name
16
+ * @param {Array<string>} columns - Column names for insertion
17
+ * @returns {object} Sink with write(records) and close() methods
18
+ */
19
+ export default function sqliteSink(path, table, columns) {
20
+ if (typeof path !== 'string' || path.length === 0) {
21
+ throw new Error('Path must be a non-empty string');
22
+ }
23
+ if (typeof table !== 'string' || table.length === 0) {
24
+ throw new Error('Table must be a non-empty string');
25
+ }
26
+ if (!Array.isArray(columns) || columns.length === 0) {
27
+ throw new Error('Columns must be a non-empty array');
28
+ }
29
+ const db = new Database(path);
30
+ db.pragma('journal_mode = WAL');
31
+ const placeholders = columns.map(() => {return '?'}).join(', ');
32
+ const sql = `INSERT INTO ${table} (${columns.join(', ')}) VALUES (${placeholders})`;
33
+ const statement = db.prepare(sql);
34
+ const transaction = db.transaction((records) => {
35
+ for (const record of records) {
36
+ statement.run(...columns.map((col) => {return record[col]}));
37
+ }
38
+ });
39
+ return {
40
+ /**
41
+ * Writes records to SQLite table inside a transaction.
42
+ *
43
+ * @param {Array} records - Array of objects with keys matching columns
44
+ */
45
+ write(records) {
46
+ transaction(records);
47
+ },
48
+ /**
49
+ * Closes the database connection.
50
+ */
51
+ close() {
52
+ db.close();
53
+ }
54
+ };
55
+ }
@@ -0,0 +1,46 @@
1
+ import positive from './positive.js';
2
+ import budgetAccept from './budgetAccept.js';
3
+
4
+ export function stompSettings(options, prefetch) {
5
+ const config = { acceptTimeoutMs: 10000, stopTimeoutMs: 5000, maxMessageBytes: 1048576, ...options };
6
+ config.maxPendingBytes ??= prefetch * config.maxMessageBytes;
7
+ ['acceptTimeoutMs', 'stopTimeoutMs', 'maxMessageBytes', 'maxPendingBytes'].forEach((key) => { positive(config[key], key); });
8
+ if (!Number.isSafeInteger(config.maxMessageBytes) || !Number.isSafeInteger(config.maxPendingBytes)) {
9
+ throw new TypeError('STOMP byte capacities must be safe integers');
10
+ }
11
+ return config;
12
+ }
13
+ function overflow() { return new Error('STOMP payload capacity exceeded'); }
14
+ function legacyRead(message, maximum) {
15
+ return new Promise((resolve, reject) => {
16
+ message.readString('utf-8', (error, body) => {
17
+ if (error) { reject(error); }
18
+ else if (Buffer.byteLength(body) > maximum) { reject(overflow()); }
19
+ else { resolve(body.toString()); }
20
+ });
21
+ });
22
+ }
23
+ /** Native stompit frames are Readables: bound bytes before concatenating the body. */
24
+ export async function readStomp(message, maximum) {
25
+ if (!message[Symbol.asyncIterator]) { return legacyRead(message, maximum); }
26
+ const chunks = [];
27
+ let bytes = 0;
28
+ for await (const chunk of message) {
29
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
30
+ bytes += buffer.length;
31
+ if (bytes > maximum) { throw overflow(); }
32
+ chunks.push(buffer);
33
+ }
34
+ return Buffer.concat(chunks, bytes).toString('utf8');
35
+ }
36
+ /** Counters and global leases follow real acceptance, including after an uncertain timeout. */
37
+ export async function acceptStomp(ctx, body, settle) {
38
+ if (!ctx.lifecycle.active) { throw new Error('STOMP source stopped'); }
39
+ const bytes = Buffer.byteLength(body);
40
+ if (ctx.lifecycle.bytes + bytes > ctx.lifecycle.maxPendingBytes) { throw overflow(); }
41
+ ctx.lifecycle.bytes += bytes;
42
+ try {
43
+ return await budgetAccept(ctx.collector, { destination: ctx.destination, payload: body, settle }, bytes,
44
+ ctx.lifecycle, { signal: ctx.lifecycle.controller.signal });
45
+ } finally { ctx.lifecycle.bytes -= bytes; }
46
+ }
@@ -0,0 +1,89 @@
1
+ import stompit from 'stompit';
2
+ import deadline from './deadline.js';
3
+ import positive from './positive.js';
4
+
5
+ /**
6
+ * Builds STOMP connect options from a URL.
7
+ *
8
+ * @param {URL} parsed - Parsed URL object
9
+ * @param {object} options - Connection options
10
+ * @returns {object} stompit connect parameters
11
+ */
12
+ /**
13
+ * STOMP SEND headers that persist the body on a durable RabbitMQ queue.
14
+ *
15
+ * @param {string} destination - STOMP destination
16
+ * @returns {object} headers including persistent true
17
+ */
18
+ export function stompHeaders(destination) {
19
+ return {
20
+ destination,
21
+ 'content-type': 'application/json',
22
+ persistent: 'true'
23
+ };
24
+ }
25
+
26
+ function connectParams(parsed, options) {
27
+ return {
28
+ host: parsed.hostname,
29
+ port: parseInt(parsed.port, 10) || 61613,
30
+ connectHeaders: {
31
+ host: options.host || parsed.hostname,
32
+ login: options.login || '',
33
+ passcode: options.passcode || ''
34
+ }
35
+ };
36
+ }
37
+
38
+ /**
39
+ * One-shot STOMP publish of a JSON payload to a destination.
40
+ *
41
+ * Connects, sends one MESSAGE frame, disconnects. For operator decisions
42
+ * and other low-volume outbound events.
43
+ *
44
+ * @example
45
+ * await stompSend('stomp://rabbitmq:61613', '/exchange/scada.user_decisions',
46
+ * { machine: 'icht2', start: 1700000000, user: 'hmi', tags: ['heating'] },
47
+ * { login: 'guest', passcode: 'guest', host: '/' });
48
+ *
49
+ * @param {string} url - STOMP broker URL
50
+ * @param {string} destination - STOMP destination
51
+ * @param {object|string} payload - JSON-serializable body or raw string
52
+ * @param {object} [options] - STOMP connection options
53
+ * @param {string} [options.login] - Login
54
+ * @param {string} [options.passcode] - Passcode
55
+ * @param {string} [options.host] - Virtual host header
56
+ * @returns {Promise<void>} Resolves when the frame is sent and client disconnects
57
+ */
58
+ export default function stompSend(url, destination, payload, options = {}) {
59
+ if (typeof url !== 'string' || url.length === 0) {
60
+ throw new Error('URL must be a non-empty string');
61
+ }
62
+ if (typeof destination !== 'string' || destination.length === 0) {
63
+ throw new Error('Destination must be a non-empty string');
64
+ }
65
+ const parsed = new URL(url);
66
+ const params = connectParams(parsed, options);
67
+ const body = typeof payload === 'string' ? payload : JSON.stringify(payload);
68
+ const timeoutMs = options.timeoutMs ?? 10000;
69
+ positive(timeoutMs, 'timeoutMs');
70
+ const state = { active: true, client: null, connecting: null };
71
+ const sent = new Promise((resolve, reject) => {
72
+ state.connecting = (options.connect || stompit.connect)({ ...params, timeout: timeoutMs }, (error, client) => {
73
+ if (error) { reject(error); return; }
74
+ state.client = client;
75
+ if (!state.active) { client.destroy(); return; }
76
+ client.on('error', reject);
77
+ try {
78
+ const frame = client.send(stompHeaders(destination), { onReceipt() {
79
+ if (state.active) { client.disconnect((failure) => { if (failure) { reject(failure); } else { resolve(); } }); }
80
+ } });
81
+ frame.end(body);
82
+ } catch (failure) { client.destroy(); reject(failure); }
83
+ });
84
+ });
85
+ return deadline(sent, timeoutMs, 'STOMP receipt/disconnect', () => {
86
+ state.active = false;
87
+ (state.client || state.connecting)?.destroy();
88
+ }).finally(() => { state.active = false; });
89
+ }
@@ -0,0 +1,201 @@
1
+ import stompit from 'stompit';
2
+ import follow from './follow.js';
3
+ import deadline from './deadline.js';
4
+ import { acceptStomp, readStomp, stompSettings } from './stompBody.js';
5
+
6
+ /**
7
+ * Idle state for STOMP source.
8
+ *
9
+ * @returns {object} State with subscribed() returning false
10
+ */
11
+ function idle() {
12
+ return {
13
+ subscribed() {
14
+ return false;
15
+ }
16
+ };
17
+ }
18
+
19
+ /**
20
+ * Subscribed state for STOMP source.
21
+ *
22
+ * @param {object} channel - STOMP channel
23
+ * @returns {object} State with subscribed() returning true
24
+ */
25
+ function subscribed(channel, delivery) {
26
+ return {
27
+ subscribed() {
28
+ return true;
29
+ },
30
+ channel() {
31
+ return channel;
32
+ },
33
+ delivery
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Builds STOMP server configuration from a URL.
39
+ *
40
+ * @param {URL} parsed - Parsed URL object
41
+ * @param {object} options - Connection options
42
+ * @returns {object} Server config for ConnectFailover
43
+ */
44
+ function serverConfig(parsed, options) {
45
+ return {
46
+ host: parsed.hostname,
47
+ port: parseInt(parsed.port, 10) || 61613,
48
+ connectHeaders: {
49
+ host: options.host || parsed.hostname,
50
+ login: options.login || '',
51
+ passcode: options.passcode || ''
52
+ }
53
+ };
54
+ }
55
+
56
+ /**
57
+ * Builds settle helpers for one STOMP frame.
58
+ *
59
+ * @param {object} channel - STOMP channel
60
+ * @param {object} message - STOMP frame
61
+ * @returns {{ ack: Function, nack: Function }}
62
+ */
63
+ function settleOf(channel, message, lifecycle) {
64
+ let settled = false;
65
+ return {
66
+ ack() {
67
+ if (lifecycle.active && !settled) { channel.ack(message); settled = true; }
68
+ },
69
+ nack(requeue = true) {
70
+ if (lifecycle.active && !settled) { channel.nack(message, { requeue: String(requeue) }); settled = true; }
71
+ }
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Delivers one STOMP body to the collector and settles the frame.
77
+ *
78
+ * @param {object} ctx - delivery context with channel/message/destination/collector/manualAck
79
+ * @param {string} body - UTF-8 payload
80
+ * @returns {Promise<void>}
81
+ */
82
+ async function deliver(ctx, body) {
83
+ const settle = settleOf(ctx.channel, ctx.message, ctx.lifecycle);
84
+ try {
85
+ await deadline(Promise.resolve().then(() => {
86
+ return acceptStomp(ctx, body, settle);
87
+ }), ctx.lifecycle.acceptTimeoutMs, 'STOMP acceptance', (error) => {
88
+ ctx.lifecycle.fatal = error; ctx.lifecycle.active = false; ctx.lifecycle.controller.abort(error); ctx.channel.close();
89
+ });
90
+ if (!ctx.manualAck) {
91
+ settle.ack();
92
+ }
93
+ } catch (error) {
94
+ if (!ctx.manualAck) {
95
+ settle.nack();
96
+ }
97
+ throw error;
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Subscribes a channel and routes frames to the collector.
103
+ * Arrivals stay in subscribe order through follow, even when persist overlaps.
104
+ *
105
+ * @param {object} channel - STOMP channel
106
+ * @param {string} destination - queue destination
107
+ * @param {object} collector - message collector
108
+ * @param {object} delivery - serial, manualAck and prefetch settings
109
+ * @returns {undefined}
110
+ */
111
+ function bind(channel, destination, collector, delivery) {
112
+ const { serial, manualAck, prefetch, lifecycle } = delivery;
113
+ let pending = Promise.resolve();
114
+ channel.subscribe({ destination, ack: 'client-individual', 'prefetch-count': String(prefetch) }, (err, message) => {
115
+ if (err) {
116
+ return;
117
+ }
118
+ const ctx = { channel, message, destination, collector, manualAck, lifecycle };
119
+ pending = follow(pending, () => {
120
+ if (!lifecycle.active) { return Promise.reject(new Error('STOMP source stopped')); }
121
+ return deadline(readStomp(message, lifecycle.maxMessageBytes), lifecycle.acceptTimeoutMs, 'STOMP read').catch((error) => {
122
+ lifecycle.fatal = error; lifecycle.active = false; channel.close(); throw error;
123
+ });
124
+ }, (body) => {
125
+ const task = deliver(ctx, body);
126
+ lifecycle.pending.add(task);
127
+ return task.finally(() => { lifecycle.pending.delete(task); });
128
+ }, serial);
129
+ lifecycle.chain = pending;
130
+ void pending.catch((error) => {
131
+ try { lifecycle.onError?.(error); } catch {
132
+ // Reporting cannot reject a transport callback.
133
+ }
134
+ });
135
+ });
136
+ }
137
+
138
+ /**
139
+ * STOMP subscription source for streaming messages to a collector.
140
+ *
141
+ * Messages are `{destination, payload, settle}`. Default ack-after-accept;
142
+ * with `manualAck` the collector owns settle. Arrivals enqueue in
143
+ * subscribe order. With `serial: false` persist may overlap after enqueue.
144
+ * A finite prefetch (default 32) bounds unacknowledged broker deliveries.
145
+ *
146
+ * @param {string} url - STOMP broker URL
147
+ * @param {string} destination - STOMP destination
148
+ * @param {object} collector - Collector with accept()
149
+ * @param {object} [options] - Connection and delivery options
150
+ * @param {number} [options.prefetch=32] - Positive integer delivery window
151
+ * @returns {object} Source with start() and stop() methods
152
+ */
153
+ export default function stompSource(url, destination, collector, options = {}) {
154
+ if (typeof url !== 'string' || url.length === 0) {
155
+ throw new Error('URL must be a non-empty string');
156
+ }
157
+ if (typeof destination !== 'string' || destination.length === 0) {
158
+ throw new Error('Destination must be a non-empty string');
159
+ }
160
+ if (!collector || typeof collector.accept !== 'function') {
161
+ throw new Error('Collector must have an accept() method');
162
+ }
163
+ const prefetch = options.prefetch ?? 32;
164
+ if (!Number.isSafeInteger(prefetch) || prefetch < 1) {
165
+ throw new Error('prefetch must be a positive safe integer');
166
+ }
167
+ const settings = stompSettings(options, prefetch);
168
+ const parsed = new URL(url);
169
+ const serial = options.serial !== false;
170
+ const manualAck = options.manualAck === true;
171
+ let state = idle();
172
+ return {
173
+ start() {
174
+ if (state.subscribed()) {
175
+ return;
176
+ }
177
+ const failover = new stompit.ConnectFailover([serverConfig(parsed, options)], {
178
+ initialReconnectDelay: 10,
179
+ maxReconnectDelay: 30000,
180
+ maxReconnects: -1
181
+ });
182
+ const channel = new stompit.Channel(failover);
183
+ const lifecycle = { ...settings, active: true, controller: new AbortController(), pending: new Set(), bytes: 0 };
184
+ bind(channel, destination, collector, { serial, manualAck, prefetch, lifecycle });
185
+ state = subscribed(channel, lifecycle);
186
+ },
187
+ stop() {
188
+ if (!state.subscribed()) {
189
+ return Promise.resolve();
190
+ }
191
+ const stopped = state;
192
+ stopped.delivery.active = false;
193
+ stopped.delivery.controller.abort(new Error('STOMP source stopped'));
194
+ stopped.channel().close();
195
+ return deadline(Promise.allSettled([stopped.delivery.chain, ...stopped.delivery.pending]), options.stopTimeoutMs ?? 5000, 'STOMP stop').then(() => {
196
+ if (stopped.delivery.fatal) { throw stopped.delivery.fatal; }
197
+ if (state === stopped) { state = idle(); }
198
+ });
199
+ }
200
+ };
201
+ }
@@ -1,4 +1,4 @@
1
- import durableConsume from '../ingest/durableConsume.js';
1
+ import { amqpSource } from '../streams/index.js';
2
2
  import operationCodec from './operationCodec.js';
3
3
  import operationSyncSink from './operationSyncSink.js';
4
4
 
@@ -43,25 +43,13 @@ export function operationConsumer(codec, channel) {
43
43
  * const ingest = operationSyncIngest(amqpUrl, 'scada.operations.ingest', dataAccess.operations);
44
44
  * await ingest.start();
45
45
  */
46
- function subscribeOperations(queue, prefetch, codec) {
47
- return async (channel) => {
48
- await channel.assertQueue(queue, { durable: true });
49
- channel.prefetch(prefetch);
50
- const onMessage = operationConsumer(codec, channel);
51
- const tag = await channel.consume(queue, onMessage, { noAck: false });
52
- return tag.consumerTag;
53
- };
54
- }
55
-
56
46
  export default function operationSyncIngest(amqpUrl, queue, operations, options = {}) {
57
47
  const prefetch = options.prefetch || 32;
58
48
  const sink = operationSyncSink(operations);
59
49
  const codec = operationCodec(sink);
60
- const session = durableConsume(
61
- amqpUrl,
62
- subscribeOperations(queue, prefetch, codec),
63
- options
64
- );
50
+ const session = amqpSource(amqpUrl, queue, {
51
+ accept(delivery) { return acceptOperationDeliver(codec, delivery.content); }
52
+ }, { ...options, prefetch });
65
53
  return {
66
54
  start() {
67
55
  return session.start();