@yarkivaev/scada 1.5.0 → 2.3.45

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 (190) hide show
  1. package/README.md +196 -33
  2. package/db/migrations/C0001__central_drop_metrics.sql +1 -0
  3. package/db/migrations/C0002__central_revoke_delete.sql +12 -0
  4. package/db/migrations/C0003__central_operators.sql +7 -0
  5. package/db/migrations/C0004__central_operations_grants.sql +2 -0
  6. package/db/migrations/C0005__central_operators_grants.sql +1 -0
  7. package/db/migrations/C0006__central_operators_registration.sql +16 -0
  8. package/db/migrations/E0001__edge_metrics.sql +8 -0
  9. package/db/migrations/E0002__edge_retention_delete.sql +8 -0
  10. package/db/migrations/E0003__edge_operations_grants.sql +2 -0
  11. package/db/migrations/V0001__baseline.sql +25 -0
  12. package/db/migrations/V0002__sink_extension_tables.sql +6 -0
  13. package/db/migrations/V0003__operations.sql +10 -0
  14. package/db/migrations/V0004__ingest_checkpoints.sql +6 -0
  15. package/db/migrations/V0005__user_decisions_operator.sql +2 -0
  16. package/index.js +75 -46
  17. package/package.json +28 -9
  18. package/src/application/bindSilentStreams.js +20 -0
  19. package/src/application/edgeOperatorCatalog.js +45 -0
  20. package/src/application/export/exportJob.js +118 -0
  21. package/src/application/export/exportQuery.js +32 -0
  22. package/src/application/export/exportSink.js +25 -0
  23. package/src/application/export/exportStream.js +33 -0
  24. package/src/application/machineInPlant.js +20 -0
  25. package/src/application/metricsPlant.js +35 -0
  26. package/src/application/plantApi.js +49 -0
  27. package/src/application/plantOperations.js +19 -0
  28. package/src/application/plantServer.js +107 -0
  29. package/src/application/shopWithTimeline.js +112 -0
  30. package/src/application/siteOperatorCatalog.js +78 -0
  31. package/src/application/siteServer.js +191 -0
  32. package/src/application/supervisorSink.js +97 -0
  33. package/src/application/timelineOperatorFromEnv.js +19 -0
  34. package/src/bin/supervisor-sink.js +28 -0
  35. package/src/{alert.js → domain/alerting/alert.js} +2 -2
  36. package/src/{alerts.js → domain/alerting/alerts.js} +3 -3
  37. package/src/domain/alerting/ingest.js +18 -0
  38. package/src/domain/ingest/ingestCursor.js +26 -0
  39. package/src/domain/operation/operation.js +23 -0
  40. package/src/domain/operation/operations.js +81 -0
  41. package/src/domain/operator/operator.js +23 -0
  42. package/src/domain/plant/machine.js +32 -0
  43. package/src/domain/plant/plant.js +20 -0
  44. package/src/domain/plant/shop.js +24 -0
  45. package/src/domain/segment/dispatch.js +31 -0
  46. package/src/domain/segment/normalize.js +31 -0
  47. package/src/domain/segment/silenceBudget.js +16 -0
  48. package/src/{initialized.js → domain/shared/initialized.js} +3 -4
  49. package/src/{pubsub.js → domain/shared/pubsub.js} +2 -3
  50. package/src/domain/timeline/timeline.js +25 -0
  51. package/src/infrastructure/catalog/tag-catalog.json +27 -0
  52. package/src/infrastructure/catalog/tagCatalog.js +65 -0
  53. package/src/infrastructure/client/index.js +20 -0
  54. package/src/infrastructure/client/machineClient.js +159 -0
  55. package/src/infrastructure/client/machineOperationsClient.js +159 -0
  56. package/src/infrastructure/client/scadaClient.js +67 -0
  57. package/src/infrastructure/client/sseConnection.js +42 -0
  58. package/src/infrastructure/http/edge/edgeApi.js +54 -0
  59. package/src/infrastructure/http/edge/httpMetricsRead.js +34 -0
  60. package/src/infrastructure/http/edge/metricsSensor.js +15 -0
  61. package/src/infrastructure/http/edge/parseStateHttpTimeoutMs.js +20 -0
  62. package/src/infrastructure/http/edge/routes/checkpoint/checkpointRoutes.js +79 -0
  63. package/src/infrastructure/http/edge/routes/metrics/metricsBatch.js +59 -0
  64. package/src/infrastructure/http/edge/routes/metrics/metricsCurrent.js +29 -0
  65. package/src/infrastructure/http/edge/routes/metrics/metricsPoll.js +25 -0
  66. package/src/infrastructure/http/edge/routes/metrics/metricsRange.js +26 -0
  67. package/src/infrastructure/http/edge/routes/metrics/metricsRoutes.js +21 -0
  68. package/src/infrastructure/http/edge/routes/retention/retentionRoutes.js +41 -0
  69. package/src/infrastructure/http/edge/startTestEdgeApi.js +39 -0
  70. package/src/infrastructure/http/edge/stateAccess.js +13 -0
  71. package/src/infrastructure/http/edge/stateHttpClient.js +106 -0
  72. package/src/infrastructure/http/edge/stateHttpTimeoutError.js +13 -0
  73. package/src/infrastructure/http/plant/json/decisionJson.js +44 -0
  74. package/src/infrastructure/http/plant/json/operationJson.js +28 -0
  75. package/src/infrastructure/http/plant/json/operatorJson.js +22 -0
  76. package/src/infrastructure/http/plant/json/segmentJson.js +27 -0
  77. package/src/infrastructure/http/plant/operationAudit.js +56 -0
  78. package/src/infrastructure/http/plant/routes/alertRoute.js +93 -0
  79. package/src/infrastructure/http/plant/routes/catalogRoute.js +21 -0
  80. package/src/infrastructure/http/plant/routes/decisionRoute.js +60 -0
  81. package/src/infrastructure/http/plant/routes/machineRoute.js +35 -0
  82. package/src/infrastructure/http/plant/routes/measurementRoute.js +60 -0
  83. package/src/infrastructure/http/plant/routes/operationDrafts.js +79 -0
  84. package/src/infrastructure/http/plant/routes/operationRoute.js +80 -0
  85. package/src/infrastructure/http/plant/routes/operationWrites.js +186 -0
  86. package/src/infrastructure/http/plant/routes/operatorRoute.js +143 -0
  87. package/src/infrastructure/http/plant/routes/simulationRoute.js +61 -0
  88. package/src/infrastructure/http/plant/routes/timelineRoute.js +112 -0
  89. package/src/infrastructure/http/plant/streams/alertStream.js +68 -0
  90. package/src/infrastructure/http/plant/streams/heartbeatStream.js +34 -0
  91. package/src/infrastructure/http/plant/streams/measurementStream.js +90 -0
  92. package/src/infrastructure/http/plant/streams/operationStream.js +42 -0
  93. package/src/infrastructure/http/plant/streams/timelineStream.js +97 -0
  94. package/src/infrastructure/http/plant/timelineOperator.js +87 -0
  95. package/src/infrastructure/ingest/activity/activityTracking.js +74 -0
  96. package/src/infrastructure/ingest/activity/activityTransformer.js +45 -0
  97. package/src/infrastructure/ingest/codecs/alertCodec.js +56 -0
  98. package/src/infrastructure/ingest/codecs/segmentCodec.js +30 -0
  99. package/src/infrastructure/ingest/codecs/userDecisionCodec.js +78 -0
  100. package/src/infrastructure/ingest/cooldown.js +24 -0
  101. package/src/infrastructure/ingest/db/migrate.js +78 -0
  102. package/src/infrastructure/ingest/db/migrationProfile.js +35 -0
  103. package/src/infrastructure/ingest/db/runRetention.js +58 -0
  104. package/src/infrastructure/ingest/ingestCheckpoint.js +71 -0
  105. package/src/infrastructure/ingest/modbus/mx210Tcp.js +81 -0
  106. package/src/infrastructure/ingest/modbus/silentStreams.js +152 -0
  107. package/src/infrastructure/ingest/mqtt/metricsTransformer.js +54 -0
  108. package/src/infrastructure/ingest/mqtt/modbusDeviceSpec.js +112 -0
  109. package/src/infrastructure/ingest/mqtt/modbusMqtt.js +204 -0
  110. package/src/infrastructure/ingest/mqtt/mqttMetrics.js +78 -0
  111. package/src/infrastructure/ingest/parseRequestTimeoutMs.js +20 -0
  112. package/src/infrastructure/ingest/pipelines/alertPipeline.js +48 -0
  113. package/src/infrastructure/ingest/pipelines/decisionPipeline.js +45 -0
  114. package/src/infrastructure/ingest/pipelines/segmentPipeline.js +60 -0
  115. package/src/infrastructure/ingest/processingErrorLog.js +25 -0
  116. package/src/infrastructure/ingest/silentOpenWatch.js +50 -0
  117. package/src/infrastructure/ingest/sinks/alertSink.js +43 -0
  118. package/src/infrastructure/ingest/sinks/closeOrphanOpen.js +32 -0
  119. package/src/infrastructure/ingest/sinks/closeSilentOpen.js +39 -0
  120. package/src/infrastructure/ingest/sinks/retagSink.js +43 -0
  121. package/src/infrastructure/ingest/sinks/userDecisionSink.js +41 -0
  122. package/src/infrastructure/ingest/telemetry/amqpMetricsIngest.js +94 -0
  123. package/src/infrastructure/ingest/telemetry/amqpMqttRelay.js +71 -0
  124. package/src/infrastructure/ingest/telemetry/deliverToMqttRecord.js +19 -0
  125. package/src/infrastructure/ingest/telemetry/streamNameFromTopic.js +21 -0
  126. package/src/infrastructure/messaging/ownership/httpOperations.js +96 -0
  127. package/src/infrastructure/messaging/ownership/httpTimeline.js +99 -0
  128. package/src/infrastructure/messaging/ownership/machineOwners.js +39 -0
  129. package/src/infrastructure/messaging/ownership/ownerTimeline.js +28 -0
  130. package/src/infrastructure/messaging/stomp/alerts/stompAlerts.js +21 -0
  131. package/src/infrastructure/messaging/stomp/alerts/stompAlertsCollection.js +51 -0
  132. package/src/infrastructure/messaging/stomp/alerts/stompAlertsInit.js +24 -0
  133. package/src/infrastructure/messaging/stomp/alerts/stompAlertsLogic.js +51 -0
  134. package/src/infrastructure/messaging/stomp/stompTimelineSegments.js +80 -0
  135. package/src/infrastructure/messaging/stomp/timeline.js +21 -0
  136. package/src/infrastructure/messaging/stomp/userDecisionBody.js +25 -0
  137. package/src/infrastructure/messaging/stomp/userDecisions.js +42 -0
  138. package/src/infrastructure/operators/centralOperators.js +64 -0
  139. package/src/infrastructure/operators/edgeOperators.js +39 -0
  140. package/src/infrastructure/operators/operatorById.js +33 -0
  141. package/src/infrastructure/operators/operatorExtras.js +41 -0
  142. package/src/infrastructure/operators/operators.js +32 -0
  143. package/src/infrastructure/operators/operatorsFromSeed.js +18 -0
  144. package/src/infrastructure/operators/operatorsSync.js +57 -0
  145. package/src/infrastructure/persistence/clickhouse/connection.js +58 -0
  146. package/src/infrastructure/persistence/clickhouse/pollTopicCursors.js +48 -0
  147. package/src/{clickhouseSensor.js → infrastructure/persistence/clickhouse/sensor.js} +9 -27
  148. package/src/infrastructure/persistence/clickhouse/streamHub.js +165 -0
  149. package/src/infrastructure/persistence/memory/alerts.js +21 -0
  150. package/src/infrastructure/persistence/memory/checkpoints.js +98 -0
  151. package/src/infrastructure/persistence/memory/metrics.js +69 -0
  152. package/src/infrastructure/persistence/memory/metricsDisabled.js +19 -0
  153. package/src/infrastructure/persistence/memory/operations.js +87 -0
  154. package/src/infrastructure/persistence/memory/segments.js +83 -0
  155. package/src/infrastructure/persistence/memory/timeline.js +56 -0
  156. package/src/infrastructure/persistence/metricsSensor.js +112 -0
  157. package/src/infrastructure/persistence/pg/alerts.js +25 -0
  158. package/src/infrastructure/persistence/pg/checkpoints.js +115 -0
  159. package/src/infrastructure/persistence/pg/metrics.js +73 -0
  160. package/src/infrastructure/persistence/pg/operations.js +95 -0
  161. package/src/infrastructure/persistence/pg/operators.js +118 -0
  162. package/src/infrastructure/persistence/pg/segments.js +48 -0
  163. package/src/infrastructure/persistence/pg/timeline.js +53 -0
  164. package/src/infrastructure/persistence/pg/userDecisions.js +72 -0
  165. package/src/infrastructure/persistence/postgresPool.js +24 -0
  166. package/src/infrastructure/persistence/stateDataFromMemory.js +28 -0
  167. package/src/infrastructure/persistence/stateDataFromPool.js +18 -0
  168. package/src/infrastructure/sync/operationCodec.js +75 -0
  169. package/src/infrastructure/sync/operationSyncIngest.js +82 -0
  170. package/src/infrastructure/sync/operationSyncSink.js +40 -0
  171. package/src/activeMelting.js +0 -54
  172. package/src/completedMelting.js +0 -42
  173. package/src/event.js +0 -24
  174. package/src/events.js +0 -51
  175. package/src/interval.js +0 -18
  176. package/src/machineChronology.js +0 -79
  177. package/src/meltingChronology.js +0 -37
  178. package/src/meltingMachine.js +0 -53
  179. package/src/meltingRuleEngine.js +0 -37
  180. package/src/meltingShop.js +0 -32
  181. package/src/meltings.js +0 -97
  182. package/src/monitoredMeltingMachine.js +0 -33
  183. package/src/plant.js +0 -23
  184. package/src/postgresSensor.js +0 -105
  185. package/src/requests.js +0 -44
  186. package/src/rule.js +0 -33
  187. package/src/rules.js +0 -23
  188. package/src/scyllaSensor.js +0 -54
  189. package/src/segments.js +0 -64
  190. package/src/sqliteSensor.js +0 -106
