@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
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Reads sensor measurements from scada.metrics table
|
|
5
5
|
* and provides real-time streaming via polling.
|
|
6
6
|
* Supports time-based downsampling using ClickHouse aggregation.
|
|
7
|
-
*
|
|
7
|
+
* Live streams share a connection-level batch hub so many sensors
|
|
8
|
+
* cost one ClickHouse query per tick instead of one query per sensor.
|
|
8
9
|
*
|
|
9
10
|
* @param {object} connection - ClickHouse connection with query method
|
|
10
11
|
* @param {string} topic - Metric topic in format '{machine}/{sensor}'
|
|
@@ -13,17 +14,18 @@
|
|
|
13
14
|
* @returns {object} sensor with name, current, measurements and stream methods
|
|
14
15
|
*
|
|
15
16
|
* @example
|
|
16
|
-
* const sensor = clickhouseSensor(conn, '
|
|
17
|
+
* const sensor = clickhouseSensor(conn, 'm1/voltage', 'Voltage', 'V');
|
|
17
18
|
* sensor.name(); // 'Voltage'
|
|
18
19
|
* await sensor.current(); // { found: true, timestamp, value, unit } or { found: false }
|
|
19
20
|
* await sensor.measurements({ start, end }, 60000); // downsampled to 1-minute intervals
|
|
20
21
|
* sensor.stream(since, 1000, callback); // live stream
|
|
21
22
|
*/
|
|
23
|
+
import clickhouseStreamHub from './streamHub.js';
|
|
24
|
+
|
|
22
25
|
function formatDateTime(date) {
|
|
23
26
|
return date.toISOString().replace('Z', '').replace('T', ' ');
|
|
24
27
|
}
|
|
25
28
|
|
|
26
|
-
// eslint-disable-next-line max-lines-per-function
|
|
27
29
|
export default function clickhouseSensor(connection, topic, displayName, unit) {
|
|
28
30
|
return {
|
|
29
31
|
name() {
|
|
@@ -64,30 +66,10 @@ export default function clickhouseSensor(connection, topic, displayName, unit) {
|
|
|
64
66
|
});
|
|
65
67
|
},
|
|
66
68
|
stream(since, step, callback, clock) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const rows = await connection.query(
|
|
72
|
-
`SELECT ts, value FROM scada.metrics
|
|
73
|
-
WHERE topic = {topic:String} AND ts > {since:DateTime64(3)} AND ts <= {until:DateTime64(3)}
|
|
74
|
-
ORDER BY ts LIMIT 100`,
|
|
75
|
-
{ topic, since: formatDateTime(lastTs), until: formatDateTime(time()) }
|
|
76
|
-
);
|
|
77
|
-
rows.forEach((row) => {
|
|
78
|
-
const timestamp = new Date(`${row.ts}Z`);
|
|
79
|
-
callback({ timestamp, value: row.value, unit });
|
|
80
|
-
lastTs = timestamp;
|
|
81
|
-
});
|
|
82
|
-
} catch (err) {
|
|
83
|
-
console.error(`Stream query failed for ${topic} on ${connection.url()}:`, err.message); // eslint-disable-line no-console
|
|
84
|
-
}
|
|
85
|
-
}, step);
|
|
86
|
-
return {
|
|
87
|
-
cancel() {
|
|
88
|
-
clearInterval(timer);
|
|
89
|
-
}
|
|
90
|
-
};
|
|
69
|
+
return clickhouseStreamHub(connection).watch(topic, since, step, callback, {
|
|
70
|
+
unit,
|
|
71
|
+
clock
|
|
72
|
+
});
|
|
91
73
|
}
|
|
92
74
|
};
|
|
93
75
|
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared ClickHouse stream hub.
|
|
3
|
+
*
|
|
4
|
+
* Coalesces many topic watchers on one connection into one batched poll per
|
|
5
|
+
* step interval, cutting ClickHouse CPU from O(sensors) to O(pulses).
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const hub = clickhouseStreamHub(connection);
|
|
9
|
+
* const sub = hub.watch('m1/V', since, 1000, onRow, { unit: 'V', clock });
|
|
10
|
+
* sub.cancel();
|
|
11
|
+
*/
|
|
12
|
+
import pollTopicCursors from './pollTopicCursors.js';
|
|
13
|
+
|
|
14
|
+
const hubs = new WeakMap();
|
|
15
|
+
|
|
16
|
+
function defaultSchedule(step, fn) {
|
|
17
|
+
const timer = setInterval(fn, step);
|
|
18
|
+
return () => {
|
|
19
|
+
clearInterval(timer);
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parseTimestamp(ts) {
|
|
24
|
+
return new Date(`${ts}Z`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function bucketKey(step) {
|
|
28
|
+
return String(step);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function createBucket(hub, step, clock) {
|
|
32
|
+
return {
|
|
33
|
+
step,
|
|
34
|
+
clock,
|
|
35
|
+
watchers: new Map(),
|
|
36
|
+
stop: hub.schedule(step, () => {
|
|
37
|
+
return hub.pulse(step);
|
|
38
|
+
})
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function addWatcher(bucket, topic, watcher) {
|
|
43
|
+
if (!bucket.watchers.has(topic)) {
|
|
44
|
+
bucket.watchers.set(topic, new Set());
|
|
45
|
+
}
|
|
46
|
+
bucket.watchers.get(topic).add(watcher);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function removeWatcher(bucket, topic, watcher) {
|
|
50
|
+
const group = bucket.watchers.get(topic);
|
|
51
|
+
if (!group) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
group.delete(watcher);
|
|
55
|
+
if (group.size === 0) {
|
|
56
|
+
bucket.watchers.delete(topic);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function cursorsOf(bucket) {
|
|
61
|
+
const oldest = new Map();
|
|
62
|
+
bucket.watchers.forEach((group, topic) => {
|
|
63
|
+
group.forEach((watcher) => {
|
|
64
|
+
const prev = oldest.get(topic);
|
|
65
|
+
if (!prev || watcher.lastTs.getTime() < prev.getTime()) {
|
|
66
|
+
oldest.set(topic, watcher.lastTs);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
const cursors = [];
|
|
71
|
+
oldest.forEach((since, topic) => {
|
|
72
|
+
cursors.push({ topic, since });
|
|
73
|
+
});
|
|
74
|
+
return cursors;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function dispatch(bucket, rows) {
|
|
78
|
+
rows.forEach((row) => {
|
|
79
|
+
const group = bucket.watchers.get(row.topic);
|
|
80
|
+
if (!group) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const timestamp = parseTimestamp(row.ts);
|
|
84
|
+
group.forEach((watcher) => {
|
|
85
|
+
if (timestamp.getTime() <= watcher.lastTs.getTime()) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
watcher.callback({ timestamp, value: row.value, unit: watcher.unit });
|
|
89
|
+
watcher.lastTs = timestamp;
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function buildHub(connection, schedule) {
|
|
95
|
+
const buckets = new Map();
|
|
96
|
+
const hub = {
|
|
97
|
+
schedule,
|
|
98
|
+
async pulse(step) {
|
|
99
|
+
const bucket = buckets.get(bucketKey(step));
|
|
100
|
+
if (!bucket || bucket.watchers.size === 0) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const rows = await pollTopicCursors(
|
|
105
|
+
connection,
|
|
106
|
+
cursorsOf(bucket),
|
|
107
|
+
bucket.clock()
|
|
108
|
+
);
|
|
109
|
+
dispatch(bucket, rows);
|
|
110
|
+
} catch (err) {
|
|
111
|
+
console.error( // eslint-disable-line no-console
|
|
112
|
+
`Stream batch failed on ${connection.url()}:`,
|
|
113
|
+
err.message
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
watch(topic, since, step, callback, options) {
|
|
118
|
+
const key = bucketKey(step);
|
|
119
|
+
if (!buckets.has(key)) {
|
|
120
|
+
buckets.set(key, createBucket(hub, step, options.clock || (() => {
|
|
121
|
+
return new Date();
|
|
122
|
+
})));
|
|
123
|
+
}
|
|
124
|
+
const bucket = buckets.get(key);
|
|
125
|
+
const watcher = { lastTs: since, callback, unit: options.unit };
|
|
126
|
+
addWatcher(bucket, topic, watcher);
|
|
127
|
+
return {
|
|
128
|
+
cancel() {
|
|
129
|
+
removeWatcher(bucket, topic, watcher);
|
|
130
|
+
if (bucket.watchers.size === 0) {
|
|
131
|
+
bucket.stop();
|
|
132
|
+
buckets.delete(key);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
return hub;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Creates a stream hub for connection, replacing any previous hub.
|
|
143
|
+
*
|
|
144
|
+
* @param {object} connection - shared ClickHouse connection
|
|
145
|
+
* @param {function} [schedule] - (step, fn) => cancel, for tests
|
|
146
|
+
* @returns {object} hub with watch(topic, since, step, callback, options)
|
|
147
|
+
*/
|
|
148
|
+
export function createClickhouseStreamHub(connection, schedule) {
|
|
149
|
+
const hub = buildHub(connection, schedule || defaultSchedule);
|
|
150
|
+
hubs.set(connection, hub);
|
|
151
|
+
return hub;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Returns the shared hub for connection, creating one if needed.
|
|
156
|
+
*
|
|
157
|
+
* @param {object} connection - shared ClickHouse connection
|
|
158
|
+
* @returns {object} hub with watch(topic, since, step, callback, options)
|
|
159
|
+
*/
|
|
160
|
+
export default function clickhouseStreamHub(connection) {
|
|
161
|
+
if (!hubs.has(connection)) {
|
|
162
|
+
return createClickhouseStreamHub(connection, defaultSchedule);
|
|
163
|
+
}
|
|
164
|
+
return hubs.get(connection);
|
|
165
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory alert hydration port for tests and local runs.
|
|
3
|
+
*
|
|
4
|
+
* @param {object} store - shared mutable store with alerts array
|
|
5
|
+
* @returns {object} alerts port with listUnacknowledged
|
|
6
|
+
*/
|
|
7
|
+
export default function alertsStateMemory(store) {
|
|
8
|
+
return {
|
|
9
|
+
listUnacknowledged(filters) {
|
|
10
|
+
let rows = store.alerts.filter((row) => {
|
|
11
|
+
return row.acknowledged === false;
|
|
12
|
+
});
|
|
13
|
+
if (filters.machine) {
|
|
14
|
+
rows = rows.filter((row) => {
|
|
15
|
+
return row.machine === filters.machine;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
return rows;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
function parseJsonField(raw) {
|
|
2
|
+
if (!raw) {
|
|
3
|
+
return null;
|
|
4
|
+
}
|
|
5
|
+
return JSON.parse(raw);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function segmentItem(row) {
|
|
9
|
+
if (!row) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
machine: row.machine,
|
|
14
|
+
name: row.name,
|
|
15
|
+
start: new Date(row.start_time).getTime() / 1000,
|
|
16
|
+
end: new Date(row.end_time).getTime() / 1000,
|
|
17
|
+
duration: row.duration,
|
|
18
|
+
tags: parseJsonField(row.tags),
|
|
19
|
+
options: parseJsonField(row.options),
|
|
20
|
+
properties: parseJsonField(row.properties)
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function segmentAt(store, machineId, startEpoch) {
|
|
25
|
+
const startMs = startEpoch * 1000;
|
|
26
|
+
const row = store.segments.find((item) => {
|
|
27
|
+
return item.machine === machineId && new Date(item.start_time).getTime() === startMs;
|
|
28
|
+
});
|
|
29
|
+
return segmentItem(row);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function replayCursorFor(store, machineId) {
|
|
33
|
+
const closed = store.segments.filter((row) => {
|
|
34
|
+
return row.machine === machineId && Number(row.duration) > 0;
|
|
35
|
+
}).map((row) => {
|
|
36
|
+
return new Date(row.end_time).getTime() / 1000;
|
|
37
|
+
});
|
|
38
|
+
const pending = store.segments.filter((row) => {
|
|
39
|
+
return row.machine === machineId && Number(row.duration) === 0;
|
|
40
|
+
}).map((row) => {
|
|
41
|
+
return new Date(row.start_time).getTime() / 1000;
|
|
42
|
+
});
|
|
43
|
+
const closedMax = closed.length ? Math.max(...closed) : null;
|
|
44
|
+
const pendingMax = pending.length ? Math.max(...pending) : null;
|
|
45
|
+
let cursor = 0;
|
|
46
|
+
if (closedMax !== null && pendingMax !== null) {
|
|
47
|
+
cursor = Math.min(closedMax, pendingMax);
|
|
48
|
+
} else if (pendingMax !== null) {
|
|
49
|
+
cursor = pendingMax;
|
|
50
|
+
} else if (closedMax !== null) {
|
|
51
|
+
cursor = closedMax;
|
|
52
|
+
}
|
|
53
|
+
return cursor;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* In-memory checkpoint state port for tests and local runs.
|
|
58
|
+
*
|
|
59
|
+
* @param {object} store - shared mutable store with segments and metrics arrays
|
|
60
|
+
* @returns {object} checkpoints port matching checkpointStatePg shape
|
|
61
|
+
*/
|
|
62
|
+
export default function checkpointStateMemory(store) {
|
|
63
|
+
return {
|
|
64
|
+
replayCursor(machineId) {
|
|
65
|
+
return replayCursorFor(store, machineId);
|
|
66
|
+
},
|
|
67
|
+
pendingSegments() {
|
|
68
|
+
return store.segments.filter((row) => {
|
|
69
|
+
return Number(row.duration) === 0;
|
|
70
|
+
}).map((row) => {
|
|
71
|
+
return {
|
|
72
|
+
machine: row.machine,
|
|
73
|
+
name: row.name,
|
|
74
|
+
start: new Date(row.start_time).getTime() / 1000,
|
|
75
|
+
end: new Date(row.end_time).getTime() / 1000
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
readings(topics, fromEpoch) {
|
|
80
|
+
const fromMs = Number(fromEpoch) * 1000;
|
|
81
|
+
return store.metrics.filter((row) => {
|
|
82
|
+
const tsMs = new Date(row.ts).getTime();
|
|
83
|
+
return topics.includes(row.topic) && tsMs > fromMs;
|
|
84
|
+
}).sort((a, b) => {
|
|
85
|
+
return new Date(a.ts) - new Date(b.ts);
|
|
86
|
+
}).map((row) => {
|
|
87
|
+
return {
|
|
88
|
+
topic: row.topic,
|
|
89
|
+
timestamp: new Date(row.ts).getTime() / 1000,
|
|
90
|
+
value: Number(row.value)
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
segment(machineId, startEpoch) {
|
|
95
|
+
return segmentAt(store, machineId, startEpoch);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
function bucketKey(tsMs, originMs, stepMs) {
|
|
2
|
+
const slot = Math.floor((tsMs - originMs) / stepMs);
|
|
3
|
+
return originMs + slot * stepMs;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
function rowsForRange(store, topic, startMs, endMs, stepMs) {
|
|
7
|
+
const hits = store.metrics.filter((row) => {
|
|
8
|
+
const tsMs = new Date(row.ts).getTime();
|
|
9
|
+
return row.topic === topic && tsMs >= startMs && tsMs <= endMs;
|
|
10
|
+
}).sort((a, b) => {
|
|
11
|
+
return new Date(a.ts) - new Date(b.ts);
|
|
12
|
+
});
|
|
13
|
+
const byBucket = new Map();
|
|
14
|
+
hits.forEach((row) => {
|
|
15
|
+
const tsMs = new Date(row.ts).getTime();
|
|
16
|
+
const key = bucketKey(tsMs, startMs, stepMs);
|
|
17
|
+
byBucket.set(key, row);
|
|
18
|
+
});
|
|
19
|
+
return Array.from(byBucket.entries()).sort((a, b) => {
|
|
20
|
+
return a[0] - b[0];
|
|
21
|
+
}).map((entry) => {
|
|
22
|
+
return { ts: new Date(entry[0]), value: entry[1].value };
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* In-memory metrics state port for tests and local runs.
|
|
28
|
+
*
|
|
29
|
+
* @param {object} store - shared mutable store with metrics array
|
|
30
|
+
* @returns {object} metrics port matching metricsStatePg shape
|
|
31
|
+
*/
|
|
32
|
+
export default function metricsStateMemory(store) {
|
|
33
|
+
return {
|
|
34
|
+
latestForTopic(topic) {
|
|
35
|
+
const hits = store.metrics.filter((row) => {
|
|
36
|
+
return row.topic === topic;
|
|
37
|
+
}).sort((a, b) => {
|
|
38
|
+
return new Date(b.ts) - new Date(a.ts);
|
|
39
|
+
});
|
|
40
|
+
return hits[0] ?? null;
|
|
41
|
+
},
|
|
42
|
+
rangeForTopic(topic, startIso, endIso, stepMs) {
|
|
43
|
+
const start = new Date(startIso).getTime();
|
|
44
|
+
const end = new Date(endIso).getTime();
|
|
45
|
+
const stepSec = Math.max(1, Math.floor(stepMs / 1000));
|
|
46
|
+
return rowsForRange(store, topic, start, end, stepSec * 1000);
|
|
47
|
+
},
|
|
48
|
+
pollTopic(topic, afterIso, untilIso) {
|
|
49
|
+
const after = new Date(afterIso).getTime();
|
|
50
|
+
const until = new Date(untilIso).getTime();
|
|
51
|
+
return store.metrics.filter((row) => {
|
|
52
|
+
const tsMs = new Date(row.ts).getTime();
|
|
53
|
+
return row.topic === topic && tsMs > after && tsMs <= until;
|
|
54
|
+
}).sort((a, b) => {
|
|
55
|
+
return new Date(a.ts) - new Date(b.ts);
|
|
56
|
+
}).slice(0, 100).map((row) => {
|
|
57
|
+
return { ts: row.ts, value: row.value };
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
insertRows(items) {
|
|
61
|
+
let i = 0;
|
|
62
|
+
while (i < items.length) {
|
|
63
|
+
const row = items[i];
|
|
64
|
+
store.metrics.push({ topic: row.topic, ts: new Date(row.ts), value: row.value });
|
|
65
|
+
i += 1;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metrics port used when SINK_DB_PROFILE is central (ClickHouse owns metrics).
|
|
3
|
+
*/
|
|
4
|
+
export default function metricsStateDisabled() {
|
|
5
|
+
return {
|
|
6
|
+
latestForTopic() {
|
|
7
|
+
return Promise.resolve(null);
|
|
8
|
+
},
|
|
9
|
+
rangeForTopic() {
|
|
10
|
+
return Promise.resolve([]);
|
|
11
|
+
},
|
|
12
|
+
pollTopic() {
|
|
13
|
+
return Promise.resolve([]);
|
|
14
|
+
},
|
|
15
|
+
insertRows() {
|
|
16
|
+
return Promise.reject(new Error('metrics storage is disabled on central profile'));
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
function sameKey(left, right) {
|
|
2
|
+
return left.key === right.key;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function findRow(store, key) {
|
|
6
|
+
return store.operations.find((row) => {
|
|
7
|
+
return sameKey(row, { key });
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function findScoped(store, machineId, key) {
|
|
12
|
+
return store.operations.find((row) => {
|
|
13
|
+
return row.machine === machineId && row.key === key;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function missing(machineId, key) {
|
|
18
|
+
return new Error(`operation '${key}' not found for machine '${machineId}'`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function filterList(store, machineId, kind, range) {
|
|
22
|
+
const from = range.from ?? null;
|
|
23
|
+
const to = range.to ?? null;
|
|
24
|
+
return store.operations.filter((row) => {
|
|
25
|
+
if (row.machine !== machineId || row.kind !== kind) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
if (from && new Date(row.occurred_at).getTime() < new Date(from).getTime()) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
if (to && new Date(row.occurred_at).getTime() > new Date(to).getTime()) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
}).sort((left, right) => {
|
|
36
|
+
return new Date(left.occurred_at) - new Date(right.occurred_at);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* In-memory operations state port for tests and local runs.
|
|
42
|
+
*
|
|
43
|
+
* @param {object} store - shared mutable store with operations array
|
|
44
|
+
* @returns {object} operations port matching operationStatePg shape
|
|
45
|
+
*/
|
|
46
|
+
export default function operationStateMemory(store) {
|
|
47
|
+
if (!store.operations) {
|
|
48
|
+
store.operations = [];
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
upsert(item) {
|
|
52
|
+
const row = findRow(store, item.key);
|
|
53
|
+
if (row) {
|
|
54
|
+
row.machine = item.machine;
|
|
55
|
+
row.occurred_at = item.occurred_at;
|
|
56
|
+
row.kind = item.kind;
|
|
57
|
+
row.payload = item.payload;
|
|
58
|
+
if (item.source_updated_at) {
|
|
59
|
+
row.source_updated_at = item.source_updated_at;
|
|
60
|
+
}
|
|
61
|
+
return Promise.resolve({ created: false });
|
|
62
|
+
}
|
|
63
|
+
store.operations.push({ ...item });
|
|
64
|
+
return Promise.resolve({ created: true });
|
|
65
|
+
},
|
|
66
|
+
get(machineId, key) {
|
|
67
|
+
const row = findScoped(store, machineId, key);
|
|
68
|
+
if (!row) {
|
|
69
|
+
return Promise.reject(missing(machineId, key));
|
|
70
|
+
}
|
|
71
|
+
return Promise.resolve(row);
|
|
72
|
+
},
|
|
73
|
+
remove(machineId, key) {
|
|
74
|
+
const index = store.operations.findIndex((row) => {
|
|
75
|
+
return row.machine === machineId && row.key === key;
|
|
76
|
+
});
|
|
77
|
+
if (index < 0) {
|
|
78
|
+
return Promise.reject(missing(machineId, key));
|
|
79
|
+
}
|
|
80
|
+
const [row] = store.operations.splice(index, 1);
|
|
81
|
+
return Promise.resolve(row);
|
|
82
|
+
},
|
|
83
|
+
listForMachine(machineId, kind, range) {
|
|
84
|
+
return Promise.resolve(filterList(store, machineId, kind, range));
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
function sameInstant(a, b) {
|
|
2
|
+
return new Date(a).getTime() === new Date(b).getTime();
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function findRow(store, machineId, start) {
|
|
6
|
+
return store.segments.find((row) => {
|
|
7
|
+
return row.machine === machineId && sameInstant(row.start_time, start);
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function parseListFilters(range) {
|
|
12
|
+
const from = range.from ?? null;
|
|
13
|
+
const to = range.to ?? null;
|
|
14
|
+
return { from, to };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function filterList(store, machineId, range) {
|
|
18
|
+
const { from, to } = parseListFilters(range);
|
|
19
|
+
return store.segments.filter((row) => {
|
|
20
|
+
if (row.machine !== machineId) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
if (from && new Date(row.end_time).getTime() < new Date(from).getTime() && row.duration !== 0) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
if (to && new Date(row.start_time).getTime() > new Date(to).getTime()) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
return true;
|
|
30
|
+
}).sort((a, b) => {
|
|
31
|
+
return new Date(a.start_time) - new Date(b.start_time);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* In-memory segment state port for tests and local runs.
|
|
37
|
+
*
|
|
38
|
+
* @param {object} store - shared mutable store with segments array
|
|
39
|
+
* @returns {object} segments port matching segmentStatePg shape
|
|
40
|
+
*/
|
|
41
|
+
export default function segmentStateMemory(store) {
|
|
42
|
+
return {
|
|
43
|
+
listForMachine(machineId, range) {
|
|
44
|
+
return filterList(store, machineId, range);
|
|
45
|
+
},
|
|
46
|
+
rowAt(machineId, start) {
|
|
47
|
+
const row = findRow(store, machineId, start);
|
|
48
|
+
return row ?? null;
|
|
49
|
+
},
|
|
50
|
+
pendingRequestsForMachine(machineId) {
|
|
51
|
+
return store.segments.filter((row) => {
|
|
52
|
+
return row.machine === machineId && row.resolved === false;
|
|
53
|
+
}).sort((a, b) => {
|
|
54
|
+
return new Date(a.start_time) - new Date(b.start_time);
|
|
55
|
+
}).map((row) => {
|
|
56
|
+
return {
|
|
57
|
+
id: row.start_time,
|
|
58
|
+
name: row.name,
|
|
59
|
+
start_time: row.start_time,
|
|
60
|
+
end_time: row.end_time,
|
|
61
|
+
duration: row.duration,
|
|
62
|
+
options: row.options
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
},
|
|
66
|
+
retag(machineId, start, tagsJson, propertiesJson) {
|
|
67
|
+
const row = findRow(store, machineId, start);
|
|
68
|
+
if (row) {
|
|
69
|
+
row.tags = tagsJson;
|
|
70
|
+
row.properties = propertiesJson;
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
resolveRequest(machineId, startKey, tagsJson, propertiesJson) {
|
|
74
|
+
const row = findRow(store, machineId, startKey);
|
|
75
|
+
if (row) {
|
|
76
|
+
row.tags = tagsJson;
|
|
77
|
+
row.properties = propertiesJson;
|
|
78
|
+
row.resolved = true;
|
|
79
|
+
row.consumed = false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import timeline from '../../../domain/timeline/timeline.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* In-memory segment store with timeline read port.
|
|
5
|
+
*
|
|
6
|
+
* @returns {object} items, pending arrays and read port
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* const store = memoryTimelineStore();
|
|
10
|
+
* store.items.push({ name: 'on', start_time: new Date(), end_time: new Date(), duration: 0 });
|
|
11
|
+
*/
|
|
12
|
+
export default function memoryTimelineStore() {
|
|
13
|
+
const items = [];
|
|
14
|
+
const pending = [];
|
|
15
|
+
return {
|
|
16
|
+
items,
|
|
17
|
+
pending,
|
|
18
|
+
read: {
|
|
19
|
+
list(range) {
|
|
20
|
+
if (!range) {
|
|
21
|
+
return items.slice();
|
|
22
|
+
}
|
|
23
|
+
return items.filter((item) => {
|
|
24
|
+
if (range.from && item.end_time < new Date(range.from)) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
if (range.to && item.start_time > new Date(range.to)) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
rowAt(start) {
|
|
34
|
+
return items.find((item) => {
|
|
35
|
+
return item.start_time.getTime() === start.getTime();
|
|
36
|
+
}) || null;
|
|
37
|
+
},
|
|
38
|
+
pending() {
|
|
39
|
+
return pending.filter((item) => {
|
|
40
|
+
return !item.resolved;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Builds in-memory timeline read port wired to a pubsub bus.
|
|
49
|
+
*
|
|
50
|
+
* @param {object} store - memoryTimelineStore instance
|
|
51
|
+
* @param {object} bus - pubsub instance
|
|
52
|
+
* @returns {object} timeline with list, rowAt, pending, stream
|
|
53
|
+
*/
|
|
54
|
+
export function memoryTimelineRead(store, bus) {
|
|
55
|
+
return timeline(store.read, bus);
|
|
56
|
+
}
|