@winkjs/composer 0.5.1 → 0.7.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.
Files changed (125) hide show
  1. package/README.md +1 -1
  2. package/package.json +3 -3
  3. package/src/composer.js +1 -1
  4. package/src/core/batch/aoo-encoder.js +4 -4
  5. package/src/core/batch/sliding-window.js +2 -2
  6. package/src/core/codec/index.js +4 -4
  7. package/src/core/emitter-manager/mqtt/connection.js +209 -0
  8. package/src/core/emitter-manager/mqtt/constants.js +43 -29
  9. package/src/core/emitter-manager/mqtt/emitter.js +108 -633
  10. package/src/core/emitter-manager/mqtt/health.js +151 -0
  11. package/src/core/emitter-manager/mqtt/index.js +68 -4
  12. package/src/core/emitter-manager/mqtt/publish.js +292 -0
  13. package/src/core/emitter-manager/mqtt/resolve-config.js +312 -0
  14. package/src/core/emitter-manager/mqtt/shutdown-drain.js +247 -0
  15. package/src/core/emitter-manager/terminal/assert-columns.js +1 -1
  16. package/src/core/emitter-manager/terminal/index.js +20 -5
  17. package/src/core/env-vars.js +143 -16
  18. package/src/core/logger/index.js +174 -0
  19. package/src/core/mqtt-protocol.js +2 -2
  20. package/src/core/partition-manager/get-stats.js +31 -0
  21. package/src/core/partition-manager/index.js +1 -0
  22. package/src/core/partition-manager/init.js +15 -0
  23. package/src/core/partition-manager/update.js +143 -87
  24. package/src/core/partition-manager/validate-flow.js +1 -1
  25. package/src/core/semantics/loader.js +1 -1
  26. package/src/core/semantics/warnings.js +5 -3
  27. package/src/core/shutdown-manager/index.js +71 -21
  28. package/src/core/source-manager/csv/start.js +109 -35
  29. package/src/core/source-manager/mqtt/client.js +276 -83
  30. package/src/core/source-manager/mqtt/dedup.js +19 -14
  31. package/src/core/source-manager/mqtt/index.js +33 -6
  32. package/src/core/source-manager/mqtt/status.js +211 -46
  33. package/src/core/source-manager/record-shape.js +55 -0
  34. package/src/core/source-manager/test-harness/field-generator.js +1 -1
  35. package/src/core/source-manager/test-harness/start.js +52 -10
  36. package/src/core/source-manager/test-harness/validate.js +1 -1
  37. package/src/core/storage-manager/questdb/address-policy.js +264 -0
  38. package/src/core/storage-manager/questdb/assert-columns.js +2 -2
  39. package/src/core/storage-manager/questdb/delivery-gate.js +218 -0
  40. package/src/core/storage-manager/questdb/ensure-tables.js +1 -1
  41. package/src/core/storage-manager/questdb/flush-engine.js +852 -0
  42. package/src/core/storage-manager/questdb/flush-tracker.js +331 -0
  43. package/src/core/storage-manager/questdb/index.js +592 -827
  44. package/src/core/storage-manager/questdb/persist-plan.js +147 -162
  45. package/src/core/storage-manager/questdb/resolve-options.js +403 -0
  46. package/src/core/storage-manager/questdb/shutdown-drain.js +181 -0
  47. package/src/core/storage-manager/questdb/skip-warnings.js +180 -0
  48. package/src/core/test-utils/tcp-proxy.js +194 -1
  49. package/src/core/tunable/helpers.js +1 -1
  50. package/src/core/utils/address/index.js +362 -0
  51. package/src/core/utils/address/probe.js +210 -0
  52. package/src/core/utils/callback-guard/index.js +176 -0
  53. package/src/core/utils/clock/index.js +34 -0
  54. package/src/core/utils/half-life/index.js +4 -4
  55. package/src/core/utils/jitter/index.js +51 -0
  56. package/src/core/utils/line-rate/index.js +104 -0
  57. package/src/core/utils/quantize/index.js +43 -19
  58. package/src/core/utils/validate/composer-validators.js +2 -2
  59. package/src/core/utils/validate/index.js +1 -1
  60. package/src/core/utils/validate/validate.js +1 -1
  61. package/src/core/wiring/assert-handle.js +5 -5
  62. package/src/core/wiring/assert-module.js +2 -2
  63. package/src/core/wiring/wire-emitters.js +7 -4
  64. package/src/core/wiring/wire-linear-graph.js +1 -1
  65. package/src/core/wiring/wire-node.js +6 -6
  66. package/src/core/wiring/wire-semantics.js +1 -1
  67. package/src/core/wiring/wire-storages.js +14 -5
  68. package/src/flow/driver.js +31 -6
  69. package/src/flow/expand-single-input-array.js +4 -4
  70. package/src/flow/flow-api-schemas.js +4 -4
  71. package/src/flow/flow.js +62 -41
  72. package/src/flow/for-each.js +12 -11
  73. package/src/flow/get-target-array.js +2 -2
  74. package/src/flow/load-node-module.js +1 -1
  75. package/src/flow/make-node-method.js +4 -4
  76. package/src/flow/resolve-pick-by-field.js +1 -1
  77. package/src/flow/run.js +200 -40
  78. package/src/flow/serialize.js +1 -1
  79. package/src/flow/spec-builder.js +1 -1
  80. package/src/flow/validate.js +1 -1
  81. package/src/nodes/butterworth-filter/init.js +5 -4
  82. package/src/nodes/categorize/update.js +3 -1
  83. package/src/nodes/controller/update.js +2 -1
  84. package/src/nodes/dwell-time-tracker/update.js +2 -1
  85. package/src/nodes/emit-if/helpers.js +57 -11
  86. package/src/nodes/emit-if/init.js +1 -1
  87. package/src/nodes/emit-if/update.js +8 -11
  88. package/src/nodes/page-hinkley/update.js +2 -1
  89. package/src/nodes/pass-if/update.js +3 -1
  90. package/src/nodes/persist-if/helpers.js +60 -4
  91. package/src/nodes/persist-if/init.js +1 -1
  92. package/src/nodes/persist-if/update.js +8 -16
  93. package/src/nodes/persistence-check/update.js +2 -1
  94. package/src/nodes/sanitize/update.js +4 -2
  95. package/src/nodes/swing-watch/update.js +3 -2
  96. package/src/nodes/threshold/update.js +2 -1
  97. package/src/nodes/trend/compute-confidence.js +4 -4
  98. package/src/nodes/trend/update.js +2 -1
  99. package/src/nodes/winnow/update.js +3 -1
  100. package/src/tools/training/features.js +13 -13
  101. package/src/tools/training/logistic.js +1 -1
  102. package/src/tools/training/metrics.js +2 -2
  103. package/src/tools/training/scale.js +2 -2
  104. package/src/tools/training/validate.js +7 -7
  105. package/src/core/utils/console-formatter/index.js +0 -207
  106. package/src/nodes/archive/csv/index.js +0 -6
  107. package/src/nodes/archive/csv/init.js +0 -36
  108. package/src/nodes/archive/csv/load.js +0 -70
  109. package/src/nodes/archive/csv/reset.js +0 -14
  110. package/src/nodes/archive/csv/simple-parser.js +0 -216
  111. package/src/nodes/archive/csv/test.js +0 -173
  112. package/src/nodes/archive/csv/update.js +0 -63
  113. package/src/nodes/sse-emitter/index.js +0 -7
  114. package/src/nodes/sse-emitter/init.js +0 -105
  115. package/src/nodes/sse-emitter/introspect.js +0 -134
  116. package/src/nodes/sse-emitter/publish-to.js +0 -6
  117. package/src/nodes/sse-emitter/recompute.js +0 -16
  118. package/src/nodes/sse-emitter/reset.js +0 -19
  119. package/src/nodes/sse-emitter/update.js +0 -69
  120. package/src/nodes/sse-emitter/utils/broadcast-heartbeat.js +0 -36
  121. package/src/nodes/sse-emitter/utils/build-message.js +0 -31
  122. package/src/nodes/sse-emitter/utils/connection-manager.js +0 -45
  123. package/src/nodes/sse-emitter/utils/endpoint-initializer.js +0 -20
  124. package/src/nodes/sse-emitter/utils/health-monitor.js +0 -38
  125. 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,6 +0,0 @@
1
- const publishTo = function ( _state, _msg ) {
2
- // Terminal node - nothing to publish for downstream
3
- // Connection counts are already added in update() before broadcasting
4
- }; // publishTo()
5
-
6
- export default publishTo;
@@ -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 };