@yarkivaev/scada 1.4.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.
- package/README.md +196 -33
- package/db/migrations/C0001__central_drop_metrics.sql +1 -0
- package/db/migrations/C0002__central_revoke_delete.sql +12 -0
- package/db/migrations/C0003__central_operators.sql +7 -0
- package/db/migrations/C0004__central_operations_grants.sql +2 -0
- package/db/migrations/C0005__central_operators_grants.sql +1 -0
- package/db/migrations/C0006__central_operators_registration.sql +16 -0
- package/db/migrations/E0001__edge_metrics.sql +8 -0
- package/db/migrations/E0002__edge_retention_delete.sql +8 -0
- package/db/migrations/E0003__edge_operations_grants.sql +2 -0
- package/db/migrations/V0001__baseline.sql +25 -0
- package/db/migrations/V0002__sink_extension_tables.sql +6 -0
- package/db/migrations/V0003__operations.sql +10 -0
- package/db/migrations/V0004__ingest_checkpoints.sql +6 -0
- package/db/migrations/V0005__user_decisions_operator.sql +2 -0
- package/index.js +75 -45
- package/package.json +28 -8
- package/src/application/bindSilentStreams.js +20 -0
- package/src/application/edgeOperatorCatalog.js +45 -0
- package/src/application/export/exportJob.js +118 -0
- package/src/application/export/exportQuery.js +32 -0
- package/src/application/export/exportSink.js +25 -0
- package/src/application/export/exportStream.js +33 -0
- package/src/application/machineInPlant.js +20 -0
- package/src/application/metricsPlant.js +35 -0
- package/src/application/plantApi.js +49 -0
- package/src/application/plantOperations.js +19 -0
- package/src/application/plantServer.js +107 -0
- package/src/application/shopWithTimeline.js +112 -0
- package/src/application/siteOperatorCatalog.js +78 -0
- package/src/application/siteServer.js +191 -0
- package/src/application/supervisorSink.js +97 -0
- package/src/application/timelineOperatorFromEnv.js +19 -0
- package/src/bin/supervisor-sink.js +28 -0
- package/src/{alert.js → domain/alerting/alert.js} +2 -2
- package/src/{alerts.js → domain/alerting/alerts.js} +3 -3
- package/src/domain/alerting/ingest.js +18 -0
- package/src/domain/ingest/ingestCursor.js +26 -0
- package/src/domain/operation/operation.js +23 -0
- package/src/domain/operation/operations.js +81 -0
- package/src/domain/operator/operator.js +23 -0
- package/src/domain/plant/machine.js +32 -0
- package/src/domain/plant/plant.js +20 -0
- package/src/domain/plant/shop.js +24 -0
- package/src/domain/segment/dispatch.js +31 -0
- package/src/domain/segment/normalize.js +31 -0
- package/src/domain/segment/silenceBudget.js +16 -0
- package/src/{initialized.js → domain/shared/initialized.js} +3 -4
- package/src/{pubsub.js → domain/shared/pubsub.js} +2 -3
- package/src/domain/timeline/timeline.js +25 -0
- package/src/infrastructure/catalog/tag-catalog.json +27 -0
- package/src/infrastructure/catalog/tagCatalog.js +65 -0
- package/src/infrastructure/client/index.js +20 -0
- package/src/infrastructure/client/machineClient.js +159 -0
- package/src/infrastructure/client/machineOperationsClient.js +159 -0
- package/src/infrastructure/client/scadaClient.js +67 -0
- package/src/infrastructure/client/sseConnection.js +42 -0
- package/src/infrastructure/http/edge/edgeApi.js +54 -0
- package/src/infrastructure/http/edge/httpMetricsRead.js +34 -0
- package/src/infrastructure/http/edge/metricsSensor.js +15 -0
- package/src/infrastructure/http/edge/parseStateHttpTimeoutMs.js +20 -0
- package/src/infrastructure/http/edge/routes/checkpoint/checkpointRoutes.js +79 -0
- package/src/infrastructure/http/edge/routes/metrics/metricsBatch.js +59 -0
- package/src/infrastructure/http/edge/routes/metrics/metricsCurrent.js +29 -0
- package/src/infrastructure/http/edge/routes/metrics/metricsPoll.js +25 -0
- package/src/infrastructure/http/edge/routes/metrics/metricsRange.js +26 -0
- package/src/infrastructure/http/edge/routes/metrics/metricsRoutes.js +21 -0
- package/src/infrastructure/http/edge/routes/retention/retentionRoutes.js +41 -0
- package/src/infrastructure/http/edge/startTestEdgeApi.js +39 -0
- package/src/infrastructure/http/edge/stateAccess.js +13 -0
- package/src/infrastructure/http/edge/stateHttpClient.js +106 -0
- package/src/infrastructure/http/edge/stateHttpTimeoutError.js +13 -0
- package/src/infrastructure/http/plant/json/decisionJson.js +44 -0
- package/src/infrastructure/http/plant/json/operationJson.js +28 -0
- package/src/infrastructure/http/plant/json/operatorJson.js +22 -0
- package/src/infrastructure/http/plant/json/segmentJson.js +27 -0
- package/src/infrastructure/http/plant/operationAudit.js +56 -0
- package/src/infrastructure/http/plant/routes/alertRoute.js +93 -0
- package/src/infrastructure/http/plant/routes/catalogRoute.js +21 -0
- package/src/infrastructure/http/plant/routes/decisionRoute.js +60 -0
- package/src/infrastructure/http/plant/routes/machineRoute.js +35 -0
- package/src/infrastructure/http/plant/routes/measurementRoute.js +60 -0
- package/src/infrastructure/http/plant/routes/operationDrafts.js +79 -0
- package/src/infrastructure/http/plant/routes/operationRoute.js +80 -0
- package/src/infrastructure/http/plant/routes/operationWrites.js +186 -0
- package/src/infrastructure/http/plant/routes/operatorRoute.js +143 -0
- package/src/infrastructure/http/plant/routes/simulationRoute.js +61 -0
- package/src/infrastructure/http/plant/routes/timelineRoute.js +112 -0
- package/src/infrastructure/http/plant/streams/alertStream.js +68 -0
- package/src/infrastructure/http/plant/streams/heartbeatStream.js +34 -0
- package/src/infrastructure/http/plant/streams/measurementStream.js +90 -0
- package/src/infrastructure/http/plant/streams/operationStream.js +42 -0
- package/src/infrastructure/http/plant/streams/timelineStream.js +97 -0
- package/src/infrastructure/http/plant/timelineOperator.js +87 -0
- package/src/infrastructure/ingest/activity/activityTracking.js +74 -0
- package/src/infrastructure/ingest/activity/activityTransformer.js +45 -0
- package/src/infrastructure/ingest/codecs/alertCodec.js +56 -0
- package/src/infrastructure/ingest/codecs/segmentCodec.js +30 -0
- package/src/infrastructure/ingest/codecs/userDecisionCodec.js +78 -0
- package/src/infrastructure/ingest/cooldown.js +24 -0
- package/src/infrastructure/ingest/db/migrate.js +78 -0
- package/src/infrastructure/ingest/db/migrationProfile.js +35 -0
- package/src/infrastructure/ingest/db/runRetention.js +58 -0
- package/src/infrastructure/ingest/ingestCheckpoint.js +71 -0
- package/src/infrastructure/ingest/modbus/mx210Tcp.js +81 -0
- package/src/infrastructure/ingest/modbus/silentStreams.js +152 -0
- package/src/infrastructure/ingest/mqtt/metricsTransformer.js +54 -0
- package/src/infrastructure/ingest/mqtt/modbusDeviceSpec.js +112 -0
- package/src/infrastructure/ingest/mqtt/modbusMqtt.js +204 -0
- package/src/infrastructure/ingest/mqtt/mqttMetrics.js +78 -0
- package/src/infrastructure/ingest/parseRequestTimeoutMs.js +20 -0
- package/src/infrastructure/ingest/pipelines/alertPipeline.js +48 -0
- package/src/infrastructure/ingest/pipelines/decisionPipeline.js +45 -0
- package/src/infrastructure/ingest/pipelines/segmentPipeline.js +60 -0
- package/src/infrastructure/ingest/processingErrorLog.js +25 -0
- package/src/infrastructure/ingest/silentOpenWatch.js +50 -0
- package/src/infrastructure/ingest/sinks/alertSink.js +43 -0
- package/src/infrastructure/ingest/sinks/closeOrphanOpen.js +32 -0
- package/src/infrastructure/ingest/sinks/closeSilentOpen.js +39 -0
- package/src/infrastructure/ingest/sinks/retagSink.js +43 -0
- package/src/infrastructure/ingest/sinks/userDecisionSink.js +41 -0
- package/src/infrastructure/ingest/telemetry/amqpMetricsIngest.js +94 -0
- package/src/infrastructure/ingest/telemetry/amqpMqttRelay.js +71 -0
- package/src/infrastructure/ingest/telemetry/deliverToMqttRecord.js +19 -0
- package/src/infrastructure/ingest/telemetry/streamNameFromTopic.js +21 -0
- package/src/infrastructure/messaging/ownership/httpOperations.js +96 -0
- package/src/infrastructure/messaging/ownership/httpTimeline.js +99 -0
- package/src/infrastructure/messaging/ownership/machineOwners.js +39 -0
- package/src/infrastructure/messaging/ownership/ownerTimeline.js +28 -0
- package/src/infrastructure/messaging/stomp/alerts/stompAlerts.js +21 -0
- package/src/infrastructure/messaging/stomp/alerts/stompAlertsCollection.js +51 -0
- package/src/infrastructure/messaging/stomp/alerts/stompAlertsInit.js +24 -0
- package/src/infrastructure/messaging/stomp/alerts/stompAlertsLogic.js +51 -0
- package/src/infrastructure/messaging/stomp/stompTimelineSegments.js +80 -0
- package/src/infrastructure/messaging/stomp/timeline.js +21 -0
- package/src/infrastructure/messaging/stomp/userDecisionBody.js +25 -0
- package/src/infrastructure/messaging/stomp/userDecisions.js +42 -0
- package/src/infrastructure/operators/centralOperators.js +64 -0
- package/src/infrastructure/operators/edgeOperators.js +39 -0
- package/src/infrastructure/operators/operatorById.js +33 -0
- package/src/infrastructure/operators/operatorExtras.js +41 -0
- package/src/infrastructure/operators/operators.js +32 -0
- package/src/infrastructure/operators/operatorsFromSeed.js +18 -0
- package/src/infrastructure/operators/operatorsSync.js +57 -0
- package/src/infrastructure/persistence/clickhouse/connection.js +58 -0
- package/src/infrastructure/persistence/clickhouse/pollTopicCursors.js +48 -0
- package/src/{clickhouseSensor.js → infrastructure/persistence/clickhouse/sensor.js} +9 -27
- package/src/infrastructure/persistence/clickhouse/streamHub.js +165 -0
- package/src/infrastructure/persistence/memory/alerts.js +21 -0
- package/src/infrastructure/persistence/memory/checkpoints.js +98 -0
- package/src/infrastructure/persistence/memory/metrics.js +69 -0
- package/src/infrastructure/persistence/memory/metricsDisabled.js +19 -0
- package/src/infrastructure/persistence/memory/operations.js +87 -0
- package/src/infrastructure/persistence/memory/segments.js +83 -0
- package/src/infrastructure/persistence/memory/timeline.js +56 -0
- package/src/infrastructure/persistence/metricsSensor.js +112 -0
- package/src/infrastructure/persistence/pg/alerts.js +25 -0
- package/src/infrastructure/persistence/pg/checkpoints.js +115 -0
- package/src/infrastructure/persistence/pg/metrics.js +73 -0
- package/src/infrastructure/persistence/pg/operations.js +95 -0
- package/src/infrastructure/persistence/pg/operators.js +118 -0
- package/src/infrastructure/persistence/pg/segments.js +48 -0
- package/src/infrastructure/persistence/pg/timeline.js +53 -0
- package/src/infrastructure/persistence/pg/userDecisions.js +72 -0
- package/src/infrastructure/persistence/postgresPool.js +24 -0
- package/src/infrastructure/persistence/stateDataFromMemory.js +28 -0
- package/src/infrastructure/persistence/stateDataFromPool.js +18 -0
- package/src/infrastructure/sync/operationCodec.js +75 -0
- package/src/infrastructure/sync/operationSyncIngest.js +82 -0
- package/src/infrastructure/sync/operationSyncSink.js +40 -0
- package/src/activeMelting.js +0 -54
- package/src/completedMelting.js +0 -42
- package/src/event.js +0 -24
- package/src/events.js +0 -51
- package/src/interval.js +0 -18
- package/src/machineChronology.js +0 -79
- package/src/meltingChronology.js +0 -37
- package/src/meltingMachine.js +0 -53
- package/src/meltingRuleEngine.js +0 -37
- package/src/meltingShop.js +0 -32
- package/src/meltings.js +0 -97
- package/src/monitoredMeltingMachine.js +0 -33
- package/src/plant.js +0 -23
- package/src/requests.js +0 -44
- package/src/rule.js +0 -33
- package/src/rules.js +0 -23
- package/src/scyllaSensor.js +0 -54
- package/src/segments.js +0 -64
- 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
|
+
}
|
package/src/activeMelting.js
DELETED
|
@@ -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
|
-
}
|
package/src/completedMelting.js
DELETED
|
@@ -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
|
-
}
|
package/src/machineChronology.js
DELETED
|
@@ -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
|
-
}
|
package/src/meltingChronology.js
DELETED
|
@@ -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
|
-
}
|