@technoculture/data-bridge 0.1.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 (37) hide show
  1. package/CMakeLists.txt +57 -0
  2. package/README.md +77 -0
  3. package/dist/index.d.mts +148 -0
  4. package/dist/index.d.ts +148 -0
  5. package/dist/index.js +314 -0
  6. package/dist/index.mjs +285 -0
  7. package/lib/index.ts +171 -0
  8. package/lib/resilient.ts +276 -0
  9. package/package.json +75 -0
  10. package/prebuilds/darwin-arm64/.ninja_deps +0 -0
  11. package/prebuilds/darwin-arm64/.ninja_log +6 -0
  12. package/prebuilds/darwin-arm64/CMakeCache.txt +398 -0
  13. package/prebuilds/darwin-arm64/CMakeFiles/4.0.3/CMakeCCompiler.cmake +84 -0
  14. package/prebuilds/darwin-arm64/CMakeFiles/4.0.3/CMakeCXXCompiler.cmake +104 -0
  15. package/prebuilds/darwin-arm64/CMakeFiles/4.0.3/CMakeDetermineCompilerABI_C.bin +0 -0
  16. package/prebuilds/darwin-arm64/CMakeFiles/4.0.3/CMakeDetermineCompilerABI_CXX.bin +0 -0
  17. package/prebuilds/darwin-arm64/CMakeFiles/4.0.3/CMakeSystem.cmake +15 -0
  18. package/prebuilds/darwin-arm64/CMakeFiles/4.0.3/CompilerIdC/CMakeCCompilerId.c +905 -0
  19. package/prebuilds/darwin-arm64/CMakeFiles/4.0.3/CompilerIdC/a.out +0 -0
  20. package/prebuilds/darwin-arm64/CMakeFiles/4.0.3/CompilerIdC/apple-sdk.c +1 -0
  21. package/prebuilds/darwin-arm64/CMakeFiles/4.0.3/CompilerIdCXX/CMakeCXXCompilerId.cpp +920 -0
  22. package/prebuilds/darwin-arm64/CMakeFiles/4.0.3/CompilerIdCXX/a.out +0 -0
  23. package/prebuilds/darwin-arm64/CMakeFiles/4.0.3/CompilerIdCXX/apple-sdk.cpp +1 -0
  24. package/prebuilds/darwin-arm64/CMakeFiles/CMakeConfigureLog.yaml +531 -0
  25. package/prebuilds/darwin-arm64/CMakeFiles/InstallScripts.json +7 -0
  26. package/prebuilds/darwin-arm64/CMakeFiles/TargetDirectories.txt +3 -0
  27. package/prebuilds/darwin-arm64/CMakeFiles/cmake.check_cache +1 -0
  28. package/prebuilds/darwin-arm64/CMakeFiles/data_bridge_node.dir/Users/satyamtiwary/Documents/Python-Things/data-bridge/src/protocol/crc16.cpp.o +0 -0
  29. package/prebuilds/darwin-arm64/CMakeFiles/data_bridge_node.dir/Users/satyamtiwary/Documents/Python-Things/data-bridge/src/transport/platform/linux/linux_serial.cpp.o +0 -0
  30. package/prebuilds/darwin-arm64/CMakeFiles/data_bridge_node.dir/src/addon.cpp.o +0 -0
  31. package/prebuilds/darwin-arm64/CMakeFiles/data_bridge_node.dir/src/serial_wrapper.cpp.o +0 -0
  32. package/prebuilds/darwin-arm64/CMakeFiles/rules.ninja +64 -0
  33. package/prebuilds/darwin-arm64/Release/data_bridge_node.node +0 -0
  34. package/prebuilds/darwin-arm64/build.ninja +192 -0
  35. package/prebuilds/darwin-arm64/cmake_install.cmake +61 -0
  36. package/src/addon.cpp +219 -0
  37. package/src/serial_wrapper.cpp +8 -0
