@winkjs/composer 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -8
- package/src/core/batch/aoo-encoder.js +4 -4
- package/src/core/batch/sliding-window.js +2 -2
- package/src/core/codec/index.js +4 -4
- package/src/core/emitter-manager/mqtt/emitter.js +44 -14
- package/src/core/emitter-manager/mqtt/index.js +2 -1
- package/src/core/emitter-manager/terminal/assert-columns.js +1 -1
- package/src/core/emitter-manager/terminal/index.js +4 -4
- package/src/core/env-vars.js +44 -3
- package/src/core/logger/index.js +174 -0
- package/src/core/mqtt-protocol.js +2 -2
- package/src/core/partition-manager/get-stats.js +31 -0
- package/src/core/partition-manager/index.js +1 -0
- package/src/core/partition-manager/init.js +15 -0
- package/src/core/partition-manager/update.js +143 -87
- package/src/core/partition-manager/validate-flow.js +1 -1
- package/src/core/semantics/loader.js +1 -1
- package/src/core/semantics/warnings.js +5 -3
- package/src/core/shutdown-manager/index.js +71 -21
- package/src/core/source-manager/csv/start.js +109 -35
- package/src/core/source-manager/mqtt/client.js +95 -36
- package/src/core/source-manager/mqtt/dedup.js +3 -3
- package/src/core/source-manager/mqtt/index.js +1 -1
- package/src/core/source-manager/mqtt/status.js +67 -14
- package/src/core/source-manager/record-shape.js +55 -0
- package/src/core/source-manager/test-harness/field-generator.js +1 -1
- package/src/core/source-manager/test-harness/start.js +52 -10
- package/src/core/source-manager/test-harness/validate.js +1 -1
- package/src/core/storage-manager/questdb/assert-columns.js +2 -2
- package/src/core/storage-manager/questdb/ensure-tables.js +1 -1
- package/src/core/storage-manager/questdb/index.js +55 -17
- package/src/core/storage-manager/questdb/persist-plan.js +32 -8
- package/src/core/tunable/helpers.js +1 -1
- package/src/core/utils/callback-guard/index.js +155 -0
- package/src/core/utils/half-life/index.js +4 -4
- package/src/core/utils/validate/composer-validators.js +2 -2
- package/src/core/utils/validate/index.js +1 -1
- package/src/core/utils/validate/validate.js +1 -1
- package/src/core/wiring/assert-handle.js +5 -5
- package/src/core/wiring/assert-module.js +2 -2
- package/src/core/wiring/wire-emitters.js +4 -3
- package/src/core/wiring/wire-linear-graph.js +1 -1
- package/src/core/wiring/wire-node.js +6 -6
- package/src/core/wiring/wire-semantics.js +1 -1
- package/src/core/wiring/wire-storages.js +4 -3
- package/src/flow/driver.js +31 -6
- package/src/flow/expand-single-input-array.js +4 -4
- package/src/flow/flow-api-schemas.js +4 -4
- package/src/flow/flow.js +41 -41
- package/src/flow/for-each.js +12 -11
- package/src/flow/get-target-array.js +2 -2
- package/src/flow/load-node-module.js +1 -1
- package/src/flow/make-node-method.js +4 -4
- package/src/flow/resolve-pick-by-field.js +1 -1
- package/src/flow/run.js +176 -38
- package/src/flow/serialize.js +1 -1
- package/src/flow/spec-builder.js +1 -1
- package/src/flow/validate.js +1 -1
- package/src/nodes/butterworth-filter/init.js +5 -4
- package/src/nodes/categorize/update.js +3 -1
- package/src/nodes/controller/update.js +2 -1
- package/src/nodes/dwell-time-tracker/update.js +2 -1
- package/src/nodes/emit-if/helpers.js +57 -11
- package/src/nodes/emit-if/init.js +1 -1
- package/src/nodes/emit-if/update.js +8 -11
- package/src/nodes/page-hinkley/update.js +2 -1
- package/src/nodes/pass-if/update.js +3 -1
- package/src/nodes/persist-if/helpers.js +60 -4
- package/src/nodes/persist-if/init.js +1 -1
- package/src/nodes/persist-if/update.js +8 -16
- package/src/nodes/persistence-check/update.js +2 -1
- package/src/nodes/sanitize/update.js +4 -2
- package/src/nodes/swing-watch/update.js +3 -2
- package/src/nodes/threshold/update.js +2 -1
- package/src/nodes/trend/compute-confidence.js +4 -4
- package/src/nodes/trend/update.js +2 -1
- package/src/nodes/winnow/update.js +3 -1
- package/src/tools/training/features.js +13 -13
- package/src/tools/training/logistic.js +1 -1
- package/src/tools/training/metrics.js +2 -2
- package/src/tools/training/scale.js +2 -2
- package/src/tools/training/validate.js +7 -7
- package/src/core/emitter-manager/mqtt/mqtt-store.js +0 -538
- package/src/core/utils/console-formatter/index.js +0 -207
- package/src/nodes/archive/csv/index.js +0 -6
- package/src/nodes/archive/csv/init.js +0 -36
- package/src/nodes/archive/csv/load.js +0 -70
- package/src/nodes/archive/csv/reset.js +0 -14
- package/src/nodes/archive/csv/simple-parser.js +0 -216
- package/src/nodes/archive/csv/test.js +0 -173
- package/src/nodes/archive/csv/update.js +0 -63
- package/src/nodes/sse-emitter/index.js +0 -7
- package/src/nodes/sse-emitter/init.js +0 -105
- package/src/nodes/sse-emitter/introspect.js +0 -134
- package/src/nodes/sse-emitter/publish-to.js +0 -6
- package/src/nodes/sse-emitter/recompute.js +0 -16
- package/src/nodes/sse-emitter/reset.js +0 -19
- package/src/nodes/sse-emitter/update.js +0 -69
- package/src/nodes/sse-emitter/utils/broadcast-heartbeat.js +0 -36
- package/src/nodes/sse-emitter/utils/build-message.js +0 -31
- package/src/nodes/sse-emitter/utils/connection-manager.js +0 -45
- package/src/nodes/sse-emitter/utils/endpoint-initializer.js +0 -20
- package/src/nodes/sse-emitter/utils/health-monitor.js +0 -38
- package/src/nodes/sse-emitter/utils/qos-handler.js +0 -41
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
// nodes/csv-source/test.js
|
|
2
|
-
import * as csvSource from './index.js';
|
|
3
|
-
import loadCSV from './load.js';
|
|
4
|
-
|
|
5
|
-
// Sample CSV data for testing
|
|
6
|
-
const sampleCSV = `timestamp,temperature,pressure,humidity,location
|
|
7
|
-
2024-01-01T00:00:00,23.5,1013.25,65,sensor_01
|
|
8
|
-
2024-01-01T00:00:10,23.7,1013.20,64,sensor_01
|
|
9
|
-
2024-01-01T00:00:20,24.1,1013.15,63,sensor_01
|
|
10
|
-
2024-01-01T00:00:30,24.5,1013.10,62,sensor_01
|
|
11
|
-
2024-01-01T00:00:40,25.2,1013.05,61,sensor_01`;
|
|
12
|
-
|
|
13
|
-
// Test 1: Basic initialization and loading
|
|
14
|
-
console.log('=== Test 1: Basic CSV Loading ===');
|
|
15
|
-
const basicSpec = {
|
|
16
|
-
source: sampleCSV, // Embedded CSV data
|
|
17
|
-
mode: 'batch', // Get all at once
|
|
18
|
-
parseOptions: {
|
|
19
|
-
header: true,
|
|
20
|
-
dynamicTyping: true,
|
|
21
|
-
skipEmptyLines: true
|
|
22
|
-
}
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const basicState = csvSource.init( basicSpec );
|
|
26
|
-
console.log( 'Initial state:', {
|
|
27
|
-
mode: basicState.mode,
|
|
28
|
-
isLoaded: basicState.isLoaded,
|
|
29
|
-
currentIndex: basicState.currentIndex
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
// Load the CSV data
|
|
33
|
-
await loadCSV( basicState );
|
|
34
|
-
console.log( `Loaded ${basicState.data.length} rows` );
|
|
35
|
-
console.log( 'First row:', basicState.data[0] );
|
|
36
|
-
console.log( 'Data types:', Object.entries( basicState.data[0] ).map(
|
|
37
|
-
([ k, v ]) => `${k}: ${typeof v}`
|
|
38
|
-
));
|
|
39
|
-
|
|
40
|
-
// Test 2: Message generation
|
|
41
|
-
console.log( '\n=== Test 2: Message Generation ===');
|
|
42
|
-
const msg1 = csvSource.update( basicState, {} );
|
|
43
|
-
console.log( 'Message 1:', msg1 );
|
|
44
|
-
|
|
45
|
-
const msg2 = csvSource.update( basicState, {} );
|
|
46
|
-
console.log( 'Message 2:', msg2 );
|
|
47
|
-
|
|
48
|
-
// Test 3: Stream mode with rate limiting
|
|
49
|
-
console.log( '\n=== Test 3: Stream Mode ===');
|
|
50
|
-
const streamSpec = {
|
|
51
|
-
source: sampleCSV,
|
|
52
|
-
mode: 'stream',
|
|
53
|
-
rate: 100, // 100 messages per second
|
|
54
|
-
loop: true,
|
|
55
|
-
parseOptions: {
|
|
56
|
-
header: true,
|
|
57
|
-
dynamicTyping: true
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
const streamState = csvSource.init( streamSpec );
|
|
62
|
-
await loadCSV( streamState );
|
|
63
|
-
|
|
64
|
-
// Simulate rapid updates - should be rate limited
|
|
65
|
-
const startTime = Date.now();
|
|
66
|
-
let messageCount = 0;
|
|
67
|
-
|
|
68
|
-
for ( let i = 0; i < 10; i += 1 ) {
|
|
69
|
-
const msg = csvSource.update( streamState, {} );
|
|
70
|
-
if ( msg ) {
|
|
71
|
-
messageCount += 1;
|
|
72
|
-
console.log( `Stream message ${messageCount}:`, msg._source );
|
|
73
|
-
}
|
|
74
|
-
// Small delay to show rate limiting
|
|
75
|
-
await new Promise( (resolve) => setTimeout( resolve, 5 ) );
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const elapsed = Date.now() - startTime;
|
|
79
|
-
console.log( `Generated ${messageCount} messages in ${elapsed}ms` );
|
|
80
|
-
console.log( `Effective rate: ${(messageCount / elapsed * 1000).toFixed(1)} msg/sec` );
|
|
81
|
-
|
|
82
|
-
// Test 4: Loop behavior
|
|
83
|
-
console.log( '\n=== Test 4: Loop Behavior ===');
|
|
84
|
-
const loopState = csvSource.init({
|
|
85
|
-
source: sampleCSV,
|
|
86
|
-
mode: 'batch',
|
|
87
|
-
loop: true
|
|
88
|
-
});
|
|
89
|
-
await loadCSV( loopState );
|
|
90
|
-
|
|
91
|
-
// Read all messages twice to test looping
|
|
92
|
-
console.log( 'First pass:' );
|
|
93
|
-
for ( let i = 0; i < 5; i += 1 ) {
|
|
94
|
-
const msg = csvSource.update( loopState, {} );
|
|
95
|
-
console.log( ` Row ${i + 1}: temp=${msg.temperature}°C` );
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
console.log( 'Second pass (should loop):' );
|
|
99
|
-
for ( let i = 0; i < 3; i += 1 ) {
|
|
100
|
-
const msg = csvSource.update( loopState, {} );
|
|
101
|
-
console.log( ` Row ${i + 1}: temp=${msg.temperature}°C` );
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Test 5: Column mapping
|
|
105
|
-
console.log( '\n=== Test 5: Column Mapping ===');
|
|
106
|
-
const mappedState = csvSource.init({
|
|
107
|
-
source: sampleCSV,
|
|
108
|
-
columns: {
|
|
109
|
-
'temperature': 'temp',
|
|
110
|
-
'pressure': 'press',
|
|
111
|
-
'location': 'id'
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
await loadCSV( mappedState );
|
|
115
|
-
|
|
116
|
-
const mappedMsg = csvSource.update( mappedState, {} );
|
|
117
|
-
console.log( 'Mapped message:', mappedMsg );
|
|
118
|
-
console.log( 'Should have: temp, press, id fields' );
|
|
119
|
-
|
|
120
|
-
// Test 6: CSV with different delimiter
|
|
121
|
-
console.log( '\n=== Test 6: Tab-Delimited CSV ===');
|
|
122
|
-
const tabCSV = `name age score
|
|
123
|
-
John Doe 30 95.5
|
|
124
|
-
Jane Smith 25 87.3`;
|
|
125
|
-
|
|
126
|
-
const tabState = csvSource.init({
|
|
127
|
-
source: tabCSV,
|
|
128
|
-
parseOptions: {
|
|
129
|
-
delimiter: '\t',
|
|
130
|
-
header: true,
|
|
131
|
-
dynamicTyping: true
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
await loadCSV( tabState );
|
|
135
|
-
|
|
136
|
-
const tabMsg = csvSource.update( tabState, {} );
|
|
137
|
-
console.log( 'Tab-delimited data:', tabMsg );
|
|
138
|
-
|
|
139
|
-
// Test 7: File loading simulation (Node.js only)
|
|
140
|
-
if ( typeof process === 'object' ) {
|
|
141
|
-
console.log( '\n=== Test 7: File Loading (Node.js) ===');
|
|
142
|
-
|
|
143
|
-
// Create a temporary CSV file
|
|
144
|
-
const fs = await import( 'fs/promises' );
|
|
145
|
-
const tempFile = './test-data.csv';
|
|
146
|
-
await fs.writeFile( tempFile, sampleCSV );
|
|
147
|
-
|
|
148
|
-
const fileState = csvSource.init({
|
|
149
|
-
source: tempFile,
|
|
150
|
-
mode: 'batch'
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
await loadCSV( fileState );
|
|
154
|
-
console.log( `Loaded ${fileState.data.length} rows from file` );
|
|
155
|
-
|
|
156
|
-
// Clean up
|
|
157
|
-
await fs.unlink( tempFile );
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// Test 8: Reset functionality
|
|
161
|
-
console.log( '\n=== Test 8: Reset ===');
|
|
162
|
-
console.log( 'Before reset:', {
|
|
163
|
-
currentIndex: basicState.currentIndex,
|
|
164
|
-
isLoaded: basicState.isLoaded
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
csvSource.reset( basicState );
|
|
168
|
-
console.log( 'After reset:', {
|
|
169
|
-
currentIndex: basicState.currentIndex,
|
|
170
|
-
isLoaded: basicState.isLoaded
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
console.log( '\n✅ All tests completed!' );
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
// nodes/csv-source/update.js
|
|
2
|
-
const update = function ( state, msg ) {
|
|
3
|
-
// Source nodes are special - they generate messages
|
|
4
|
-
// The msg parameter here is typically a trigger/timer signal
|
|
5
|
-
|
|
6
|
-
if ( !state.isLoaded || state.isPaused ) {
|
|
7
|
-
return null; // Nothing to emit
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
if ( state.currentIndex >= state.data.length ) {
|
|
11
|
-
if ( state.loop ) {
|
|
12
|
-
state.currentIndex = 0; // Reset to beginning
|
|
13
|
-
} else {
|
|
14
|
-
state.isPaused = true; // Stop emitting
|
|
15
|
-
return null;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Rate limiting for stream mode
|
|
20
|
-
if ( state.mode === 'stream' ) {
|
|
21
|
-
const now = Date.now();
|
|
22
|
-
const timeSinceLastEmit = now - state.lastEmitTime;
|
|
23
|
-
const minInterval = 1000 / state.rate;
|
|
24
|
-
|
|
25
|
-
if ( timeSinceLastEmit < minInterval ) {
|
|
26
|
-
return null; // Too soon, skip this update
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
state.lastEmitTime = now;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Get next row
|
|
33
|
-
const row = state.data[ state.currentIndex ];
|
|
34
|
-
state.currentIndex += 1;
|
|
35
|
-
|
|
36
|
-
// Create output message
|
|
37
|
-
const output = Object.create( null );
|
|
38
|
-
|
|
39
|
-
// Add metadata
|
|
40
|
-
output._source = {
|
|
41
|
-
type: 'csv',
|
|
42
|
-
index: state.currentIndex - 1,
|
|
43
|
-
total: state.data.length,
|
|
44
|
-
timestamp: Date.now()
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
// Map columns
|
|
48
|
-
if ( state.columns === 'auto' ) {
|
|
49
|
-
// Use all columns as-is
|
|
50
|
-
Object.assign( output, row );
|
|
51
|
-
} else {
|
|
52
|
-
// Custom column mapping
|
|
53
|
-
for ( const [ csvCol, msgField ] of Object.entries( state.columns ) ) {
|
|
54
|
-
if ( row.hasOwnProperty( csvCol ) ) {
|
|
55
|
-
output[ msgField ] = row[ csvCol ];
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return output; // This will be the new message
|
|
61
|
-
}; // update()
|
|
62
|
-
|
|
63
|
-
export default update;
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export { default as init } from './init.js';
|
|
2
|
-
export { default as update } from './update.js';
|
|
3
|
-
export { default as reset } from './reset.js';
|
|
4
|
-
export { default as recompute } from './recompute.js';
|
|
5
|
-
export { default as publishTo } from './publish-to.js';
|
|
6
|
-
// Re-export everything from introspection.js
|
|
7
|
-
export * from './introspect.js';
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import { validateSpec } from '../../core/utils/node/index.js';
|
|
2
|
-
import * as introspect from './introspect.js';
|
|
3
|
-
import { DATA_PREFIX, MESSAGE_SUFFIX } from './utils/build-message.js';
|
|
4
|
-
import { createSSEHeaders, createConnection, setupConnectionHandlers } from './utils/connection-manager.js';
|
|
5
|
-
import { initializeEndpoints } from './utils/endpoint-initializer.js';
|
|
6
|
-
import { handleHealthCheck } from './utils/health-monitor.js';
|
|
7
|
-
import { broadcastHeartbeat } from './utils/broadcast-heartbeat.js';
|
|
8
|
-
import { createServer } from 'node:http';
|
|
9
|
-
|
|
10
|
-
// Simple module-level resources
|
|
11
|
-
let server = null;
|
|
12
|
-
let heartbeatInterval = null; // eslint-disable-line no-unused-vars
|
|
13
|
-
|
|
14
|
-
const handleSSEConnection = function ( state, req, res, endpointName ) {
|
|
15
|
-
// Check connection limit
|
|
16
|
-
if ( state.totalConnections >= state.maxConnections ) {
|
|
17
|
-
res.writeHead( 503, { 'Content-Type': 'text/plain' } );
|
|
18
|
-
res.end( `Max connections reached (${state.maxConnections})` );
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// Set SSE headers
|
|
23
|
-
res.writeHead( 200, createSSEHeaders() );
|
|
24
|
-
|
|
25
|
-
// Create and register connection
|
|
26
|
-
const connection = createConnection( state, res, endpointName );
|
|
27
|
-
|
|
28
|
-
console.log( `SSE ${endpointName} connected (${state.totalConnections}/${state.maxConnections})` );
|
|
29
|
-
|
|
30
|
-
// Send initial message using same format as broadcasts
|
|
31
|
-
const initData = `{"connected":true,"endpoint":"${endpointName}"}`;
|
|
32
|
-
const initMessage = `data: ${initData}\n\n`;
|
|
33
|
-
res.write( initMessage );
|
|
34
|
-
|
|
35
|
-
// Setup disconnect handlers
|
|
36
|
-
setupConnectionHandlers( state, req, connection, endpointName );
|
|
37
|
-
}; // handleSSEConnection()
|
|
38
|
-
|
|
39
|
-
const routeRequest = function ( state, req, res ) {
|
|
40
|
-
const url = req.url;
|
|
41
|
-
|
|
42
|
-
if ( url === '/health' ) {
|
|
43
|
-
handleHealthCheck( state, res );
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if ( url.startsWith( '/events/' ) ) {
|
|
48
|
-
const endpointName = url.slice( 8 ); // Remove '/events/' prefix
|
|
49
|
-
|
|
50
|
-
if ( state.connections[ endpointName ] ) {
|
|
51
|
-
handleSSEConnection( state, req, res, endpointName );
|
|
52
|
-
} else {
|
|
53
|
-
res.writeHead( 404, { 'Content-Type': 'text/plain' } );
|
|
54
|
-
res.end( `Endpoint not configured. Available: ${state.endpointNames.join( ', ' )}` );
|
|
55
|
-
}
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
res.writeHead( 404 );
|
|
60
|
-
res.end( 'Not found' );
|
|
61
|
-
}; // routeRequest()
|
|
62
|
-
|
|
63
|
-
const init = function ( spec ) {
|
|
64
|
-
validateSpec( spec, introspect );
|
|
65
|
-
|
|
66
|
-
const state = Object.create( null );
|
|
67
|
-
|
|
68
|
-
// 2. Extract and store configuration
|
|
69
|
-
state.port = spec.port || 3000;
|
|
70
|
-
state.maxConnections = spec.maxConnections || 4;
|
|
71
|
-
state.stats = spec.stats;
|
|
72
|
-
state.nodeType = introspect.getNodeType();
|
|
73
|
-
|
|
74
|
-
// 3. Initialize endpoints and connections
|
|
75
|
-
initializeEndpoints( state );
|
|
76
|
-
|
|
77
|
-
// 4. Performance optimization: Pre-allocated buffers and constants
|
|
78
|
-
state.messageBuffer = Buffer.alloc( 4096 ); // 4KB reusable buffer
|
|
79
|
-
state.deadConnectionsPool = []; // Reusable arrays for cleanup
|
|
80
|
-
state.dataPrefix = DATA_PREFIX;
|
|
81
|
-
state.messageSuffix = MESSAGE_SUFFIX;
|
|
82
|
-
|
|
83
|
-
// 5. Initialize statistics tracking
|
|
84
|
-
state.totalConnections = 0;
|
|
85
|
-
state.totalSent = 0;
|
|
86
|
-
state.totalErrors = 0;
|
|
87
|
-
|
|
88
|
-
// 6. Create and start HTTP server
|
|
89
|
-
server = createServer( ( req, res ) => {
|
|
90
|
-
routeRequest( state, req, res );
|
|
91
|
-
} );
|
|
92
|
-
|
|
93
|
-
server.listen( state.port, () => {
|
|
94
|
-
console.log( `SSE Emitter listening on port ${state.port}` );
|
|
95
|
-
console.log( `Configured endpoints: ${state.endpointNames.join( ', ' )}` );
|
|
96
|
-
} );
|
|
97
|
-
|
|
98
|
-
heartbeatInterval = setInterval(() => {
|
|
99
|
-
broadcastHeartbeat(state);
|
|
100
|
-
}, 30000);
|
|
101
|
-
|
|
102
|
-
return state;
|
|
103
|
-
}; // init()
|
|
104
|
-
|
|
105
|
-
export default init;
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
import { validators, composerValidators } from '../../core/utils/validate/index.js';
|
|
2
|
-
|
|
3
|
-
/** Single source of truth for what this node can do */
|
|
4
|
-
const SUPPORTED_STATS = [
|
|
5
|
-
'dashboard',
|
|
6
|
-
'alerts',
|
|
7
|
-
'database',
|
|
8
|
-
'monitoring'
|
|
9
|
-
];
|
|
10
|
-
|
|
11
|
-
/** QoS level constants for performance (numeric comparison in hot path) */
|
|
12
|
-
export const QOS_FIRE_AND_FORGET = 0;
|
|
13
|
-
export const QOS_BEST_EFFORT = 1;
|
|
14
|
-
export const QOS_RELIABLE = 2;
|
|
15
|
-
|
|
16
|
-
/** Human-readable descriptions */
|
|
17
|
-
const STAT_DESCRIPTIONS = {
|
|
18
|
-
dashboard: 'Real-time dashboard connections',
|
|
19
|
-
alerts: 'Alert system connections',
|
|
20
|
-
database: 'Database sink connections',
|
|
21
|
-
monitoring: 'System monitoring connections'
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
/** QoS level descriptions */
|
|
25
|
-
const QOS_DESCRIPTIONS = {
|
|
26
|
-
[ QOS_FIRE_AND_FORGET ]: 'Fire-and-forget: No queuing, drop on backpressure (fastest)',
|
|
27
|
-
[ QOS_BEST_EFFORT ]: 'Best-effort: Small queue, drop oldest on overflow (balanced)',
|
|
28
|
-
[ QOS_RELIABLE ]: 'Reliable: Large queue, backpressure monitoring (most reliable)'
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
/** The type of this node */
|
|
32
|
-
const NODE_TYPE = 'SSE Emitter';
|
|
33
|
-
|
|
34
|
-
/** Node capabilities */
|
|
35
|
-
const CAPABILITIES = {
|
|
36
|
-
description: 'Broadcasts entire messages via Server-Sent Events with configurable QoS levels',
|
|
37
|
-
features: [
|
|
38
|
-
'Terminal node - broadcasts complete pipeline messages',
|
|
39
|
-
'Four endpoint types: dashboard, alerts, database, monitoring',
|
|
40
|
-
'Numeric QoS levels: 0 (fire-and-forget), 1 (best-effort), 2 (reliable)',
|
|
41
|
-
'High-performance broadcasting with buffer reuse',
|
|
42
|
-
'Production-ready connection management'
|
|
43
|
-
]
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
// Export functions for accessing metadata
|
|
47
|
-
export const getSupportedStats = () => SUPPORTED_STATS.slice();
|
|
48
|
-
export const getStatDescriptions = () => ( { ...STAT_DESCRIPTIONS } );
|
|
49
|
-
export const getNodeType = () => NODE_TYPE;
|
|
50
|
-
export const getCapabilities = () => ( { ...CAPABILITIES } );
|
|
51
|
-
export const getQoSDescriptions = () => ( { ...QOS_DESCRIPTIONS } );
|
|
52
|
-
|
|
53
|
-
// DSL Metadata
|
|
54
|
-
const DSL_METADATA = {
|
|
55
|
-
// Validation schema
|
|
56
|
-
specSchema: {
|
|
57
|
-
nodeType: {
|
|
58
|
-
type: 'string',
|
|
59
|
-
required: true,
|
|
60
|
-
value: NODE_TYPE,
|
|
61
|
-
validator: composerValidators.nodeType,
|
|
62
|
-
error: 'Invalid node type'
|
|
63
|
-
},
|
|
64
|
-
name: {
|
|
65
|
-
type: 'string',
|
|
66
|
-
required: true,
|
|
67
|
-
validator: validators.identifier,
|
|
68
|
-
error: 'Name must be a valid JavaScript identifier'
|
|
69
|
-
},
|
|
70
|
-
// Note: No 'from' field - terminal node broadcasts entire message
|
|
71
|
-
port: {
|
|
72
|
-
type: 'number',
|
|
73
|
-
required: false,
|
|
74
|
-
default: 3000, // Documentation only!
|
|
75
|
-
min: 1024,
|
|
76
|
-
max: 65535,
|
|
77
|
-
integer: true,
|
|
78
|
-
error: 'Port must be between 1024 and 65535'
|
|
79
|
-
},
|
|
80
|
-
maxConnections: {
|
|
81
|
-
type: 'number',
|
|
82
|
-
required: false,
|
|
83
|
-
default: 4, // Documentation only!
|
|
84
|
-
min: 1,
|
|
85
|
-
max: 10,
|
|
86
|
-
integer: true,
|
|
87
|
-
validator: validators.positiveInteger,
|
|
88
|
-
error: 'Max connections must be between 1 and 10'
|
|
89
|
-
},
|
|
90
|
-
stats: {
|
|
91
|
-
type: 'object',
|
|
92
|
-
required: true,
|
|
93
|
-
minProperties: 1,
|
|
94
|
-
propertyNames: SUPPORTED_STATS, // Array of allowed endpoint names
|
|
95
|
-
propertySchema: {
|
|
96
|
-
type: 'object',
|
|
97
|
-
required: true,
|
|
98
|
-
validator: composerValidators.statSpec, // Using composerValidator!
|
|
99
|
-
properties: {
|
|
100
|
-
qos: {
|
|
101
|
-
type: 'number',
|
|
102
|
-
required: false,
|
|
103
|
-
default: 0, // Documentation only!
|
|
104
|
-
integer: true,
|
|
105
|
-
validator: validators.oneOf( [ 0, 1, 2 ] ),
|
|
106
|
-
error: 'QoS must be 0 (fire-and-forget), 1 (best-effort), or 2 (reliable)'
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
},
|
|
110
|
-
error: 'Stats must define at least one endpoint with valid configuration'
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
|
-
|
|
114
|
-
// 3. Cross-field validators - none needed for SSE Emitter
|
|
115
|
-
crossFieldValidators: [],
|
|
116
|
-
|
|
117
|
-
// 4. Build spec from DSL
|
|
118
|
-
buildSpec: function ( port = 3000, maxConnections = 4 ) {
|
|
119
|
-
return {
|
|
120
|
-
nodeType: NODE_TYPE,
|
|
121
|
-
name: '{{AUTO_NAME}}',
|
|
122
|
-
port,
|
|
123
|
-
maxConnections,
|
|
124
|
-
stats: {
|
|
125
|
-
dashboard: { storeAs: 'dashConnected', qos: 0 }, // Default fire-and-forget
|
|
126
|
-
database: { storeAs: 'dbConnected', qos: 2 } // Default reliable
|
|
127
|
-
}
|
|
128
|
-
};
|
|
129
|
-
},
|
|
130
|
-
|
|
131
|
-
acceptsSpecObject: true
|
|
132
|
-
};
|
|
133
|
-
|
|
134
|
-
export const getDSLMetadata = () => ({ ...DSL_METADATA });
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
const recompute = function ( state ) {
|
|
2
|
-
// Recompute total connection count from all endpoints
|
|
3
|
-
let totalConnections = 0;
|
|
4
|
-
|
|
5
|
-
for ( let i = 0; i < state.endpointNames.length; i += 1 ) {
|
|
6
|
-
const endpointName = state.endpointNames[i];
|
|
7
|
-
totalConnections += state.connections[endpointName].length;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
state.totalConnections = totalConnections;
|
|
11
|
-
|
|
12
|
-
console.log( `composer/sseEmitter: Recomputed ${state.totalConnections} active connections across ${state.endpointNames.length} endpoints` );
|
|
13
|
-
return true;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export default recompute;
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
const reset = function ( state ) {
|
|
2
|
-
// Reset statistics only (keep connections active)
|
|
3
|
-
state.totalSent = 0;
|
|
4
|
-
state.totalErrors = 0;
|
|
5
|
-
|
|
6
|
-
// Reset per-connection message counts
|
|
7
|
-
for ( let i = 0; i < state.endpointNames.length; i += 1 ) {
|
|
8
|
-
const endpointName = state.endpointNames[ i ];
|
|
9
|
-
const connections = state.connections[ endpointName ];
|
|
10
|
-
|
|
11
|
-
for ( let j = 0; j < connections.length; j += 1 ) {
|
|
12
|
-
connections[ j ].messagesSent = 0;
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
return true;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export default reset;
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { buildMessage } from './utils/build-message.js';
|
|
2
|
-
import { QOS_HANDLERS } from './utils/qos-handler.js';
|
|
3
|
-
|
|
4
|
-
const cleanupDeadConnections = function ( state, connections, deadConnections ) {
|
|
5
|
-
// Remove dead connections in reverse order to maintain indices
|
|
6
|
-
for ( let i = deadConnections.length - 1; i >= 0; i -= 1 ) {
|
|
7
|
-
connections.splice( deadConnections[ i ], 1 );
|
|
8
|
-
state.totalConnections -= 1;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
// Return array to pool for reuse
|
|
12
|
-
state.deadConnectionsPool.push( deadConnections );
|
|
13
|
-
}; // cleanupDeadConnections()
|
|
14
|
-
|
|
15
|
-
const broadcastToEndpoint = function ( state, connections, message, endpointName ) {
|
|
16
|
-
// Get reusable dead connections array
|
|
17
|
-
const deadConnections = state.deadConnectionsPool.pop() || [];
|
|
18
|
-
deadConnections.length = 0;
|
|
19
|
-
|
|
20
|
-
// Fast iteration with minimal nesting
|
|
21
|
-
for ( let i = 0; i < connections.length; i += 1 ) {
|
|
22
|
-
const connection = connections[ i ];
|
|
23
|
-
|
|
24
|
-
if ( !connection.response.writable ) {
|
|
25
|
-
deadConnections.push( i );
|
|
26
|
-
continue; // eslint-disable-line no-continue
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
// O(1) QoS handler dispatch - no switch statement nesting
|
|
31
|
-
const qosHandler = QOS_HANDLERS[ connection.qos ] || QOS_HANDLERS[ 0 ];
|
|
32
|
-
qosHandler( connection, message, state, endpointName );
|
|
33
|
-
} catch {
|
|
34
|
-
deadConnections.push( i );
|
|
35
|
-
state.totalErrors += 1;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
cleanupDeadConnections( state, connections, deadConnections );
|
|
40
|
-
}; // broadcastToEndpoint()
|
|
41
|
-
|
|
42
|
-
// High-performance broadcast with zero allocations
|
|
43
|
-
const update = function ( state, msg ) {
|
|
44
|
-
// Fast path: no connections
|
|
45
|
-
if ( state.totalConnections === 0 ) return;
|
|
46
|
-
|
|
47
|
-
// Add connection counts directly to the message
|
|
48
|
-
for ( let i = 0; i < state.endpointNames.length; i += 1 ) {
|
|
49
|
-
const endpointName = state.endpointNames[ i ];
|
|
50
|
-
const storeAs = state.stats[ endpointName ].storeAs;
|
|
51
|
-
msg[ storeAs ] = state.connections[ endpointName ].length;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// Serialize entire message (only unavoidable allocation)
|
|
55
|
-
const jsonString = JSON.stringify( msg );
|
|
56
|
-
const messageToSend = buildMessage( state, jsonString );
|
|
57
|
-
|
|
58
|
-
// Broadcast to all configured endpoints
|
|
59
|
-
for ( let i = 0; i < state.endpointNames.length; i += 1 ) {
|
|
60
|
-
const endpointName = state.endpointNames[ i ];
|
|
61
|
-
const connections = state.connections[ endpointName ];
|
|
62
|
-
|
|
63
|
-
if ( connections.length > 0 ) {
|
|
64
|
-
broadcastToEndpoint( state, connections, messageToSend, endpointName );
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}; // update()
|
|
68
|
-
|
|
69
|
-
export default update;
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
// utils/broadcast-heartbeat.js
|
|
2
|
-
|
|
3
|
-
const HEARTBEAT_MESSAGE = Buffer.from(':heartbeat\n\n');
|
|
4
|
-
|
|
5
|
-
export const broadcastHeartbeat = function (state) {
|
|
6
|
-
// SSE comment format (`:` prefix) keeps connection alive without triggering client events
|
|
7
|
-
|
|
8
|
-
for (let i = 0; i < state.endpointNames.length; i += 1) {
|
|
9
|
-
const endpointName = state.endpointNames[i];
|
|
10
|
-
const connections = state.connections[endpointName];
|
|
11
|
-
|
|
12
|
-
// Track dead connections
|
|
13
|
-
const deadIndices = [];
|
|
14
|
-
|
|
15
|
-
for (let j = 0; j < connections.length; j += 1) {
|
|
16
|
-
const connection = connections[j];
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
if (connection.response.writable) {
|
|
20
|
-
connection.response.write(HEARTBEAT_MESSAGE);
|
|
21
|
-
} else {
|
|
22
|
-
deadIndices.push(j);
|
|
23
|
-
}
|
|
24
|
-
} catch ( error ) {
|
|
25
|
-
console.error(`composer/emitter: Write error on ${endpointName}:`, error.message);
|
|
26
|
-
deadIndices.push(j);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Clean up dead connections
|
|
31
|
-
for (let k = deadIndices.length - 1; k >= 0; k -= 1) {
|
|
32
|
-
connections.splice(deadIndices[k], 1);
|
|
33
|
-
state.totalConnections -= 1;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
};
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
// utils/build-message.js
|
|
2
|
-
|
|
3
|
-
const DATA_PREFIX = Buffer.from( 'data: ' );
|
|
4
|
-
const MESSAGE_SUFFIX = Buffer.from( '\n\n' );
|
|
5
|
-
|
|
6
|
-
const buildMessage = function ( state, jsonString ) {
|
|
7
|
-
const { messageBuffer, dataPrefix, messageSuffix } = state;
|
|
8
|
-
const jsonBuffer = Buffer.from( jsonString, 'utf8' );
|
|
9
|
-
const totalSize = dataPrefix.length + jsonBuffer.length + messageSuffix.length;
|
|
10
|
-
|
|
11
|
-
// Use reusable buffer for small messages
|
|
12
|
-
if ( totalSize <= messageBuffer.length ) {
|
|
13
|
-
let offset = 0;
|
|
14
|
-
|
|
15
|
-
dataPrefix.copy( messageBuffer, offset );
|
|
16
|
-
offset += dataPrefix.length;
|
|
17
|
-
|
|
18
|
-
jsonBuffer.copy( messageBuffer, offset );
|
|
19
|
-
offset += jsonBuffer.length;
|
|
20
|
-
|
|
21
|
-
messageSuffix.copy( messageBuffer, offset );
|
|
22
|
-
offset += messageSuffix.length;
|
|
23
|
-
|
|
24
|
-
return messageBuffer.subarray( 0, offset );
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// Fallback for large messages
|
|
28
|
-
return Buffer.concat( [ dataPrefix, jsonBuffer, messageSuffix ] );
|
|
29
|
-
}; // buildSSEMessage()
|
|
30
|
-
|
|
31
|
-
export { buildMessage, DATA_PREFIX, MESSAGE_SUFFIX };
|