@@ -0,0 +1,72 @@
1
+ /**
2
+ * PostgreSQL user_decisions port for segment and operation audit.
3
+ *
4
+ * list(machine, start) returns segment chronology.
5
+ * listByKey(machine, key) returns operation chronology by payload.key.
6
+ * insert writes one audit row.
7
+ *
8
+ * @param {object} pool - pg pool
9
+ * @returns {object} catalog with list, listByKey, insert
10
+ *
11
+ * @example
12
+ * const catalog = userDecisionsFromPg(pool);
13
+ * await catalog.insert({
14
+ * machine: 'm1', startTime: new Date(), username: 'Ivan',
15
+ * decidedAt: new Date(), payload: { kind: 'operation_op', verb: 'create', key: 'bath:m1:1' }
16
+ * });
17
+ */
18
+ function mapRow(row) {
19
+ const item = {
20
+ username: row.username,
21
+ decidedAt: row.decided_at,
22
+ payload: row.payload
23
+ };
24
+ if (row.operator_id !== undefined && row.operator_id !== null) {
25
+ item.operatorId = row.operator_id;
26
+ }
27
+ return item;
28
+ }
29
+
30
+ export default function userDecisionsFromPg(pool) {
31
+ return {
32
+ async list(machine, start) {
33
+ const result = await pool.query(
34
+ `SELECT username, operator_id, decided_at, payload
35
+ FROM user_decisions
36
+ WHERE machine = $1 AND start_time = $2
37
+ ORDER BY decided_at ASC NULLS LAST`,
38
+ [machine, start]
39
+ );
40
+ return result.rows.map(mapRow);
41
+ },
42
+ async listByKey(machine, key) {
43
+ const result = await pool.query(
44
+ `SELECT username, operator_id, decided_at, payload
45
+ FROM user_decisions
46
+ WHERE machine = $1
47
+ AND (payload::jsonb->>'key') = $2
48
+ ORDER BY decided_at ASC NULLS LAST`,
49
+ [machine, key]
50
+ );
51
+ return result.rows.map(mapRow);
52
+ },
53
+ async insert(row) {
54
+ const payload = typeof row.payload === 'string'
55
+ ? row.payload
56
+ : JSON.stringify(row.payload);
57
+ await pool.query(
58
+ `INSERT INTO user_decisions
59
+ (machine, start_time, username, operator_id, decided_at, payload)
60
+ VALUES ($1, $2, $3, $4, $5, $6)`,
61
+ [
62
+ row.machine,
63
+ row.startTime,
64
+ row.username,
65
+ row.operatorId ?? null,
66
+ row.decidedAt,
67
+ payload
68
+ ]
69
+ );
70
+ }
71
+ };
72
+ }
@@ -0,0 +1,24 @@
1
+ import pg from 'pg';
2
+
3
+ /**
4
+ * Creates a PostgreSQL pool for supervisor state reads.
5
+ *
6
+ * @param {object} [options] - optional existing pool or connection string
7
+ * @returns {object|undefined} pg pool
8
+ *
9
+ * @example
10
+ * const pool = postgresPool({ connectionString: process.env.SUPERVISOR_STATE_PG_URL });
11
+ */
12
+ export default function postgresPool(options = {}) {
13
+ if (options.pool) {
14
+ return options.pool;
15
+ }
16
+ const url = options.connectionString
17
+ || process.env.SUPERVISOR_STATE_PG_URL
18
+ || process.env.SUPERVISOR_STATE_DATABASE_URL
19
+ || process.env.POSTGRES_URL;
20
+ if (!url) {
21
+ return undefined;
22
+ }
23
+ return new pg.Pool({ connectionString: url });
24
+ }
@@ -0,0 +1,28 @@
1
+ import alertsStateMemory from './memory/alerts.js';
2
+ import checkpointStateMemory from './memory/checkpoints.js';
3
+ import metricsStateMemory from './memory/metrics.js';
4
+ import operationStateMemory from './memory/operations.js';
5
+ import segmentStateMemory from './memory/segments.js';
6
+
7
+ /**
8
+ * In-memory supervisor state bundle for tests and local runs.
9
+ *
10
+ * @param {object} [initial] - optional seed segments, metrics, alerts
11
+ * @returns {object} state data ports with shared seed store
12
+ */
13
+ export default function stateDataFromMemory(initial = {}) {
14
+ const store = {
15
+ segments: [...(initial.segments || [])],
16
+ metrics: [...(initial.metrics || [])],
17
+ alerts: [...(initial.alerts || [])],
18
+ operations: [...(initial.operations || [])]
19
+ };
20
+ return {
21
+ segments: segmentStateMemory(store),
22
+ metrics: metricsStateMemory(store),
23
+ alerts: alertsStateMemory(store),
24
+ checkpoints: checkpointStateMemory(store),
25
+ operations: operationStateMemory(store),
26
+ seed: store
27
+ };
28
+ }
@@ -0,0 +1,18 @@
1
+ import alertsStatePg from './pg/alerts.js';
2
+ import checkpointStatePg from './pg/checkpoints.js';
3
+ import metricsStateDisabled from './memory/metricsDisabled.js';
4
+ import metricsStatePg from './pg/metrics.js';
5
+ import operationStatePg from './pg/operations.js';
6
+ import segmentStatePg from './pg/segments.js';
7
+
8
+ export default function stateDataFromPool(pool, options = {}) {
9
+ const metricsEnabled = options.metricsEnabled !== false;
10
+ const metrics = metricsEnabled ? metricsStatePg(pool) : metricsStateDisabled();
11
+ return {
12
+ segments: segmentStatePg(pool),
13
+ alerts: alertsStatePg(pool),
14
+ metrics,
15
+ checkpoints: checkpointStatePg(pool, metricsEnabled),
16
+ operations: operationStatePg(pool)
17
+ };
18
+ }
@@ -0,0 +1,75 @@
1
+ import processingErrorLog from '../ingest/processingErrorLog.js';
2
+
3
+ function parseOccurredAt(value) {
4
+ const timestamp = new Date(value);
5
+ if (isNaN(timestamp.getTime())) {
6
+ throw new RangeError(`Invalid occurred_at: ${value}`);
7
+ }
8
+ return timestamp;
9
+ }
10
+
11
+ function requireString(parsed, field) {
12
+ if (typeof parsed[field] !== 'string') {
13
+ throw new Error(`Operation missing ${field} field`);
14
+ }
15
+ }
16
+
17
+ function requirePayload(parsed) {
18
+ if (typeof parsed.payload !== 'object' || parsed.payload === null || Array.isArray(parsed.payload)) {
19
+ throw new Error('Operation missing payload field');
20
+ }
21
+ }
22
+
23
+ function identity(parsed) {
24
+ requireString(parsed, 'machine');
25
+ requireString(parsed, 'kind');
26
+ requireString(parsed, 'external_key');
27
+ return {
28
+ machine: parsed.machine,
29
+ occurred_at: parseOccurredAt(parsed.occurred_at),
30
+ kind: parsed.kind,
31
+ key: parsed.external_key
32
+ };
33
+ }
34
+
35
+ async function decode(parsed, collector) {
36
+ const record = identity(parsed);
37
+ if (parsed.type === 'deleted') {
38
+ if (typeof collector.remove !== 'function') {
39
+ throw new Error('Collector must have a remove() method');
40
+ }
41
+ await collector.remove(record);
42
+ return;
43
+ }
44
+ requirePayload(parsed);
45
+ await collector.accept({ ...record, payload: parsed.payload });
46
+ }
47
+
48
+ /**
49
+ * Codec for converting AMQP operation sync messages to PostgreSQL-ready records.
50
+ *
51
+ * Upsert messages require payload. Deleted messages (type=deleted) call collector.remove.
52
+ *
53
+ * @param {object} collector - Collector with accept() and optional remove()
54
+ * @returns {object} Codec with accept() method
55
+ *
56
+ * @example
57
+ * const codec = operationCodec(collector);
58
+ * await codec.accept(Buffer.from('{"machine":"m1","occurred_at":"2024-06-01T10:00:00.000Z","kind":"chem","external_key":"nb-1","payload":{}}'));
59
+ * await codec.accept(Buffer.from('{"type":"deleted","machine":"m1","occurred_at":"2024-06-01T10:00:00.000Z","kind":"bath","external_key":"k"}'));
60
+ */
61
+ export default function operationCodec(collector) {
62
+ if (!collector || typeof collector.accept !== 'function') {
63
+ throw new Error('Collector must have an accept() method');
64
+ }
65
+ return {
66
+ async accept(content) {
67
+ try {
68
+ await decode(JSON.parse(content.toString()), collector);
69
+ } catch (error) {
70
+ processingErrorLog('operation_codec', error, { content: content.toString() });
71
+ throw error;
72
+ }
73
+ }
74
+ };
75
+ }
@@ -0,0 +1,82 @@
1
+ import amqp from 'amqplib';
2
+ import operationCodec from './operationCodec.js';
3
+ import operationSyncSink from './operationSyncSink.js';
4
+
5
+ /**
6
+ * Decodes one AMQP operation message body through the codec.
7
+ *
8
+ * @param {object} codec - operationCodec instance
9
+ * @param {Buffer} content - message body
10
+ * @returns {Promise<void>}
11
+ */
12
+ export function acceptOperationDeliver(codec, content) {
13
+ return codec.accept(content);
14
+ }
15
+
16
+ /**
17
+ * Builds a queue consumer callback bound to one AMQP channel.
18
+ *
19
+ * @param {object} codec - operationCodec instance
20
+ * @param {object} channel - amqplib channel with ack(msg)
21
+ * @returns {Function} consume callback for ch.consume
22
+ */
23
+ export function operationConsumer(codec, channel) {
24
+ return async (msg) => {
25
+ if (!msg) {
26
+ return;
27
+ }
28
+ await acceptOperationDeliver(codec, msg.content);
29
+ channel.ack(msg);
30
+ };
31
+ }
32
+
33
+ /**
34
+ * AMQP queue consumer that UPSERTs federated operation events into central PG.
35
+ *
36
+ * @param {string} amqpUrl - RabbitMQ AMQP URL
37
+ * @param {string} queue - Durable queue name to consume
38
+ * @param {object} operations - Operations port with upsert(item)
39
+ * @param {object} [options] - prefetch options
40
+ * @returns {object} consumer with start and stop
41
+ *
42
+ * @example
43
+ * const ingest = operationSyncIngest(amqpUrl, 'scada.operations.ingest', dataAccess.operations);
44
+ * await ingest.start();
45
+ */
46
+ export default function operationSyncIngest(amqpUrl, queue, operations, options = {}) {
47
+ const prefetch = options.prefetch || 32;
48
+ const sink = operationSyncSink(operations);
49
+ const codec = operationCodec(sink);
50
+ let session;
51
+ return {
52
+ async start() {
53
+ if (session) {
54
+ return;
55
+ }
56
+ const conn = await amqp.connect(amqpUrl);
57
+ const ch = await conn.createChannel();
58
+ await ch.assertQueue(queue, { durable: true });
59
+ ch.prefetch(prefetch);
60
+ const onMessage = operationConsumer(codec, ch);
61
+ const tag = await ch.consume(queue, onMessage, { noAck: false });
62
+ const started = { conn, channel: ch, tag: tag.consumerTag };
63
+ if (session) {
64
+ await ch.cancel(tag.consumerTag);
65
+ await ch.close();
66
+ await conn.close();
67
+ return;
68
+ }
69
+ session = started;
70
+ },
71
+ async stop() {
72
+ const active = session;
73
+ if (!active) {
74
+ return;
75
+ }
76
+ session = undefined;
77
+ await active.channel.cancel(active.tag);
78
+ await active.channel.close();
79
+ await active.conn.close();
80
+ }
81
+ };
82
+ }
@@ -0,0 +1,40 @@
1
+ import processingErrorLog from '../ingest/processingErrorLog.js';
2
+
3
+ /**
4
+ * PostgreSQL sink for generic operation sync records.
5
+ *
6
+ * @param {object} operations - Operations port with upsert(item) and remove(machineId, key)
7
+ * @returns {object} Sink with accept() and remove() methods
8
+ *
9
+ * @example
10
+ * const sink = operationSyncSink(dataAccess.operations);
11
+ * await sink.accept({ machine: 'm1', occurred_at: new Date(), kind: 'chem', key: 'nb-1', payload: {} });
12
+ * await sink.remove({ machine: 'm1', kind: 'chem', key: 'nb-1', occurred_at: new Date() });
13
+ */
14
+ export default function operationSyncSink(operations) {
15
+ return {
16
+ async accept(record) {
17
+ try {
18
+ await operations.upsert(record);
19
+ } catch (error) {
20
+ processingErrorLog('operation_sync_sink', error, {
21
+ machine: record.machine,
22
+ key: record.key
23
+ });
24
+ throw error;
25
+ }
26
+ },
27
+ async remove(record) {
28
+ try {
29
+ await operations.remove(record.machine, record.key);
30
+ } catch (error) {
31
+ processingErrorLog('operation_sync_sink', error, {
32
+ machine: record.machine,
33
+ key: record.key,
34
+ action: 'remove'
35
+ });
36
+ throw error;
37
+ }
38
+ }
39
+ };
40
+ }
@@ -1,54 +0,0 @@
1
- import meltingChronology from './meltingChronology.js';
2
- import completedMelting from './completedMelting.js';
3
-
4
- /**
5
- * In-progress melting session that can be stopped to produce a completed melting.
6
- * Chronology is bound to machine and derives values from machine's weight history.
7
- * Notifies callbacks when stopped or updated.
8
- *
9
- * @param {string} id - unique melting session identifier
10
- * @param {object} machine - the melting machine running this session
11
- * @param {Date} start - when the melting session started
12
- * @param {function} onStop - callback invoked with completedMelting when stopped
13
- * @param {function} onUpdate - callback invoked when melting is updated
14
- * @returns {object} active melting with id, machine, chronology, stop, update methods
15
- *
16
- * @example
17
- * const active = activeMelting('m1', machine, new Date(), onStop, onUpdate);
18
- * machine.load(500);
19
- * machine.dispense(480);
20
- * const completed = active.stop();
21
- */
22
- export default function activeMelting(id, machine, start, onStop, onUpdate) {
23
- return {
24
- id() {
25
- return id;
26
- },
27
- machine() {
28
- return machine;
29
- },
30
- chronology() {
31
- return meltingChronology(machine, start, undefined);
32
- },
33
- stop() {
34
- const end = new Date();
35
- const chron = meltingChronology(machine, start, end);
36
- const completed = completedMelting(id, machine, chron, onUpdate);
37
- onStop(completed);
38
- return completed;
39
- },
40
- update(data) {
41
- const opts = data === undefined ? {} : data;
42
- const newStart = opts.start === undefined ? start : new Date(opts.start);
43
- if (opts.end !== undefined) {
44
- const chron = meltingChronology(machine, newStart, new Date(opts.end));
45
- const completed = completedMelting(id, machine, chron, onUpdate);
46
- onStop(completed);
47
- return completed;
48
- }
49
- const updated = activeMelting(id, machine, newStart, onStop, onUpdate);
50
- onUpdate(updated);
51
- return updated;
52
- }
53
- };
54
- }
@@ -1,42 +0,0 @@
1
- import meltingChronology from './meltingChronology.js';
2
-
3
- /**
4
- * Immutable record of a finished melting session.
5
- * Chronology is bound to machine and derives values from machine's weight history.
6
- * Notifies callback when updated.
7
- *
8
- * @param {string} id - unique melting session identifier
9
- * @param {object} machine - the machine that performed this melting
10
- * @param {object} chron - chronology bound to machine with start/end times
11
- * @param {function} onUpdate - callback invoked when melting is updated
12
- * @returns {object} completed melting with id, machine, chronology, update methods
13
- *
14
- * @example
15
- * const completed = completedMelting('m1', machine, chron, onUpdate);
16
- * completed.id(); // 'm1'
17
- * completed.chronology().get().start; // start time
18
- * completed.chronology().get().end; // end time
19
- */
20
- export default function completedMelting(id, machine, chron, onUpdate) {
21
- return {
22
- id() {
23
- return id;
24
- },
25
- machine() {
26
- return machine;
27
- },
28
- chronology() {
29
- return chron;
30
- },
31
- update(data) {
32
- const opts = data === undefined ? {} : data;
33
- const original = chron.get();
34
- const newStart = opts.start === undefined ? original.start : new Date(opts.start);
35
- const newEnd = opts.end === undefined ? original.end : new Date(opts.end);
36
- const updated = meltingChronology(machine, newStart, newEnd);
37
- const result = completedMelting(id, machine, updated, onUpdate);
38
- onUpdate(result);
39
- return result;
40
- }
41
- };
42
- }
package/src/event.js DELETED
@@ -1,24 +0,0 @@
1
- /**
2
- * Immutable event record with properties and labels.
3
- * Represents a single occurrence in the system log.
4
- *
5
- * @param {string} id - unique event identifier
6
- * @param {Date} timestamp - when the event occurred
7
- * @param {object} properties - event-specific data
8
- * @param {string[]} labels - classification tags for the event
9
- * @returns {object} event with id, timestamp, properties, labels methods
10
- *
11
- * @example
12
- * const e = event('ev-1', new Date(), {machine: 'icht1', voltage: 340}, ['sensor']);
13
- * e.id(); // 'ev-1'
14
- * e.labels(); // ['sensor']
15
- * e.properties(); // {machine: 'icht1', voltage: 340}
16
- */
17
- export default function event(id, timestamp, properties, labels) {
18
- return {
19
- id: () => {return id},
20
- timestamp: () => {return timestamp},
21
- properties: () => {return properties},
22
- labels: () => {return [...labels]}
23
- };
24
- }
package/src/events.js DELETED
@@ -1,51 +0,0 @@
1
- import pubsub from './pubsub.js';
2
-
3
- /**
4
- * Append-only event log for system occurrences.
5
- * Events are immutable and never modified after creation.
6
- * Optionally evaluates rules when events are created.
7
- *
8
- * @param {function} factory - factory function to create event records
9
- * @param {object} rules - optional rules collection to evaluate on create
10
- * @returns {object} log with create, all, find, stream methods
11
- *
12
- * @example
13
- * const log = events(event, rules([rule1, rule2]));
14
- * const e = log.create(new Date(), {machine: 'icht1'}, ['sensor']);
15
- * log.all(); // returns all events
16
- * log.all((e) => e.labels().includes('sensor')); // filter events
17
- * log.find('ev-0'); // find by id
18
- * log.stream((evt) => console.log(evt)); // subscribe to new events
19
- */
20
- export default function events(factory, rules) {
21
- const items = [];
22
- const bus = pubsub();
23
- let counter = 0;
24
- return {
25
- create(timestamp, properties, labels) {
26
- const id = `ev-${counter}`;
27
- counter += 1;
28
- const tags = labels === undefined ? [] : labels;
29
- const e = factory(id, timestamp, properties, tags);
30
- items.push(e);
31
- bus.emit({ type: 'created', event: e });
32
- if (rules !== undefined) {
33
- rules.evaluate({ event: e });
34
- }
35
- return e;
36
- },
37
- all(...filters) {
38
- return items.filter((e) => {
39
- return filters.every((filter) => {
40
- return filter(e);
41
- });
42
- });
43
- },
44
- find(id) {
45
- return items.find((e) => {
46
- return e.id() === id;
47
- });
48
- },
49
- stream: bus.stream
50
- };
51
- }
package/src/interval.js DELETED
@@ -1,18 +0,0 @@
1
- /**
2
- * Periodic action executor wrapping setInterval.
3
- *
4
- * @param {number} period - interval in milliseconds
5
- * @param {function} action - callback to execute periodically
6
- * @returns {object} interval with start method
7
- *
8
- * @example
9
- * const i = interval(1000, function() { console.log('tick'); });
10
- * i.start();
11
- */
12
- export default function interval(period, action) {
13
- return {
14
- start() {
15
- setInterval(action, period);
16
- }
17
- };
18
- }
@@ -1,79 +0,0 @@
1
- /**
2
- * Immutable chronology that queries shared machine weight history.
3
- * Supports point-in-time and range queries via query object.
4
- * Optionally includes sensor readings in current snapshot.
5
- *
6
- * @param {number} initial - initial weight when machine was created
7
- * @param {array} history - shared mutable array of { timestamp, weight } entries
8
- * @param {object} sensors - optional sensor objects keyed by name with current() method
9
- * @returns {object} chronology with get method
10
- *
11
- * @example
12
- * const history = [{ timestamp: new Date(), weight: 0 }];
13
- * const chron = machineChronology(0, history, { voltage: sensor });
14
- * chron.get({ type: 'current' }).weight; // current weight
15
- * chron.get({ type: 'current' }).voltage; // current voltage reading
16
- * chron.get({ type: 'point', at: someDate }).weight; // weight at someDate
17
- * chron.get({ type: 'range', from: start, to: end }); // { loaded, dispensed }
18
- */
19
- // eslint-disable-next-line max-lines-per-function
20
- export default function machineChronology(initial, history, sensors) {
21
- async function current() {
22
- const result = { weight: history[history.length - 1].weight };
23
- if (sensors) {
24
- const keys = Object.keys(sensors);
25
- const values = await Promise.all(keys.map((key) => {
26
- return sensors[key].current();
27
- }));
28
- keys.forEach((key, i) => {
29
- result[key] = values[i].found ? values[i] : { value: 0, unit: '' };
30
- });
31
- }
32
- return result;
33
- }
34
- function point(datetime) {
35
- const time = new Date(datetime).getTime();
36
- for (let i = history.length - 1; i >= 0; i -= 1) {
37
- if (history[i].timestamp.getTime() <= time) {
38
- return { weight: history[i].weight };
39
- }
40
- }
41
- return { weight: initial };
42
- }
43
- function range(from, to) {
44
- const start = new Date(from).getTime();
45
- const end = new Date(to).getTime();
46
- const entries = history.filter((entry) => {
47
- const time = entry.timestamp.getTime();
48
- return time >= start && time < end;
49
- });
50
- let loaded = 0;
51
- let dispensed = 0;
52
- const base = point(from).weight;
53
- let previous = base;
54
- for (const entry of entries) {
55
- const delta = entry.weight - previous;
56
- if (delta > 0) {
57
- loaded += delta;
58
- } else if (delta < 0) {
59
- dispensed += Math.abs(delta);
60
- }
61
- previous = entry.weight;
62
- }
63
- return { loaded, dispensed };
64
- }
65
- return {
66
- get(query) {
67
- if (query.type === 'current') {
68
- return current();
69
- }
70
- if (query.type === 'point') {
71
- return point(query.at);
72
- }
73
- if (query.type === 'range') {
74
- return range(query.from, query.to);
75
- }
76
- throw new Error(`Unknown query type: ${query.type}`);
77
- }
78
- };
79
- }
@@ -1,37 +0,0 @@
1
- /**
2
- * Melting chronology bound to a machine.
3
- * Derives values from machine's weight history during the melting window.
4
- *
5
- * @param {object} machine - melting machine with chronology method
6
- * @param {Date} start - melting start time
7
- * @param {Date} end - melting end time (undefined for active meltings)
8
- * @returns {object} chronology with get method
9
- *
10
- * @example
11
- * const chron = meltingChronology(machine, startTime, endTime);
12
- * chron.get(); // { start, end, initial, weight, loaded, dispensed }
13
- * chron.get(someTime); // state at specific time
14
- */
15
- export default function meltingChronology(machine, start, end) {
16
- return {
17
- get(datetime) {
18
- const machineChron = machine.chronology();
19
- const defaultTime = end === undefined ? new Date() : end;
20
- const queryTime = datetime === undefined ? defaultTime : datetime;
21
- const initial = machineChron.get({ type: 'point', at: start }).weight;
22
- const current = machineChron.get({ type: 'point', at: queryTime }).weight;
23
- const range = machineChron.get({ type: 'range', from: start, to: queryTime });
24
- const result = {
25
- start,
26
- initial,
27
- weight: current,
28
- loaded: range.loaded,
29
- dispensed: range.dispensed
30
- };
31
- if (end !== undefined) {
32
- result.end = end;
33
- }
34
- return result;
35
- }
36
- };
37
- }