package/lib/index.ts ADDED
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Data Bridge - Guaranteed Reliable Serial Communication
3
+ *
4
+ * TypeScript wrapper for the native Node-API addon.
5
+ * Provides a clean, async-friendly API for Electron applications.
6
+ */
7
+
8
+ import { EventEmitter } from 'events';
9
+ import path from 'path';
10
+
11
+ // Load native addon - works in both ESM and CJS
12
+ let addon: NativeAddon;
13
+
14
+ function loadAddon (): NativeAddon {
15
+ const possiblePaths = [
16
+ // Prebuilt binaries
17
+ `../prebuilds/${process.platform}-${process.arch}/data_bridge_node.node`,
18
+ // Local build
19
+ '../build/Release/data_bridge_node.node',
20
+ ];
21
+
22
+ for (const p of possiblePaths)
23
+ {
24
+ try
25
+ {
26
+ // Use require for native addons (works in both ESM and CJS)
27
+ return require(path.join(__dirname, p));
28
+ } catch
29
+ {
30
+ continue;
31
+ }
32
+ }
33
+
34
+ throw new Error('Failed to load native addon. Run `npm run build` first.');
35
+ }
36
+
37
+ addon = loadAddon();
38
+
39
+ interface NativeAddon {
40
+ DataBridge: new () => NativeDataBridge;
41
+ }
42
+
43
+ interface NativeDataBridge {
44
+ open (port: string, baud: number, callback?: (data: Buffer) => void): Promise<boolean>;
45
+ send (data: Buffer | string): Promise<number>;
46
+ close (): boolean;
47
+ isOpen (): boolean;
48
+ }
49
+
50
+ export interface DataBridgeOptions {
51
+ baudRate?: number;
52
+ }
53
+
54
+ export interface DataBridgeEvents {
55
+ data: (data: Buffer) => void;
56
+ error: (error: Error) => void;
57
+ close: () => void;
58
+ }
59
+
60
+ /**
61
+ * DataBridge provides guaranteed reliable serial communication.
62
+ *
63
+ * Features:
64
+ * - Automatic retransmission on packet loss
65
+ * - CRC32 integrity checking
66
+ * - COBS framing for robust delimitation
67
+ * - Fragmentation for large messages
68
+ *
69
+ * @example
70
+ * ```typescript
71
+ * const bridge = await DataBridge.open('/dev/ttyUSB0');
72
+ *
73
+ * bridge.on('data', (data) => {
74
+ * console.log('Received:', data.toString());
75
+ * });
76
+ *
77
+ * await bridge.send('Hello, World!');
78
+ * await bridge.close();
79
+ * ```
80
+ */
81
+ export class DataBridge extends EventEmitter {
82
+ private native: NativeDataBridge;
83
+ private _isOpen = false;
84
+
85
+ private constructor() {
86
+ super();
87
+ this.native = new addon.DataBridge();
88
+ }
89
+
90
+ /**
91
+ * Open a serial port with guaranteed reliable communication.
92
+ *
93
+ * @param port - Serial port path (e.g., '/dev/ttyUSB0' or 'COM3')
94
+ * @param options - Configuration options
95
+ * @returns Promise resolving to a DataBridge instance
96
+ */
97
+ static async open (port: string, options: DataBridgeOptions = {}): Promise<DataBridge> {
98
+ const instance = new DataBridge();
99
+ const baud = options.baudRate ?? 115200;
100
+
101
+ const onData = (data: Buffer) => {
102
+ instance.emit('data', data);
103
+ };
104
+
105
+ try
106
+ {
107
+ await instance.native.open(port, baud, onData);
108
+ instance._isOpen = true;
109
+ return instance;
110
+ } catch (err)
111
+ {
112
+ throw new Error(`Failed to open ${port}: ${err instanceof Error ? err.message : err}`);
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Send data with guaranteed delivery.
118
+ *
119
+ * The data will be fragmented if necessary, checksummed, and
120
+ * retransmitted until acknowledged by the receiver.
121
+ *
122
+ * @param data - Data to send (Buffer or string)
123
+ * @returns Promise resolving when data is acknowledged
124
+ */
125
+ async send (data: Buffer | string): Promise<void> {
126
+ if (!this._isOpen)
127
+ {
128
+ throw new Error('Port not open');
129
+ }
130
+
131
+ const buffer = typeof data === 'string' ? Buffer.from(data) : data;
132
+ await this.native.send(buffer);
133
+ }
134
+
135
+ /**
136
+ * Close the serial port.
137
+ */
138
+ async close (): Promise<void> {
139
+ if (this._isOpen)
140
+ {
141
+ this.native.close();
142
+ this._isOpen = false;
143
+ this.emit('close');
144
+ }
145
+ }
146
+
147
+ /**
148
+ * Check if the port is currently open.
149
+ */
150
+ get isOpen (): boolean {
151
+ return this._isOpen && this.native.isOpen();
152
+ }
153
+
154
+ // Type-safe event emitter methods
155
+ on<K extends keyof DataBridgeEvents> (event: K, listener: DataBridgeEvents[K]): this {
156
+ return super.on(event, listener);
157
+ }
158
+
159
+ once<K extends keyof DataBridgeEvents> (event: K, listener: DataBridgeEvents[K]): this {
160
+ return super.once(event, listener);
161
+ }
162
+
163
+ emit<K extends keyof DataBridgeEvents> (event: K, ...args: Parameters<DataBridgeEvents[K]>): boolean {
164
+ return super.emit(event, ...args);
165
+ }
166
+ }
167
+
168
+ export default DataBridge;
169
+
170
+ // Re-export resilient wrapper
171
+ export { ResilientDataBridge, ResilientOptions, ResilientEvents } from './resilient';
@@ -0,0 +1,276 @@
1
+ /**
2
+ * ResilientDataBridge - Connection-resilient wrapper
3
+ *
4
+ * Guarantees message delivery across USB disconnections by:
5
+ * 1. Auto-reconnection with exponential backoff
6
+ * 2. Message queue for pending sends during disconnect
7
+ * 3. Automatic re-handshake and queue flush on reconnect
8
+ */
9
+
10
+ import { EventEmitter } from 'events';
11
+ import { DataBridge, DataBridgeOptions } from './index';
12
+
13
+ export interface ResilientOptions extends DataBridgeOptions {
14
+ /** Enable auto-reconnection (default: true) */
15
+ reconnect?: boolean;
16
+ /** Initial reconnect delay in ms (default: 1000) */
17
+ reconnectDelay?: number;
18
+ /** Maximum reconnect delay in ms (default: 30000) */
19
+ maxReconnectDelay?: number;
20
+ /** Maximum queue size before dropping old messages (default: 1000) */
21
+ maxQueueSize?: number;
22
+ }
23
+
24
+ export interface ResilientEvents {
25
+ data: (data: Buffer) => void;
26
+ error: (error: Error) => void;
27
+ close: () => void;
28
+ disconnect: () => void;
29
+ reconnecting: (attempt: number, delay: number) => void;
30
+ reconnected: () => void;
31
+ }
32
+
33
+ interface QueuedMessage {
34
+ data: Buffer;
35
+ resolve: () => void;
36
+ reject: (err: Error) => void;
37
+ timestamp: number;
38
+ }
39
+
40
+ export class ResilientDataBridge extends EventEmitter {
41
+ private bridge: DataBridge | null = null;
42
+ private port: string;
43
+ private options: Required<ResilientOptions>;
44
+ private messageQueue: QueuedMessage[] = [];
45
+ private isConnected = false;
46
+ private isReconnecting = false;
47
+ private shouldReconnect = true;
48
+ private reconnectAttempt = 0;
49
+ private reconnectTimeout: NodeJS.Timeout | null = null;
50
+
51
+ private constructor(port: string, options: ResilientOptions) {
52
+ super();
53
+ this.port = port;
54
+ this.options = {
55
+ baudRate: options.baudRate ?? 115200,
56
+ reconnect: options.reconnect ?? true,
57
+ reconnectDelay: options.reconnectDelay ?? 1000,
58
+ maxReconnectDelay: options.maxReconnectDelay ?? 30000,
59
+ maxQueueSize: options.maxQueueSize ?? 1000,
60
+ };
61
+ }
62
+
63
+ /**
64
+ * Open a resilient serial connection.
65
+ *
66
+ * @param port - Serial port path
67
+ * @param options - Configuration including reconnection settings
68
+ */
69
+ static async open (port: string, options: ResilientOptions = {}): Promise<ResilientDataBridge> {
70
+ const instance = new ResilientDataBridge(port, options);
71
+ await instance.connect();
72
+ return instance;
73
+ }
74
+
75
+ private async connect (): Promise<void> {
76
+ try
77
+ {
78
+ this.bridge = await DataBridge.open(this.port, {
79
+ baudRate: this.options.baudRate,
80
+ });
81
+
82
+ this.isConnected = true;
83
+ this.reconnectAttempt = 0;
84
+
85
+ // Forward data events
86
+ this.bridge.on('data', (data) => {
87
+ this.emit('data', data);
88
+ });
89
+
90
+ // Handle errors and detect disconnection
91
+ this.bridge.on('error', (err) => {
92
+ this.emit('error', err);
93
+ this.handleDisconnect();
94
+ });
95
+
96
+ // If we're reconnecting, emit event and flush queue
97
+ if (this.isReconnecting)
98
+ {
99
+ this.isReconnecting = false;
100
+ this.emit('reconnected');
101
+ await this.flushQueue();
102
+ }
103
+ } catch (err)
104
+ {
105
+ this.isConnected = false;
106
+ if (this.shouldReconnect && this.options.reconnect)
107
+ {
108
+ this.scheduleReconnect();
109
+ } else
110
+ {
111
+ throw err;
112
+ }
113
+ }
114
+ }
115
+
116
+ private handleDisconnect (): void {
117
+ if (!this.isConnected) return;
118
+
119
+ this.isConnected = false;
120
+ this.bridge = null;
121
+ this.emit('disconnect');
122
+
123
+ if (this.shouldReconnect && this.options.reconnect)
124
+ {
125
+ this.scheduleReconnect();
126
+ }
127
+ }
128
+
129
+ private scheduleReconnect (): void {
130
+ if (this.reconnectTimeout) return;
131
+
132
+ this.isReconnecting = true;
133
+ this.reconnectAttempt++;
134
+
135
+ // Exponential backoff with jitter
136
+ const baseDelay = this.options.reconnectDelay;
137
+ const maxDelay = this.options.maxReconnectDelay;
138
+ const delay = Math.min(
139
+ baseDelay * Math.pow(2, this.reconnectAttempt - 1) + Math.random() * 1000,
140
+ maxDelay
141
+ );
142
+
143
+ this.emit('reconnecting', this.reconnectAttempt, delay);
144
+
145
+ this.reconnectTimeout = setTimeout(async () => {
146
+ this.reconnectTimeout = null;
147
+ try
148
+ {
149
+ await this.connect();
150
+ } catch
151
+ {
152
+ // connect() will schedule another attempt if needed
153
+ }
154
+ }, delay);
155
+ }
156
+
157
+ /**
158
+ * Send data with guaranteed delivery.
159
+ *
160
+ * If disconnected, the message is queued and will be sent
161
+ * automatically when the connection is restored.
162
+ *
163
+ * @param data - Data to send
164
+ * @returns Promise that resolves when data is acknowledged
165
+ */
166
+ async send (data: Buffer | string): Promise<void> {
167
+ const buffer = typeof data === 'string' ? Buffer.from(data) : data;
168
+
169
+ if (this.isConnected && this.bridge)
170
+ {
171
+ // Connected - send immediately
172
+ try
173
+ {
174
+ await this.bridge.send(buffer);
175
+ } catch (err)
176
+ {
177
+ // Send failed - queue it and handle disconnect
178
+ return this.queueMessage(buffer);
179
+ }
180
+ } else
181
+ {
182
+ // Disconnected - queue the message
183
+ return this.queueMessage(buffer);
184
+ }
185
+ }
186
+
187
+ private queueMessage (data: Buffer): Promise<void> {
188
+ return new Promise((resolve, reject) => {
189
+ // Enforce max queue size (drop oldest)
190
+ while (this.messageQueue.length >= this.options.maxQueueSize)
191
+ {
192
+ const dropped = this.messageQueue.shift();
193
+ dropped?.reject(new Error('Message dropped: queue overflow'));
194
+ }
195
+
196
+ this.messageQueue.push({
197
+ data,
198
+ resolve,
199
+ reject,
200
+ timestamp: Date.now(),
201
+ });
202
+ });
203
+ }
204
+
205
+ private async flushQueue (): Promise<void> {
206
+ while (this.messageQueue.length > 0 && this.isConnected && this.bridge)
207
+ {
208
+ const msg = this.messageQueue.shift()!;
209
+ try
210
+ {
211
+ await this.bridge.send(msg.data);
212
+ msg.resolve();
213
+ } catch (err)
214
+ {
215
+ // Put it back at the front and stop flushing
216
+ this.messageQueue.unshift(msg);
217
+ break;
218
+ }
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Close the connection permanently.
224
+ * Rejects all pending queued messages.
225
+ */
226
+ async close (): Promise<void> {
227
+ this.shouldReconnect = false;
228
+
229
+ if (this.reconnectTimeout)
230
+ {
231
+ clearTimeout(this.reconnectTimeout);
232
+ this.reconnectTimeout = null;
233
+ }
234
+
235
+ // Reject all queued messages
236
+ for (const msg of this.messageQueue)
237
+ {
238
+ msg.reject(new Error('Connection closed'));
239
+ }
240
+ this.messageQueue = [];
241
+
242
+ if (this.bridge)
243
+ {
244
+ await this.bridge.close();
245
+ this.bridge = null;
246
+ }
247
+
248
+ this.isConnected = false;
249
+ this.emit('close');
250
+ }
251
+
252
+ /** Current connection state */
253
+ get connected (): boolean {
254
+ return this.isConnected;
255
+ }
256
+
257
+ /** Number of messages waiting in queue */
258
+ get queueLength (): number {
259
+ return this.messageQueue.length;
260
+ }
261
+
262
+ // Type-safe event emitter
263
+ on<K extends keyof ResilientEvents> (event: K, listener: ResilientEvents[K]): this {
264
+ return super.on(event, listener);
265
+ }
266
+
267
+ once<K extends keyof ResilientEvents> (event: K, listener: ResilientEvents[K]): this {
268
+ return super.once(event, listener);
269
+ }
270
+
271
+ emit<K extends keyof ResilientEvents> (event: K, ...args: Parameters<ResilientEvents[K]>): boolean {
272
+ return super.emit(event, ...args);
273
+ }
274
+ }
275
+
276
+ export default ResilientDataBridge;
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@technoculture/data-bridge",
3
+ "version": "0.1.0",
4
+ "description": "Guaranteed reliable serial communication for Node.js and Electron",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.mjs",
11
+ "require": "./dist/index.js",
12
+ "types": "./dist/index.d.ts"
13
+ }
14
+ },
15
+ "scripts": {
16
+ "install": "prebuild-install || cmake-js compile",
17
+ "build": "npm run build:native && npm run build:ts",
18
+ "build:native": "cmake-js compile",
19
+ "build:ts": "tsup lib/index.ts --format cjs,esm --dts",
20
+ "prebuild": "cmake-js compile -O prebuilds/$(node -p \"process.platform + '-' + process.arch\")",
21
+ "prepublishOnly": "npm run build:ts",
22
+ "test": "vitest run",
23
+ "clean": "cmake-js clean && rm -rf dist prebuilds"
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "prebuilds",
28
+ "src",
29
+ "lib",
30
+ "CMakeLists.txt"
31
+ ],
32
+ "dependencies": {
33
+ "node-addon-api": "^7.0.0",
34
+ "prebuild-install": "^7.1.1"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^20.10.0",
38
+ "cmake-js": "^7.3.0",
39
+ "tsup": "^8.0.0",
40
+ "typescript": "^5.3.0",
41
+ "vitest": "^1.0.0"
42
+ },
43
+ "engines": {
44
+ "node": ">=18.0.0"
45
+ },
46
+ "os": [
47
+ "win32",
48
+ "linux",
49
+ "darwin"
50
+ ],
51
+ "cpu": [
52
+ "x64",
53
+ "arm64",
54
+ "ia32"
55
+ ],
56
+ "binary": {
57
+ "napi_versions": [
58
+ 8
59
+ ]
60
+ },
61
+ "repository": {
62
+ "type": "git",
63
+ "url": "https://github.com/aspect-labs/data-bridge"
64
+ },
65
+ "keywords": [
66
+ "serial",
67
+ "uart",
68
+ "reliable",
69
+ "medical",
70
+ "embedded",
71
+ "electron",
72
+ "napi"
73
+ ],
74
+ "license": "MIT"
75
+ }
@@ -0,0 +1,6 @@
1
+ # ninja log v6
2
+ 1 42 1767813639052166764 CMakeFiles/data_bridge_node.dir/Users/satyamtiwary/Documents/Python-Things/data-bridge/src/protocol/crc16.cpp.o 44a8848073165fd4
3
+ 1 286 1767813639052016345 CMakeFiles/data_bridge_node.dir/src/serial_wrapper.cpp.o 9b7a8f38fa1f98b2
4
+ 1 301 1767813639052311933 CMakeFiles/data_bridge_node.dir/Users/satyamtiwary/Documents/Python-Things/data-bridge/src/transport/platform/linux/linux_serial.cpp.o 1a4595fea1e208f8
5
+ 1 698 1767813639051708840 CMakeFiles/data_bridge_node.dir/src/addon.cpp.o 7d74cb8705ae44b7
6
+ 698 745 1767813639749307606 Release/data_bridge_node.node 1305e389db74efac