@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,191 @@
|
|
|
1
|
+
import { clickhouseSink } from '@yarkivaev/source-to-sink';
|
|
2
|
+
import supervisorSink from './supervisorSink.js';
|
|
3
|
+
import plantServer from './plantServer.js';
|
|
4
|
+
import plantOperations from './plantOperations.js';
|
|
5
|
+
import edgeApi from '../infrastructure/http/edge/edgeApi.js';
|
|
6
|
+
import runRetention from '../infrastructure/ingest/db/runRetention.js';
|
|
7
|
+
import mqttMetrics from '../infrastructure/ingest/mqtt/mqttMetrics.js';
|
|
8
|
+
import amqpMetricsIngest from '../infrastructure/ingest/telemetry/amqpMetricsIngest.js';
|
|
9
|
+
import operationSyncIngest from '../infrastructure/sync/operationSyncIngest.js';
|
|
10
|
+
import { metricsSinkFromPool } from '../infrastructure/persistence/pg/metrics.js';
|
|
11
|
+
import bindSilentStreams from './bindSilentStreams.js';
|
|
12
|
+
import timelineOperatorFromEnv from './timelineOperatorFromEnv.js';
|
|
13
|
+
import { buildSiteOperatorCatalog } from './siteOperatorCatalog.js';
|
|
14
|
+
|
|
15
|
+
function stompFromEnv(env) {
|
|
16
|
+
return {
|
|
17
|
+
url: env.STOMP_URL,
|
|
18
|
+
login: env.STOMP_LOGIN || 'guest',
|
|
19
|
+
passcode: env.STOMP_PASSCODE || 'guest',
|
|
20
|
+
host: env.STOMP_HOST || '/',
|
|
21
|
+
operatorUser: env.HMI_OPERATOR_USER || 'hmi-kiosk'
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function mqttConfigFromEnv(env) {
|
|
26
|
+
return {
|
|
27
|
+
size: parseInt(env.BATCH_SIZE || '50', 10),
|
|
28
|
+
interval: parseInt(env.FLUSH_INTERVAL || '5', 10),
|
|
29
|
+
threshold: parseInt(env.CIRCUIT_THRESHOLD || '5', 10),
|
|
30
|
+
timeout: parseInt(env.CIRCUIT_TIMEOUT || '60', 10),
|
|
31
|
+
clientId: env.CLIENT_ID,
|
|
32
|
+
sessionExpiryInterval: parseInt(env.SESSION_EXPIRY || '3600', 10)
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function mqttSinkForProfile(sink, env) {
|
|
37
|
+
const profile = env.SINK_DB_PROFILE === 'central' ? 'central' : 'edge';
|
|
38
|
+
if (profile === 'edge') {
|
|
39
|
+
return metricsSinkFromPool(sink.pool);
|
|
40
|
+
}
|
|
41
|
+
const url = env.CLICKHOUSE_URL
|
|
42
|
+
|| (env.CLICKHOUSE_HOST ? `http://${env.CLICKHOUSE_HOST}:8123` : undefined);
|
|
43
|
+
if (!url) {
|
|
44
|
+
throw new Error('CLICKHOUSE_URL or CLICKHOUSE_HOST is required for central MQTT metrics');
|
|
45
|
+
}
|
|
46
|
+
return clickhouseSink(url, 'scada.metrics');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function startMqtt(sink, env) {
|
|
50
|
+
if (!env.MQTT_URL || !env.MQTT_TOPICS) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const metricsSink = mqttSinkForProfile(sink, env);
|
|
54
|
+
const pipeline = mqttMetrics(env.MQTT_URL, metricsSink, env.MQTT_TOPICS, mqttConfigFromEnv(env));
|
|
55
|
+
pipeline.start();
|
|
56
|
+
return pipeline;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function startOperationSync(sink, env) {
|
|
60
|
+
if (env.SINK_DB_PROFILE === 'edge' || !env.AMQP_URL) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
const ingest = operationSyncIngest(
|
|
64
|
+
env.AMQP_URL,
|
|
65
|
+
env.AMQP_OPERATIONS_QUEUE || 'scada.operations.ingest',
|
|
66
|
+
sink.dataAccess.operations,
|
|
67
|
+
{ prefetch: parseInt(env.AMQP_PREFETCH || '32', 10) }
|
|
68
|
+
);
|
|
69
|
+
ingest.start();
|
|
70
|
+
return ingest;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function clickhouseMetricsUrl(env) {
|
|
74
|
+
return env.CLICKHOUSE_URL
|
|
75
|
+
|| (env.CLICKHOUSE_HOST ? `http://${env.CLICKHOUSE_HOST}:8123` : undefined);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function startAmqpMetrics(env, sink, onSeen) {
|
|
79
|
+
if (!env.AMQP_URL) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
const batchConfig = mqttConfigFromEnv(env);
|
|
83
|
+
const ingest = amqpMetricsIngest(
|
|
84
|
+
env.AMQP_URL,
|
|
85
|
+
env.AMQP_QUEUE || 'scada.telemetry.ingest',
|
|
86
|
+
sink,
|
|
87
|
+
{
|
|
88
|
+
size: batchConfig.size,
|
|
89
|
+
interval: batchConfig.interval,
|
|
90
|
+
threshold: batchConfig.threshold,
|
|
91
|
+
timeout: batchConfig.timeout,
|
|
92
|
+
prefetch: parseInt(env.AMQP_PREFETCH || '32', 10),
|
|
93
|
+
onSeen
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
ingest.start();
|
|
97
|
+
return ingest;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function startTelemetryIngest(env, streams) {
|
|
101
|
+
if (env.SINK_DB_PROFILE === 'edge' || (!env.AMQP_URL && !streams)) {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
const url = clickhouseMetricsUrl(env);
|
|
105
|
+
if (!url) {
|
|
106
|
+
throw new Error('CLICKHOUSE_URL or CLICKHOUSE_HOST is required for AMQP telemetry ingest');
|
|
107
|
+
}
|
|
108
|
+
const sink = clickhouseSink(url, 'scada.metrics');
|
|
109
|
+
const onSeen = bindSilentStreams(streams, sink);
|
|
110
|
+
const ingest = startAmqpMetrics(env, sink, onSeen);
|
|
111
|
+
if (streams) {
|
|
112
|
+
streams.start();
|
|
113
|
+
}
|
|
114
|
+
return { ingest, streams };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function plantFactoryWithOperations(plantFactory, ops, sink) {
|
|
118
|
+
return (ctx) => {
|
|
119
|
+
const built = plantFactory({ ...ctx, operations: ops }, sink);
|
|
120
|
+
return Promise.resolve(built).then((p) => {
|
|
121
|
+
if (p.operations) {
|
|
122
|
+
return p;
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
...p,
|
|
126
|
+
operations: ops,
|
|
127
|
+
init() {
|
|
128
|
+
p.init();
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function siteExtraRoutes(catalog, extraRoutes) {
|
|
136
|
+
return (path, plant, clock) => {
|
|
137
|
+
const userExtra = extraRoutes ? extraRoutes(path, plant, clock) : [];
|
|
138
|
+
return [...catalog.routes, ...userExtra];
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Unified site process: supervisor-sink HTTP, plant API, and optional MQTT ingest.
|
|
144
|
+
*
|
|
145
|
+
* @param {object} config - port, basePath, translations, requirePool, plantFactory, extraRoutes, operatorCatalog, kindSources, streams, env
|
|
146
|
+
* @returns {Promise<object>} sink, plant, mqtt pipeline
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* await siteServer({ plantFactory: ({ alerts, userDecisions }, sink) => edgePlant(machines, { metrics: sink.dataAccess.metrics, alerts, userDecisions }), kindSources: { temp } });
|
|
150
|
+
*/
|
|
151
|
+
export default async function siteServer(config) {
|
|
152
|
+
const env = config.env || process.env;
|
|
153
|
+
const sink = supervisorSink(env);
|
|
154
|
+
const ops = plantOperations(
|
|
155
|
+
sink.dataAccess.operations,
|
|
156
|
+
config.kindSources || config.operationSources
|
|
157
|
+
);
|
|
158
|
+
sink.dataAccess.operations = ops;
|
|
159
|
+
const http = edgeApi(sink.dataAccess, {
|
|
160
|
+
port: sink.apiPort,
|
|
161
|
+
token: sink.apiToken,
|
|
162
|
+
metricsEnabled: sink.metricsEnabled,
|
|
163
|
+
retentionEnabled: sink.retentionEnabled,
|
|
164
|
+
retentionDays: sink.retentionDays,
|
|
165
|
+
requestTimeoutMs: sink.requestTimeoutMs,
|
|
166
|
+
pool: sink.pool,
|
|
167
|
+
runRetention
|
|
168
|
+
});
|
|
169
|
+
await sink.run(http);
|
|
170
|
+
const mqtt = startMqtt(sink, env);
|
|
171
|
+
const telemetry = startTelemetryIngest(env, config.streams);
|
|
172
|
+
const operationSync = startOperationSync(sink, env);
|
|
173
|
+
const basePath = config.basePath || '/api/v1';
|
|
174
|
+
const catalog = buildSiteOperatorCatalog(config, basePath, sink, env);
|
|
175
|
+
if (catalog.sync) {
|
|
176
|
+
await catalog.sync.start();
|
|
177
|
+
}
|
|
178
|
+
const plant = await plantServer({
|
|
179
|
+
port: config.port || parseInt(env.PORT || '3000', 10),
|
|
180
|
+
basePath,
|
|
181
|
+
translations: config.translations,
|
|
182
|
+
requirePool: config.requirePool,
|
|
183
|
+
stomp: stompFromEnv(env),
|
|
184
|
+
plantFactory: plantFactoryWithOperations(config.plantFactory, ops, sink),
|
|
185
|
+
extraRoutes: siteExtraRoutes(catalog, config.extraRoutes),
|
|
186
|
+
timelineOperator: timelineOperatorFromEnv(catalog, env, config),
|
|
187
|
+
operationDecisions: catalog.decisions,
|
|
188
|
+
owners: config.owners
|
|
189
|
+
});
|
|
190
|
+
return { sink, plant, mqtt, telemetry, operationSync, operatorsSync: catalog.sync };
|
|
191
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import segmentPipeline, {
|
|
2
|
+
segmentsIngestDestination
|
|
3
|
+
} from '../infrastructure/ingest/pipelines/segmentPipeline.js';
|
|
4
|
+
import decisionPipeline from '../infrastructure/ingest/pipelines/decisionPipeline.js';
|
|
5
|
+
import alertPipeline from '../infrastructure/ingest/pipelines/alertPipeline.js';
|
|
6
|
+
import migrate, { migratePrivileged } from '../infrastructure/ingest/db/migrate.js';
|
|
7
|
+
import stateDataFromPool from '../infrastructure/persistence/stateDataFromPool.js';
|
|
8
|
+
import { parseRequestTimeoutMs } from '../infrastructure/ingest/parseRequestTimeoutMs.js';
|
|
9
|
+
import pg from 'pg';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
|
|
13
|
+
const appRoot = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Reads deployment environment into a normalized config object.
|
|
17
|
+
*
|
|
18
|
+
* @param {NodeJS.ProcessEnv} env - process environment
|
|
19
|
+
* @returns {object} deployment configuration
|
|
20
|
+
*/
|
|
21
|
+
export function readDeploymentConfig(env) {
|
|
22
|
+
const root = path.join(appRoot, '../..');
|
|
23
|
+
return {
|
|
24
|
+
stomp: env.STOMP_URL,
|
|
25
|
+
postgres: env.POSTGRES_URL,
|
|
26
|
+
postgresAdmin: env.POSTGRES_ADMIN_URL,
|
|
27
|
+
apiPort: parseInt(
|
|
28
|
+
env.STATE_HTTP_PORT || env.INTERNAL_API_PORT || '8081',
|
|
29
|
+
10
|
|
30
|
+
),
|
|
31
|
+
apiToken: env.STATE_HTTP_TOKEN || env.INTERNAL_API_TOKEN,
|
|
32
|
+
sinkDbProfile: env.SINK_DB_PROFILE === 'central' ? 'central' : 'edge',
|
|
33
|
+
retentionEnabled: env.RETENTION_ENABLED === 'true',
|
|
34
|
+
retentionDays: parseInt(env.RETENTION_DAYS || '30', 10),
|
|
35
|
+
migrationsDir: path.join(root, 'db', 'migrations'),
|
|
36
|
+
requestTimeoutMs: parseRequestTimeoutMs(env.REQUEST_TIMEOUT_MS),
|
|
37
|
+
pipeline: {
|
|
38
|
+
size: parseInt(env.BATCH_SIZE || '50', 10),
|
|
39
|
+
interval: parseInt(env.FLUSH_INTERVAL || '2', 10),
|
|
40
|
+
threshold: parseInt(env.CIRCUIT_THRESHOLD || '5', 10),
|
|
41
|
+
timeout: parseInt(env.CIRCUIT_TIMEOUT || '60', 10),
|
|
42
|
+
poll: parseInt(env.POLL_INTERVAL || '5', 10),
|
|
43
|
+
segmentWindow: parseInt(env.SEGMENT_WINDOW_SECONDS || '15', 10),
|
|
44
|
+
segmentsDestination: env.SEGMENTS_STOMP_DESTINATION ||
|
|
45
|
+
segmentsIngestDestination,
|
|
46
|
+
login: env.STOMP_LOGIN || 'guest',
|
|
47
|
+
passcode: env.STOMP_PASSCODE || 'guest',
|
|
48
|
+
host: env.STOMP_HOST || '/'
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Wires STOMP pipelines, migrations, and state data access from env config.
|
|
55
|
+
* HTTP binding uses edgeApi (metrics, checkpoint, retention).
|
|
56
|
+
*
|
|
57
|
+
* @param {NodeJS.ProcessEnv} [env] - process environment, defaults to process.env
|
|
58
|
+
* @returns {object} sink runtime with pool, dataAccess, and run(httpServer?)
|
|
59
|
+
*/
|
|
60
|
+
export default function supervisorSink(env = process.env) {
|
|
61
|
+
const cfg = readDeploymentConfig(env);
|
|
62
|
+
const pool = new pg.Pool({ connectionString: cfg.postgres });
|
|
63
|
+
const segments = segmentPipeline(cfg.stomp, cfg.postgres, pool, cfg.pipeline);
|
|
64
|
+
const decisions = decisionPipeline(cfg.stomp, pool, cfg.pipeline);
|
|
65
|
+
const alerts = alertPipeline(cfg.stomp, pool, cfg.pipeline);
|
|
66
|
+
const metricsEnabled = cfg.sinkDbProfile === 'edge';
|
|
67
|
+
const dataAccess = stateDataFromPool(pool, { metricsEnabled });
|
|
68
|
+
return {
|
|
69
|
+
apiPort: cfg.apiPort,
|
|
70
|
+
apiToken: cfg.apiToken,
|
|
71
|
+
sinkDbProfile: cfg.sinkDbProfile,
|
|
72
|
+
retentionEnabled: cfg.retentionEnabled,
|
|
73
|
+
retentionDays: cfg.retentionDays,
|
|
74
|
+
requestTimeoutMs: cfg.requestTimeoutMs,
|
|
75
|
+
metricsEnabled,
|
|
76
|
+
pool,
|
|
77
|
+
dataAccess,
|
|
78
|
+
async run(httpServer) {
|
|
79
|
+
if (!cfg.postgresAdmin) {
|
|
80
|
+
throw new Error('POSTGRES_ADMIN_URL is required for supervisor-sink database migrations');
|
|
81
|
+
}
|
|
82
|
+
const adminPool = new pg.Pool({ connectionString: cfg.postgresAdmin });
|
|
83
|
+
try {
|
|
84
|
+
await migrate(adminPool, cfg.migrationsDir, cfg.sinkDbProfile);
|
|
85
|
+
await migratePrivileged(adminPool, cfg.migrationsDir, cfg.sinkDbProfile);
|
|
86
|
+
} finally {
|
|
87
|
+
await adminPool.end();
|
|
88
|
+
}
|
|
89
|
+
segments.start();
|
|
90
|
+
decisions.start();
|
|
91
|
+
alerts.start();
|
|
92
|
+
if (httpServer) {
|
|
93
|
+
httpServer.start();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds timelineOperator options from env and optional site config.
|
|
3
|
+
*
|
|
4
|
+
* @param {object} catalog - operator routes catalog with provider
|
|
5
|
+
* @param {object} env - process environment
|
|
6
|
+
* @param {object} config - siteServer config with optional anonymousUsers
|
|
7
|
+
* @returns {object} timelineOperator options
|
|
8
|
+
*/
|
|
9
|
+
export default function timelineOperatorFromEnv(catalog, env, config) {
|
|
10
|
+
const options = {
|
|
11
|
+
provider: catalog.provider,
|
|
12
|
+
requireOperator: env.REQUIRE_OPERATOR === 'true',
|
|
13
|
+
defaultUser: env.HMI_OPERATOR_USER || 'hmi-kiosk'
|
|
14
|
+
};
|
|
15
|
+
if (config && config.anonymousUsers) {
|
|
16
|
+
options.anonymousUsers = config.anonymousUsers;
|
|
17
|
+
}
|
|
18
|
+
return options;
|
|
19
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Supervisor-sink service entry point.
|
|
3
|
+
*/
|
|
4
|
+
process.on('unhandledRejection', (err) => {
|
|
5
|
+
process.stderr.write(`Unhandled rejection: ${String(err)}\n`);
|
|
6
|
+
process.exit(1);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
import supervisorSink from '../application/supervisorSink.js';
|
|
10
|
+
import runRetention from '../infrastructure/ingest/db/runRetention.js';
|
|
11
|
+
import edgeApi from '../infrastructure/http/edge/edgeApi.js';
|
|
12
|
+
|
|
13
|
+
const sink = supervisorSink(process.env);
|
|
14
|
+
const http = edgeApi(sink.dataAccess, {
|
|
15
|
+
port: sink.apiPort,
|
|
16
|
+
token: sink.apiToken,
|
|
17
|
+
metricsEnabled: sink.metricsEnabled,
|
|
18
|
+
retentionEnabled: sink.retentionEnabled,
|
|
19
|
+
retentionDays: sink.retentionDays,
|
|
20
|
+
requestTimeoutMs: sink.requestTimeoutMs,
|
|
21
|
+
pool: sink.pool,
|
|
22
|
+
runRetention
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
sink.run(http).catch((error) => {
|
|
26
|
+
process.stderr.write(`${String(error)}\n`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
});
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* @returns {object} alert with id, message, timestamp, object, event, acknowledge, acknowledged
|
|
13
13
|
*
|
|
14
14
|
* @example
|
|
15
|
-
* const a = alert('alert-1', 'High voltage', new Date(), '
|
|
15
|
+
* const a = alert('alert-1', 'High voltage', new Date(), 'm1', srcEvent, ackCallback);
|
|
16
16
|
* a.id; // 'alert-1'
|
|
17
17
|
* a.event; // srcEvent reference
|
|
18
18
|
* a.acknowledged; // false
|
|
@@ -42,7 +42,7 @@ export function alert(id, message, timestamp, object, source, acknowledge) {
|
|
|
42
42
|
* @returns {object} acknowledged alert with id, message, timestamp, object, event, acknowledged
|
|
43
43
|
*
|
|
44
44
|
* @example
|
|
45
|
-
* const a = acknowledgedAlert('alert-1', 'High voltage', new Date(), '
|
|
45
|
+
* const a = acknowledgedAlert('alert-1', 'High voltage', new Date(), 'm1', srcEvent);
|
|
46
46
|
* a.id; // 'alert-1'
|
|
47
47
|
* a.event; // srcEvent reference
|
|
48
48
|
* a.acknowledged; // true
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import pubsub from '
|
|
1
|
+
import pubsub from '../shared/pubsub.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* History of alert occurrences with filtering support.
|
|
@@ -11,10 +11,10 @@ import pubsub from './pubsub.js';
|
|
|
11
11
|
*
|
|
12
12
|
* @example
|
|
13
13
|
* const history = alerts(alert, acknowledgedAlert);
|
|
14
|
-
* const a = history.trigger('High voltage', new Date(), '
|
|
14
|
+
* const a = history.trigger('High voltage', new Date(), 'm1');
|
|
15
15
|
* a.id; // 'alert-0'
|
|
16
16
|
* a.acknowledge(); // replaces with acknowledged version
|
|
17
|
-
* history.trigger('From event', new Date(), '
|
|
17
|
+
* history.trigger('From event', new Date(), 'm1', sourceEvent); // with event
|
|
18
18
|
* history.stream((evt) => console.log(evt)); // subscribe to events
|
|
19
19
|
*/
|
|
20
20
|
export default function alerts(alert, acknowledgedAlert) {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps alert ingest status to persistence action.
|
|
3
|
+
*
|
|
4
|
+
* @param {object} record - alert record with status field
|
|
5
|
+
* @returns {string|undefined} action name insert or acknowledge
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* alertIngestAction({ status: 'pending' });
|
|
9
|
+
*/
|
|
10
|
+
export function alertIngestAction(record) {
|
|
11
|
+
if (record.status === 'pending') {
|
|
12
|
+
return 'insert';
|
|
13
|
+
}
|
|
14
|
+
if (record.status === 'completed') {
|
|
15
|
+
return 'acknowledge';
|
|
16
|
+
}
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Absent ingest poll cursor for first-run backfill.
|
|
3
|
+
*
|
|
4
|
+
* @returns {{ kind: 'empty' }} discriminated union tag without cursor instant
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* const cursor = ingestCursorEmpty();
|
|
8
|
+
* cursor.kind;
|
|
9
|
+
*/
|
|
10
|
+
export function ingestCursorEmpty() {
|
|
11
|
+
return { kind: 'empty' };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Positioned ingest poll cursor after a processed source batch.
|
|
16
|
+
*
|
|
17
|
+
* @param {Date} at - last processed source updatedAt instant
|
|
18
|
+
* @returns {{ kind: 'cursor', at: Date }} discriminated union tag with cursor instant
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* const cursor = ingestCursorAt(new Date('2024-06-01T10:00:00.000Z'));
|
|
22
|
+
* cursor.at.toISOString();
|
|
23
|
+
*/
|
|
24
|
+
export function ingestCursorAt(at) {
|
|
25
|
+
return { kind: 'cursor', at };
|
|
26
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Immutable generic machine operation record.
|
|
3
|
+
*
|
|
4
|
+
* @param {string} machine - machine identifier
|
|
5
|
+
* @param {Date} occurredAt - when the operation occurred on the machine
|
|
6
|
+
* @param {string} kind - opaque operation category defined by the plant package
|
|
7
|
+
* @param {string} key - idempotent storage key
|
|
8
|
+
* @param {object} payload - structured operation body
|
|
9
|
+
* @returns {object} operation value with machine, occurredAt, kind, key, payload
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* const item = operation('m1', new Date(), 'sample', 'nb-1', { lot: 'A' });
|
|
13
|
+
* item.key;
|
|
14
|
+
*/
|
|
15
|
+
export function operation(machine, occurredAt, kind, key, payload) {
|
|
16
|
+
return {
|
|
17
|
+
machine,
|
|
18
|
+
occurredAt,
|
|
19
|
+
kind,
|
|
20
|
+
key,
|
|
21
|
+
payload
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
function stampRow(item, updatedAt) {
|
|
2
|
+
return {
|
|
3
|
+
machine: item.machine,
|
|
4
|
+
occurred_at: item.occurred_at,
|
|
5
|
+
kind: item.kind,
|
|
6
|
+
key: item.key,
|
|
7
|
+
payload: item.payload,
|
|
8
|
+
source_updated_at: updatedAt
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function loadKind(persistence, sources, machineId, kind, range) {
|
|
13
|
+
const source = sources[kind];
|
|
14
|
+
if (source) {
|
|
15
|
+
return source.list(machineId, range);
|
|
16
|
+
}
|
|
17
|
+
return persistence.listForMachine(machineId, kind, range);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function resolveKinds(kinds, sources) {
|
|
21
|
+
if (kinds === undefined) {
|
|
22
|
+
return Object.keys(sources);
|
|
23
|
+
}
|
|
24
|
+
if (Array.isArray(kinds)) {
|
|
25
|
+
return kinds;
|
|
26
|
+
}
|
|
27
|
+
return [kinds];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Operations wired to persistence, pubsub, and optional non-PG kind sources.
|
|
32
|
+
*
|
|
33
|
+
* listForMachine merges requested kinds from injectable sources and Postgres,
|
|
34
|
+
* sorted by occurred_at. Omitted kinds default to injectable source keys.
|
|
35
|
+
*
|
|
36
|
+
* @param {object} persistence - store with upsert, get, remove, listForMachine
|
|
37
|
+
* @param {object} bus - pubsub instance with stream and emit methods
|
|
38
|
+
* @param {object} [kindSources] - map of kind to { list(machineId, range) }
|
|
39
|
+
* @returns {object} operations with listForMachine, upsert, get, remove, stream
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* const ops = operations(store, bus, { temp: temperaturePort });
|
|
43
|
+
* await ops.listForMachine('m1', ['chem', 'temp'], { from: new Date() });
|
|
44
|
+
* await ops.remove('m1', 'nb-1');
|
|
45
|
+
*/
|
|
46
|
+
export default function operations(persistence, bus, kindSources) {
|
|
47
|
+
const sources = kindSources || {};
|
|
48
|
+
return {
|
|
49
|
+
listForMachine(machineId, kinds, range) {
|
|
50
|
+
const requested = resolveKinds(kinds, sources);
|
|
51
|
+
if (requested.length === 0) {
|
|
52
|
+
return Promise.resolve([]);
|
|
53
|
+
}
|
|
54
|
+
return Promise.all(requested.map((kind) => {
|
|
55
|
+
return loadKind(persistence, sources, machineId, kind, range);
|
|
56
|
+
})).then((batches) => {
|
|
57
|
+
return batches.flat().sort((left, right) => {
|
|
58
|
+
return new Date(left.occurred_at) - new Date(right.occurred_at);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
upsert(item) {
|
|
63
|
+
const updatedAt = new Date();
|
|
64
|
+
const row = stampRow(item, updatedAt);
|
|
65
|
+
return persistence.upsert(row).then((result) => {
|
|
66
|
+
const type = result.created ? 'created' : 'updated';
|
|
67
|
+
bus.emit({ type, operation: row });
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
get(machineId, key) {
|
|
71
|
+
return persistence.get(machineId, key);
|
|
72
|
+
},
|
|
73
|
+
remove(machineId, key) {
|
|
74
|
+
return persistence.remove(machineId, key).then((operation) => {
|
|
75
|
+
bus.emit({ type: 'deleted', operation });
|
|
76
|
+
return operation;
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
stream: bus.stream
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Immutable operator identity for central audit and HMI card lookup.
|
|
3
|
+
*
|
|
4
|
+
* @param {number} id - database primary key
|
|
5
|
+
* @param {string} cardUid - RFID or badge identifier
|
|
6
|
+
* @param {string} firstName - given name
|
|
7
|
+
* @param {string} lastName - family name
|
|
8
|
+
* @param {string} displayName - full name shown in HMI
|
|
9
|
+
* @returns {object} operator with id, cardUid, firstName, lastName, displayName
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* const op = operator(1, 'dev-card-001', 'Ivan', 'Petrov', 'Ivan Petrov');
|
|
13
|
+
* op.displayName;
|
|
14
|
+
*/
|
|
15
|
+
export default function operator(id, cardUid, firstName, lastName, displayName) {
|
|
16
|
+
return {
|
|
17
|
+
id,
|
|
18
|
+
cardUid,
|
|
19
|
+
firstName,
|
|
20
|
+
lastName,
|
|
21
|
+
displayName
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic monitored machine with sensors, timeline, and alerts.
|
|
3
|
+
*
|
|
4
|
+
* @param {string} name - unique machine identifier
|
|
5
|
+
* @param {object} deps - machine dependencies
|
|
6
|
+
* @param {object} deps.sensors - sensor instances keyed by name
|
|
7
|
+
* @param {object} deps.alerts - shop-level alerts collection
|
|
8
|
+
* @param {object} deps.timeline - timeline port with list, retag, pending, respond, stream
|
|
9
|
+
* @returns {object} machine with name, sensors, timeline, alerts methods
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* const m = machine('m1', { sensors: { voltage }, alerts: history, timeline: tl });
|
|
13
|
+
* m.name();
|
|
14
|
+
*/
|
|
15
|
+
export default function machine(name, deps) {
|
|
16
|
+
const { sensors, alerts: alertHistory, timeline: tl } = deps;
|
|
17
|
+
return {
|
|
18
|
+
name() {
|
|
19
|
+
return name;
|
|
20
|
+
},
|
|
21
|
+
sensors,
|
|
22
|
+
timeline: tl,
|
|
23
|
+
alerts() {
|
|
24
|
+
return alertHistory.all((item) => {
|
|
25
|
+
return item.object === name;
|
|
26
|
+
});
|
|
27
|
+
},
|
|
28
|
+
init() {
|
|
29
|
+
return this;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Top-level plant structure containing shops.
|
|
3
|
+
* Provides initialization for all contained shops.
|
|
4
|
+
*
|
|
5
|
+
* @param {object} shops - initialized list of shops
|
|
6
|
+
* @returns {object} plant with shops property and init method
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* const p = plant(initialized({ area: shop }, Object.values));
|
|
10
|
+
* p.init();
|
|
11
|
+
*/
|
|
12
|
+
export default function plant(shops, options = {}) {
|
|
13
|
+
return {
|
|
14
|
+
shops,
|
|
15
|
+
operations: options.operations,
|
|
16
|
+
init() {
|
|
17
|
+
shops.init();
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shop containing machines and shop-level alerts.
|
|
3
|
+
*
|
|
4
|
+
* @param {string} name - unique identifier for the shop
|
|
5
|
+
* @param {object} machines - initialized wrapper of machines
|
|
6
|
+
* @param {object} alertHistory - alerts collection for the shop
|
|
7
|
+
* @returns {object} shop with name, machines, alerts properties and init method
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const area = shop('area1', initialized({ m1: machine }, Object.values), alerts());
|
|
11
|
+
* area.init();
|
|
12
|
+
*/
|
|
13
|
+
export default function shop(name, machines, alertHistory) {
|
|
14
|
+
return {
|
|
15
|
+
name() {
|
|
16
|
+
return name;
|
|
17
|
+
},
|
|
18
|
+
machines,
|
|
19
|
+
alerts: alertHistory,
|
|
20
|
+
init() {
|
|
21
|
+
machines.init();
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Routes normalized segment records to the correct persistence sink.
|
|
3
|
+
*
|
|
4
|
+
* @param {object} segmentSink - batch insert/update sink
|
|
5
|
+
* @param {object} retag - retag sink
|
|
6
|
+
* @param {object} splitSink - split update sink
|
|
7
|
+
* @param {object} closer - orphan open segment closer
|
|
8
|
+
* @returns {object} collector with accept(record)
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* const route = segmentDispatch(segmentSink, retag, splitSink, closer);
|
|
12
|
+
* await route.accept({ type: 'segment', machine: 'm1', start_time: '...', duration: 0 });
|
|
13
|
+
*/
|
|
14
|
+
export default function segmentDispatch(segmentSink, retag, splitSink, closer) {
|
|
15
|
+
return {
|
|
16
|
+
async accept(record) {
|
|
17
|
+
if (record.type === 'retag') {
|
|
18
|
+
await retag.accept(record);
|
|
19
|
+
} else if (record.type === 'split') {
|
|
20
|
+
await splitSink.write([record]);
|
|
21
|
+
} else if (record.type === 'segment') {
|
|
22
|
+
if (record.duration === 0) {
|
|
23
|
+
await closer.close(record.machine, record.start_time);
|
|
24
|
+
}
|
|
25
|
+
await segmentSink.write([record]);
|
|
26
|
+
} else {
|
|
27
|
+
throw new Error(`Segment command type ${record.type} is not defined`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|