@shet-anirudh/crdt-sync-transport-ws 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.
- package/LICENSE +21 -0
- package/dist/__tests__/serialization.test.d.ts +2 -0
- package/dist/__tests__/serialization.test.d.ts.map +1 -0
- package/dist/__tests__/serialization.test.js +39 -0
- package/dist/__tests__/serialization.test.js.map +1 -0
- package/dist/__tests__/transport.test.d.ts +2 -0
- package/dist/__tests__/transport.test.d.ts.map +1 -0
- package/dist/__tests__/transport.test.js +90 -0
- package/dist/__tests__/transport.test.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/relay-server.d.ts +36 -0
- package/dist/relay-server.d.ts.map +1 -0
- package/dist/relay-server.js +195 -0
- package/dist/relay-server.js.map +1 -0
- package/dist/serialization.d.ts +36 -0
- package/dist/serialization.d.ts.map +1 -0
- package/dist/serialization.js +137 -0
- package/dist/serialization.js.map +1 -0
- package/dist/types.d.ts +64 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/ws-transport.d.ts +49 -0
- package/dist/ws-transport.d.ts.map +1 -0
- package/dist/ws-transport.js +188 -0
- package/dist/ws-transport.js.map +1 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 crdt-sync contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serialization.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/serialization.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { createRecord, applyLocalChange, deleteField } from '@shet-anirudh/crdt-sync-core';
|
|
3
|
+
import { serializeRecord, deserializeRecord } from '../serialization.js';
|
|
4
|
+
describe('MessagePack serialization', () => {
|
|
5
|
+
it('serializes and deserializes an empty record', () => {
|
|
6
|
+
const record = createRecord();
|
|
7
|
+
const bytes = serializeRecord(record);
|
|
8
|
+
const restored = deserializeRecord(bytes);
|
|
9
|
+
// Deep equal check
|
|
10
|
+
expect(restored).toEqual(record);
|
|
11
|
+
// Verify it uses a null prototype like the original
|
|
12
|
+
expect(Object.getPrototypeOf(restored)).toBeNull();
|
|
13
|
+
});
|
|
14
|
+
it('serializes and deserializes a record with various field types', () => {
|
|
15
|
+
let record = createRecord();
|
|
16
|
+
const now = 1000;
|
|
17
|
+
record = applyLocalChange(record, 'stringField', 'hello', 'dev-a', now);
|
|
18
|
+
record = applyLocalChange(record, 'numberField', 42, 'dev-a', now);
|
|
19
|
+
record = applyLocalChange(record, 'booleanField', true, 'dev-a', now);
|
|
20
|
+
const bytes = serializeRecord(record);
|
|
21
|
+
const restored = deserializeRecord(bytes);
|
|
22
|
+
expect(restored).toEqual(record);
|
|
23
|
+
expect(restored['stringField']?.value).toBe('hello');
|
|
24
|
+
expect(restored['stringField']?.origin).toBe('dev-a');
|
|
25
|
+
expect(restored['numberField']?.value).toBe(42);
|
|
26
|
+
expect(restored['booleanField']?.value).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
it('serializes and deserializes tombstones (deleted fields)', () => {
|
|
29
|
+
let record = createRecord();
|
|
30
|
+
record = applyLocalChange(record, 'temp', 'value', 'dev', 1000);
|
|
31
|
+
record = deleteField(record, 'temp', 'dev', 2000);
|
|
32
|
+
const bytes = serializeRecord(record);
|
|
33
|
+
const restored = deserializeRecord(bytes);
|
|
34
|
+
expect(restored).toEqual(record);
|
|
35
|
+
expect(restored['temp']?.deleted).toBe(true);
|
|
36
|
+
expect(restored['temp']?.value).toBeNull();
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
//# sourceMappingURL=serialization.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serialization.test.js","sourceRoot":"","sources":["../../src/__tests__/serialization.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAC3F,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAEzE,QAAQ,CAAC,2BAA2B,EAAE,GAAG,EAAE;IACzC,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACrD,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;QAC9B,MAAM,KAAK,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;QACtC,MAAM,QAAQ,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAC;QAE1C,mBAAmB;QACnB,MAAM,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAEjC,oDAAoD;QACpD,MAAM,CAAC,MAAM,CAAC,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;IACrD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+DAA+D,EAAE,GAAG,EAAE;QACvE,IAAI,MAAM,GAAG,YAAY,EAAE,CAAC;QAC5B,MAAM,GAAG,GAAG,IAAI,CAAC;QAEjB,MAAM,GAAG,gBAAgB,CAAC,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC;QACxE,MAAM,GAAG,gBAAgB,CAAC,MAAM,EAAE,aAAa,EAAE,EAAE,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC;QACnE,MAAM,GAAG,gBAAgB,CAAC,MAAM,EAAE,cAAc,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC;QAEtE,MAAM,KAAK,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;QACtC,MAAM,QAAQ,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAC;QAE1C,MAAM,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAEjC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,KAAK,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACrD,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACtD,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,KAAK,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAChD,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yDAAyD,EAAE,GAAG,EAAE;QACjE,IAAI,MAAM,GAAG,YAAY,EAAE,CAAC;QAC5B,MAAM,GAAG,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;QAChE,MAAM,GAAG,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;QAElD,MAAM,KAAK,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;QACtC,MAAM,QAAQ,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAC;QAE1C,MAAM,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QACjC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC7C,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,KAAK,CAAC,CAAC,QAAQ,EAAE,CAAC;IAC7C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/transport.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import { createRecord, applyLocalChange, mergeRecords } from '@shet-anirudh/crdt-sync-core';
|
|
3
|
+
import { startRelayServer } from '../relay-server.js';
|
|
4
|
+
import { WebSocketTransport } from '../ws-transport.js';
|
|
5
|
+
import { serializeRecord, deserializeRecord } from '../serialization.js';
|
|
6
|
+
describe('WebSocket Transport Integration', () => {
|
|
7
|
+
let server;
|
|
8
|
+
let serverUrl;
|
|
9
|
+
beforeAll(async () => {
|
|
10
|
+
// Start on random available port
|
|
11
|
+
server = await startRelayServer({ port: 0 });
|
|
12
|
+
serverUrl = `ws://localhost:${server.port}`;
|
|
13
|
+
});
|
|
14
|
+
afterAll(async () => {
|
|
15
|
+
if (server) {
|
|
16
|
+
await server.close();
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
it('connects two peers and exchanges messages', async () => {
|
|
20
|
+
const transportA = new WebSocketTransport({ serverUrl, initialReconnectDelayMs: 100 });
|
|
21
|
+
const transportB = new WebSocketTransport({ serverUrl, initialReconnectDelayMs: 100 });
|
|
22
|
+
const messagesReceivedByA = [];
|
|
23
|
+
const messagesReceivedByB = [];
|
|
24
|
+
transportA.onReceive((from, payload) => {
|
|
25
|
+
expect(from).toBe('peer-b');
|
|
26
|
+
messagesReceivedByA.push(payload);
|
|
27
|
+
});
|
|
28
|
+
transportB.onReceive((from, payload) => {
|
|
29
|
+
expect(from).toBe('peer-a');
|
|
30
|
+
messagesReceivedByB.push(payload);
|
|
31
|
+
});
|
|
32
|
+
// 1. Connect
|
|
33
|
+
await Promise.all([
|
|
34
|
+
transportA.connect('peer-a'),
|
|
35
|
+
transportB.connect('peer-b'),
|
|
36
|
+
]);
|
|
37
|
+
expect(transportA.isConnected).toBe(true);
|
|
38
|
+
expect(transportB.isConnected).toBe(true);
|
|
39
|
+
// 2. Exchange CRDT records
|
|
40
|
+
let recordA = createRecord();
|
|
41
|
+
recordA = applyLocalChange(recordA, 'title', 'Hello from A', 'peer-a', Date.now());
|
|
42
|
+
let recordB = createRecord();
|
|
43
|
+
recordB = applyLocalChange(recordB, 'status', 'Active', 'peer-b', Date.now());
|
|
44
|
+
// A sends to B
|
|
45
|
+
await transportA.send('peer-b', serializeRecord(recordA));
|
|
46
|
+
// B sends to A
|
|
47
|
+
await transportB.send('peer-a', serializeRecord(recordB));
|
|
48
|
+
// Wait a moment for delivery
|
|
49
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
50
|
+
expect(messagesReceivedByA.length).toBe(1);
|
|
51
|
+
expect(messagesReceivedByB.length).toBe(1);
|
|
52
|
+
// 3. Deserialize and Merge
|
|
53
|
+
const receivedRecordA = deserializeRecord(messagesReceivedByB[0]); // B received A's record
|
|
54
|
+
const receivedRecordB = deserializeRecord(messagesReceivedByA[0]); // A received B's record
|
|
55
|
+
const mergedAtA = mergeRecords(recordA, receivedRecordB);
|
|
56
|
+
const mergedAtB = mergeRecords(recordB, receivedRecordA);
|
|
57
|
+
// Both should now have exactly the same data
|
|
58
|
+
expect(mergedAtA['title']?.value).toBe('Hello from A');
|
|
59
|
+
expect(mergedAtA['status']?.value).toBe('Active');
|
|
60
|
+
expect(mergedAtA).toEqual(mergedAtB);
|
|
61
|
+
// 4. Disconnect
|
|
62
|
+
await Promise.all([
|
|
63
|
+
transportA.disconnect(),
|
|
64
|
+
transportB.disconnect(),
|
|
65
|
+
]);
|
|
66
|
+
expect(transportA.isConnected).toBe(false);
|
|
67
|
+
expect(transportB.isConnected).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
it('buffers messages sent while offline and flushes on reconnect', async () => {
|
|
70
|
+
const transportA = new WebSocketTransport({ serverUrl, initialReconnectDelayMs: 100 });
|
|
71
|
+
const transportB = new WebSocketTransport({ serverUrl, initialReconnectDelayMs: 100 });
|
|
72
|
+
const receivedByB = [];
|
|
73
|
+
transportB.onReceive((_from, payload) => {
|
|
74
|
+
receivedByB.push(payload);
|
|
75
|
+
});
|
|
76
|
+
await transportB.connect('peer-b');
|
|
77
|
+
// A sends a message BEFORE connecting
|
|
78
|
+
const payload = new Uint8Array([1, 2, 3]);
|
|
79
|
+
await transportA.send('peer-b', payload);
|
|
80
|
+
expect(receivedByB.length).toBe(0);
|
|
81
|
+
// Connect A — it should flush the queue
|
|
82
|
+
await transportA.connect('peer-a');
|
|
83
|
+
// Wait for delivery
|
|
84
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
85
|
+
expect(receivedByB.length).toBe(1);
|
|
86
|
+
expect(receivedByB[0]).toEqual(payload);
|
|
87
|
+
await Promise.all([transportA.disconnect(), transportB.disconnect()]);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
//# sourceMappingURL=transport.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.test.js","sourceRoot":"","sources":["../../src/__tests__/transport.test.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @shet-anirudh/crdt-sync-transport-ws — Public API
|
|
3
|
+
*
|
|
4
|
+
* Provides a WebSocket-based transport adapter and relay server for the
|
|
5
|
+
* CRDT sync engine.
|
|
6
|
+
*/
|
|
7
|
+
export type { PeerId, SyncTransport, WireMessage } from './types.js';
|
|
8
|
+
export { serializeRecord, deserializeRecord, serializeMessage, deserializeMessage, } from './serialization.js';
|
|
9
|
+
export { WebSocketTransport, type WebSocketTransportOptions, } from './ws-transport.js';
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAErE,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,gBAAgB,EAChB,kBAAkB,GACnB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,kBAAkB,EAClB,KAAK,yBAAyB,GAC/B,MAAM,mBAAmB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @shet-anirudh/crdt-sync-transport-ws — Public API
|
|
3
|
+
*
|
|
4
|
+
* Provides a WebSocket-based transport adapter and relay server for the
|
|
5
|
+
* CRDT sync engine.
|
|
6
|
+
*/
|
|
7
|
+
export { serializeRecord, deserializeRecord, serializeMessage, deserializeMessage, } from './serialization.js';
|
|
8
|
+
export { WebSocketTransport, } from './ws-transport.js';
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,gBAAgB,EAChB,kBAAkB,GACnB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,kBAAkB,GAEnB,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket Relay Server
|
|
3
|
+
*
|
|
4
|
+
* A minimal relay server that connects two (or more) peers by name.
|
|
5
|
+
* Peers register with their DeviceId, then send messages addressed to
|
|
6
|
+
* another DeviceId. The server forwards the raw bytes — it is CRDT-unaware.
|
|
7
|
+
*
|
|
8
|
+
* Architecture:
|
|
9
|
+
* [Device A] ──ws──▶ [Relay Server] ──ws──▶ [Device B]
|
|
10
|
+
*
|
|
11
|
+
* Why relay mode instead of direct P2P?
|
|
12
|
+
* Direct P2P needs NAT traversal (ICE/STUN/TURN). A relay is simpler
|
|
13
|
+
* to demo reliably and is the standard starting point before adding
|
|
14
|
+
* WebRTC or Nearby Connections.
|
|
15
|
+
*
|
|
16
|
+
* Wire protocol (all messages are MessagePack-encoded):
|
|
17
|
+
* Client → Server: { type: 'register', peerId: string }
|
|
18
|
+
* { type: 'send', to: string, payload: Uint8Array }
|
|
19
|
+
* Server → Client: { type: 'message', from: string, payload: Uint8Array }
|
|
20
|
+
* { type: 'peer_joined', peerId: string }
|
|
21
|
+
* { type: 'peer_left', peerId: string }
|
|
22
|
+
* { type: 'error', message: string }
|
|
23
|
+
*/
|
|
24
|
+
export interface RelayServerOptions {
|
|
25
|
+
port?: number;
|
|
26
|
+
host?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Start the relay server.
|
|
30
|
+
* @returns A promise resolving to an object with the bound port and a close() method.
|
|
31
|
+
*/
|
|
32
|
+
export declare function startRelayServer(options?: RelayServerOptions): Promise<{
|
|
33
|
+
close: () => Promise<void>;
|
|
34
|
+
port: number;
|
|
35
|
+
}>;
|
|
36
|
+
//# sourceMappingURL=relay-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relay-server.d.ts","sourceRoot":"","sources":["../src/relay-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAOH,MAAM,WAAW,kBAAkB;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAOD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC;IAC1E,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3B,IAAI,EAAE,MAAM,CAAC;CACd,CAAC,CAsJD"}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket Relay Server
|
|
3
|
+
*
|
|
4
|
+
* A minimal relay server that connects two (or more) peers by name.
|
|
5
|
+
* Peers register with their DeviceId, then send messages addressed to
|
|
6
|
+
* another DeviceId. The server forwards the raw bytes — it is CRDT-unaware.
|
|
7
|
+
*
|
|
8
|
+
* Architecture:
|
|
9
|
+
* [Device A] ──ws──▶ [Relay Server] ──ws──▶ [Device B]
|
|
10
|
+
*
|
|
11
|
+
* Why relay mode instead of direct P2P?
|
|
12
|
+
* Direct P2P needs NAT traversal (ICE/STUN/TURN). A relay is simpler
|
|
13
|
+
* to demo reliably and is the standard starting point before adding
|
|
14
|
+
* WebRTC or Nearby Connections.
|
|
15
|
+
*
|
|
16
|
+
* Wire protocol (all messages are MessagePack-encoded):
|
|
17
|
+
* Client → Server: { type: 'register', peerId: string }
|
|
18
|
+
* { type: 'send', to: string, payload: Uint8Array }
|
|
19
|
+
* Server → Client: { type: 'message', from: string, payload: Uint8Array }
|
|
20
|
+
* { type: 'peer_joined', peerId: string }
|
|
21
|
+
* { type: 'peer_left', peerId: string }
|
|
22
|
+
* { type: 'error', message: string }
|
|
23
|
+
*/
|
|
24
|
+
import { createServer } from 'http';
|
|
25
|
+
import { decode, encode } from '@msgpack/msgpack';
|
|
26
|
+
import { WebSocket, WebSocketServer } from 'ws';
|
|
27
|
+
/**
|
|
28
|
+
* Start the relay server.
|
|
29
|
+
* @returns A promise resolving to an object with the bound port and a close() method.
|
|
30
|
+
*/
|
|
31
|
+
export function startRelayServer(options = {}) {
|
|
32
|
+
return new Promise((resolveStart) => {
|
|
33
|
+
const port = options.port ?? 8787;
|
|
34
|
+
const host = options.host ?? '0.0.0.0';
|
|
35
|
+
const httpServer = createServer();
|
|
36
|
+
const wss = new WebSocketServer({ server: httpServer });
|
|
37
|
+
// PeerId → ConnectedPeer
|
|
38
|
+
const peers = new Map();
|
|
39
|
+
wss.on('connection', (socket) => {
|
|
40
|
+
let registeredPeerId = null;
|
|
41
|
+
socket.on('message', (raw) => {
|
|
42
|
+
let msg;
|
|
43
|
+
try {
|
|
44
|
+
const decoded = decode(raw);
|
|
45
|
+
if (typeof decoded !== 'object' || decoded === null) {
|
|
46
|
+
sendError(socket, 'Message must be an object');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
msg = decoded;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
sendError(socket, 'Failed to decode MessagePack message');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const type = msg['type'];
|
|
56
|
+
// ── Register ──────────────────────────────────────────────────────────
|
|
57
|
+
if (type === 'register') {
|
|
58
|
+
const peerId = msg['peerId'];
|
|
59
|
+
if (typeof peerId !== 'string' || peerId.length === 0) {
|
|
60
|
+
sendError(socket, 'register.peerId must be a non-empty string');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
// If the peer is already registered (e.g. React StrictMode remount or
|
|
64
|
+
// reconnect), close the stale connection and accept the new one.
|
|
65
|
+
if (peers.has(peerId)) {
|
|
66
|
+
const stale = peers.get(peerId);
|
|
67
|
+
if (stale.socket !== socket) {
|
|
68
|
+
stale.socket.close();
|
|
69
|
+
}
|
|
70
|
+
peers.delete(peerId);
|
|
71
|
+
}
|
|
72
|
+
registeredPeerId = peerId;
|
|
73
|
+
peers.set(peerId, { peerId, socket });
|
|
74
|
+
console.log(`[relay] peer registered: ${peerId} (${peers.size} total)`);
|
|
75
|
+
// Reply to sender to confirm registration (ws-transport awaits this)
|
|
76
|
+
socket.send(encode({ type: 'registered' }));
|
|
77
|
+
// Notify all other peers that someone joined
|
|
78
|
+
broadcast(peerId, encode({ type: 'peer_joined', peerId }), peers);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// ── Send ──────────────────────────────────────────────────────────────
|
|
82
|
+
if (type === 'send') {
|
|
83
|
+
if (registeredPeerId === null) {
|
|
84
|
+
sendError(socket, 'Must register before sending');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const to = msg['to'];
|
|
88
|
+
const payload = msg['payload'];
|
|
89
|
+
if (typeof to !== 'string') {
|
|
90
|
+
sendError(socket, 'send.to must be a string');
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (!(payload instanceof Uint8Array) && !Buffer.isBuffer(payload)) {
|
|
94
|
+
sendError(socket, 'send.payload must be a Uint8Array');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const recipient = peers.get(to);
|
|
98
|
+
if (recipient === undefined) {
|
|
99
|
+
sendError(socket, `Peer "${to}" is not connected`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
// Forward the payload to the recipient, tagging it with the sender
|
|
103
|
+
const envelope = encode({
|
|
104
|
+
type: 'message',
|
|
105
|
+
from: registeredPeerId,
|
|
106
|
+
payload: payload instanceof Buffer ? new Uint8Array(payload) : payload,
|
|
107
|
+
});
|
|
108
|
+
recipient.socket.send(envelope);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
// ── Broadcast ─────────────────────────────────────────────────────────
|
|
112
|
+
if (type === 'broadcast') {
|
|
113
|
+
if (registeredPeerId === null) {
|
|
114
|
+
sendError(socket, 'Must register before broadcasting');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const payload = msg['payload'];
|
|
118
|
+
if (!(payload instanceof Uint8Array) && !Buffer.isBuffer(payload)) {
|
|
119
|
+
sendError(socket, 'broadcast.payload must be a Uint8Array');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const bytes = payload instanceof Buffer ? new Uint8Array(payload) : payload;
|
|
123
|
+
const envelope = encode({ type: 'message', from: registeredPeerId, payload: bytes });
|
|
124
|
+
broadcast(registeredPeerId, envelope, peers);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
sendError(socket, `Unknown message type: ${String(type)}`);
|
|
128
|
+
});
|
|
129
|
+
socket.on('close', () => {
|
|
130
|
+
if (registeredPeerId !== null) {
|
|
131
|
+
peers.delete(registeredPeerId);
|
|
132
|
+
console.log(`[relay] peer left: ${registeredPeerId} (${peers.size} remaining)`);
|
|
133
|
+
broadcast(registeredPeerId, encode({ type: 'peer_left', peerId: registeredPeerId }), peers);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
socket.on('error', (err) => {
|
|
137
|
+
console.error(`[relay] socket error for ${registeredPeerId ?? 'unregistered'}:`, err.message);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
httpServer.listen(port, host, () => {
|
|
141
|
+
const address = httpServer.address();
|
|
142
|
+
const boundPort = address && typeof address === 'object' ? address.port : port;
|
|
143
|
+
console.log(`[relay] server listening on ws://${host}:${boundPort}`);
|
|
144
|
+
resolveStart({
|
|
145
|
+
port: boundPort,
|
|
146
|
+
close: () => new Promise((resolve, reject) => {
|
|
147
|
+
wss.close((err) => {
|
|
148
|
+
if (err) {
|
|
149
|
+
reject(err);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
httpServer.close((err2) => {
|
|
153
|
+
if (err2)
|
|
154
|
+
reject(err2);
|
|
155
|
+
else
|
|
156
|
+
resolve();
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
}),
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// Helpers
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
function sendError(socket, message) {
|
|
168
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
169
|
+
socket.send(encode({ type: 'error', message }));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/** Send encoded bytes to all peers except the sender. */
|
|
173
|
+
function broadcast(excludePeerId, encoded, peers) {
|
|
174
|
+
for (const [id, peer] of peers) {
|
|
175
|
+
if (id !== excludePeerId && peer.socket.readyState === WebSocket.OPEN) {
|
|
176
|
+
peer.socket.send(encoded);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// CLI entrypoint — auto-start when run directly: node dist/relay-server.js
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
const isMain = typeof process !== 'undefined' &&
|
|
184
|
+
process.argv[1] !== undefined &&
|
|
185
|
+
(process.argv[1].endsWith('relay-server.js') || process.argv[1].endsWith('relay-server.ts'));
|
|
186
|
+
if (isMain) {
|
|
187
|
+
const port = process.env['PORT'] ? Number(process.env['PORT']) : 8787;
|
|
188
|
+
startRelayServer({ port }).then(({ port: bound }) => {
|
|
189
|
+
console.log(`[relay] ready — ws://localhost:${bound}`);
|
|
190
|
+
}).catch((err) => {
|
|
191
|
+
console.error('[relay] failed to start:', err);
|
|
192
|
+
process.exit(1);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=relay-server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relay-server.js","sourceRoot":"","sources":["../src/relay-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,MAAM,CAAC;AAEpC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,IAAI,CAAC;AAYhD;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,UAA8B,EAAE;IAI/D,OAAO,IAAI,OAAO,CAAC,CAAC,YAAY,EAAE,EAAE;QAClC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,IAAI,CAAC;QAClC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,SAAS,CAAC;QAEvC,MAAM,UAAU,GAAG,YAAY,EAAE,CAAC;QAClC,MAAM,GAAG,GAAG,IAAI,eAAe,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;QAExD,yBAAyB;QACzB,MAAM,KAAK,GAAG,IAAI,GAAG,EAAyB,CAAC;QAE/C,GAAG,CAAC,EAAE,CAAC,YAAY,EAAE,CAAC,MAAiB,EAAE,EAAE;YACzC,IAAI,gBAAgB,GAAkB,IAAI,CAAC;YAE3C,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,GAAW,EAAE,EAAE;gBACnC,IAAI,GAA4B,CAAC;gBACjC,IAAI,CAAC;oBACH,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;oBAC5B,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;wBACpD,SAAS,CAAC,MAAM,EAAE,2BAA2B,CAAC,CAAC;wBAC/C,OAAO;oBACT,CAAC;oBACD,GAAG,GAAG,OAAkC,CAAC;gBAC3C,CAAC;gBAAC,MAAM,CAAC;oBACP,SAAS,CAAC,MAAM,EAAE,sCAAsC,CAAC,CAAC;oBAC1D,OAAO;gBACT,CAAC;gBAED,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;gBAEzB,yEAAyE;gBACzE,IAAI,IAAI,KAAK,UAAU,EAAE,CAAC;oBACxB,MAAM,MAAM,GAAG,GAAG,CAAC,QAAQ,CAAC,CAAC;oBAC7B,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;wBACtD,SAAS,CAAC,MAAM,EAAE,4CAA4C,CAAC,CAAC;wBAChE,OAAO;oBACT,CAAC;oBACD,sEAAsE;oBACtE,iEAAiE;oBACjE,IAAI,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;wBACtB,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,MAAM,CAAE,CAAC;wBACjC,IAAI,KAAK,CAAC,MAAM,KAAK,MAAM,EAAE,CAAC;4BAC5B,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;wBACvB,CAAC;wBACD,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;oBACvB,CAAC;oBAED,gBAAgB,GAAG,MAAM,CAAC;oBAC1B,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;oBAEtC,OAAO,CAAC,GAAG,CAAC,4BAA4B,MAAM,KAAK,KAAK,CAAC,IAAI,SAAS,CAAC,CAAC;oBAExE,qEAAqE;oBACrE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC;oBAE5C,6CAA6C;oBAC7C,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC;oBAClE,OAAO;gBACT,CAAC;gBAED,yEAAyE;gBACzE,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;oBACpB,IAAI,gBAAgB,KAAK,IAAI,EAAE,CAAC;wBAC9B,SAAS,CAAC,MAAM,EAAE,8BAA8B,CAAC,CAAC;wBAClD,OAAO;oBACT,CAAC;oBAED,MAAM,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC;oBACrB,MAAM,OAAO,GAAG,GAAG,CAAC,SAAS,CAAC,CAAC;oBAE/B,IAAI,OAAO,EAAE,KAAK,QAAQ,EAAE,CAAC;wBAC3B,SAAS,CAAC,MAAM,EAAE,0BAA0B,CAAC,CAAC;wBAC9C,OAAO;oBACT,CAAC;oBACD,IAAI,CAAC,CAAC,OAAO,YAAY,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;wBAClE,SAAS,CAAC,MAAM,EAAE,mCAAmC,CAAC,CAAC;wBACvD,OAAO;oBACT,CAAC;oBAED,MAAM,SAAS,GAAG,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;oBAChC,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;wBAC5B,SAAS,CAAC,MAAM,EAAE,SAAS,EAAE,oBAAoB,CAAC,CAAC;wBACnD,OAAO;oBACT,CAAC;oBAED,mEAAmE;oBACnE,MAAM,QAAQ,GAAG,MAAM,CAAC;wBACtB,IAAI,EAAE,SAAS;wBACf,IAAI,EAAE,gBAAgB;wBACtB,OAAO,EAAE,OAAO,YAAY,MAAM,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO;qBACvE,CAAC,CAAC;oBACH,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;oBAChC,OAAO;gBACT,CAAC;gBAED,yEAAyE;gBACzE,IAAI,IAAI,KAAK,WAAW,EAAE,CAAC;oBACzB,IAAI,gBAAgB,KAAK,IAAI,EAAE,CAAC;wBAC9B,SAAS,CAAC,MAAM,EAAE,mCAAmC,CAAC,CAAC;wBACvD,OAAO;oBACT,CAAC;oBAED,MAAM,OAAO,GAAG,GAAG,CAAC,SAAS,CAAC,CAAC;oBAC/B,IAAI,CAAC,CAAC,OAAO,YAAY,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;wBAClE,SAAS,CAAC,MAAM,EAAE,wCAAwC,CAAC,CAAC;wBAC5D,OAAO;oBACT,CAAC;oBAED,MAAM,KAAK,GAAG,OAAO,YAAY,MAAM,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;oBAC5E,MAAM,QAAQ,GAAG,MAAM,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;oBACrF,SAAS,CAAC,gBAAgB,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC;oBAC7C,OAAO;gBACT,CAAC;gBAED,SAAS,CAAC,MAAM,EAAE,yBAAyB,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAC7D,CAAC,CAAC,CAAC;YAEH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;gBACtB,IAAI,gBAAgB,KAAK,IAAI,EAAE,CAAC;oBAC9B,KAAK,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC;oBAC/B,OAAO,CAAC,GAAG,CAAC,sBAAsB,gBAAgB,KAAK,KAAK,CAAC,IAAI,aAAa,CAAC,CAAC;oBAChF,SAAS,CAAC,gBAAgB,EAAE,MAAM,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC;gBAC9F,CAAC;YACH,CAAC,CAAC,CAAC;YAEH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAU,EAAE,EAAE;gBAChC,OAAO,CAAC,KAAK,CAAC,4BAA4B,gBAAgB,IAAI,cAAc,GAAG,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;YAChG,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;QAEH,UAAU,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE;YACjC,MAAM,OAAO,GAAG,UAAU,CAAC,OAAO,EAAE,CAAC;YACrC,MAAM,SAAS,GAAG,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;YAC/E,OAAO,CAAC,GAAG,CAAC,oCAAoC,IAAI,IAAI,SAAS,EAAE,CAAC,CAAC;YAErE,YAAY,CAAC;gBACX,IAAI,EAAE,SAAS;gBACf,KAAK,EAAE,GAAkB,EAAE,CACzB,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;oBAC9B,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;wBAChB,IAAI,GAAG,EAAE,CAAC;4BAAC,MAAM,CAAC,GAAG,CAAC,CAAC;4BAAC,OAAO;wBAAC,CAAC;wBACjC,UAAU,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE;4BACxB,IAAI,IAAI;gCAAE,MAAM,CAAC,IAAI,CAAC,CAAC;;gCAClB,OAAO,EAAE,CAAC;wBACjB,CAAC,CAAC,CAAC;oBACL,CAAC,CAAC,CAAC;gBACL,CAAC,CAAC;aACL,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAED,8EAA8E;AAC9E,UAAU;AACV,8EAA8E;AAE9E,SAAS,SAAS,CAAC,MAAiB,EAAE,OAAe;IACnD,IAAI,MAAM,CAAC,UAAU,KAAK,SAAS,CAAC,IAAI,EAAE,CAAC;QACzC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC;IAClD,CAAC;AACH,CAAC;AAED,yDAAyD;AACzD,SAAS,SAAS,CAChB,aAAqB,EACrB,OAAmB,EACnB,KAAiC;IAEjC,KAAK,MAAM,CAAC,EAAE,EAAE,IAAI,CAAC,IAAI,KAAK,EAAE,CAAC;QAC/B,IAAI,EAAE,KAAK,aAAa,IAAI,IAAI,CAAC,MAAM,CAAC,UAAU,KAAK,SAAS,CAAC,IAAI,EAAE,CAAC;YACtE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC5B,CAAC;IACH,CAAC;AACH,CAAC;AAED,8EAA8E;AAC9E,2EAA2E;AAC3E,8EAA8E;AAC9E,MAAM,MAAM,GACV,OAAO,OAAO,KAAK,WAAW;IAC9B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,SAAS;IAC7B,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,iBAAiB,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,iBAAiB,CAAC,CAAC,CAAC;AAE/F,IAAI,MAAM,EAAE,CAAC;IACX,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACtE,gBAAgB,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE;QAClD,OAAO,CAAC,GAAG,CAAC,kCAAkC,KAAK,EAAE,CAAC,CAAC;IACzD,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE;QACxB,OAAO,CAAC,KAAK,CAAC,0BAA0B,EAAE,GAAG,CAAC,CAAC;QAC/C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MessagePack serialization for CRDT records.
|
|
3
|
+
*
|
|
4
|
+
* Why MessagePack over JSON?
|
|
5
|
+
* - Binary format: no string escaping, smaller payloads (~30% on average)
|
|
6
|
+
* - Preserves numeric types (no JSON number precision loss)
|
|
7
|
+
* - Fast encode/decode
|
|
8
|
+
* - Well-supported in Node.js and browsers
|
|
9
|
+
*
|
|
10
|
+
* The wire format wraps the CRDTRecord in an envelope:
|
|
11
|
+
* { type, from, to?, record }
|
|
12
|
+
*/
|
|
13
|
+
import type { CRDTRecord } from '@shet-anirudh/crdt-sync-core';
|
|
14
|
+
import type { WireMessage } from './types.js';
|
|
15
|
+
/**
|
|
16
|
+
* Serialize a CRDTRecord into a compact binary Uint8Array using MessagePack.
|
|
17
|
+
*
|
|
18
|
+
* The record is first converted to a plain JSON-compatible object (no
|
|
19
|
+
* null-prototype, HLC expanded to { wallTime, logical }) before encoding.
|
|
20
|
+
*/
|
|
21
|
+
export declare function serializeRecord(record: CRDTRecord): Uint8Array;
|
|
22
|
+
/**
|
|
23
|
+
* Deserialize a Uint8Array (MessagePack) back into a CRDTRecord.
|
|
24
|
+
* @throws If the bytes are malformed or missing required fields.
|
|
25
|
+
*/
|
|
26
|
+
export declare function deserializeRecord(bytes: Uint8Array): CRDTRecord;
|
|
27
|
+
/**
|
|
28
|
+
* Serialize a full WireMessage (including CRDTRecord payload) to bytes.
|
|
29
|
+
*/
|
|
30
|
+
export declare function serializeMessage(msg: WireMessage): Uint8Array;
|
|
31
|
+
/**
|
|
32
|
+
* Deserialize a WireMessage from bytes.
|
|
33
|
+
* @throws If the bytes are malformed.
|
|
34
|
+
*/
|
|
35
|
+
export declare function deserializeMessage(bytes: Uint8Array): WireMessage;
|
|
36
|
+
//# sourceMappingURL=serialization.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serialization.d.ts","sourceRoot":"","sources":["../src/serialization.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAmB,MAAM,8BAA8B,CAAC;AAGhF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAM9C;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,UAAU,GAAG,UAAU,CAG9D;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,GAAG,UAAU,CAG/D;AAMD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,WAAW,GAAG,UAAU,CAW7D;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,UAAU,GAAG,WAAW,CAuBjE"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MessagePack serialization for CRDT records.
|
|
3
|
+
*
|
|
4
|
+
* Why MessagePack over JSON?
|
|
5
|
+
* - Binary format: no string escaping, smaller payloads (~30% on average)
|
|
6
|
+
* - Preserves numeric types (no JSON number precision loss)
|
|
7
|
+
* - Fast encode/decode
|
|
8
|
+
* - Well-supported in Node.js and browsers
|
|
9
|
+
*
|
|
10
|
+
* The wire format wraps the CRDTRecord in an envelope:
|
|
11
|
+
* { type, from, to?, record }
|
|
12
|
+
*/
|
|
13
|
+
import { decode, encode } from '@msgpack/msgpack';
|
|
14
|
+
import { hlcFromJSON, hlcToJSON } from '@shet-anirudh/crdt-sync-core';
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// CRDTRecord serialization
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
/**
|
|
19
|
+
* Serialize a CRDTRecord into a compact binary Uint8Array using MessagePack.
|
|
20
|
+
*
|
|
21
|
+
* The record is first converted to a plain JSON-compatible object (no
|
|
22
|
+
* null-prototype, HLC expanded to { wallTime, logical }) before encoding.
|
|
23
|
+
*/
|
|
24
|
+
export function serializeRecord(record) {
|
|
25
|
+
const plain = recordToPlain(record);
|
|
26
|
+
return encode(plain);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Deserialize a Uint8Array (MessagePack) back into a CRDTRecord.
|
|
30
|
+
* @throws If the bytes are malformed or missing required fields.
|
|
31
|
+
*/
|
|
32
|
+
export function deserializeRecord(bytes) {
|
|
33
|
+
const plain = decode(bytes);
|
|
34
|
+
return plainToRecord(plain);
|
|
35
|
+
}
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// WireMessage serialization (used by relay server + client)
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
/**
|
|
40
|
+
* Serialize a full WireMessage (including CRDTRecord payload) to bytes.
|
|
41
|
+
*/
|
|
42
|
+
export function serializeMessage(msg) {
|
|
43
|
+
const plain = {
|
|
44
|
+
type: msg.type,
|
|
45
|
+
from: msg.from,
|
|
46
|
+
};
|
|
47
|
+
if (msg.to !== undefined)
|
|
48
|
+
plain['to'] = msg.to;
|
|
49
|
+
if (msg.payload !== undefined) {
|
|
50
|
+
// payload is already a CRDTRecord; convert to plain before outer encode
|
|
51
|
+
plain['payload'] = recordToPlain(msg.payload);
|
|
52
|
+
}
|
|
53
|
+
return encode(plain);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Deserialize a WireMessage from bytes.
|
|
57
|
+
* @throws If the bytes are malformed.
|
|
58
|
+
*/
|
|
59
|
+
export function deserializeMessage(bytes) {
|
|
60
|
+
const raw = decode(bytes);
|
|
61
|
+
assertObject(raw, 'WireMessage');
|
|
62
|
+
const obj = raw;
|
|
63
|
+
const type = obj['type'];
|
|
64
|
+
const from = obj['from'];
|
|
65
|
+
if (type !== 'sync' && type !== 'ping' && type !== 'pong') {
|
|
66
|
+
throw new Error(`Invalid WireMessage type: ${String(type)}`);
|
|
67
|
+
}
|
|
68
|
+
if (typeof from !== 'string') {
|
|
69
|
+
throw new Error(`WireMessage.from must be a string`);
|
|
70
|
+
}
|
|
71
|
+
const msg = { type, from };
|
|
72
|
+
if (typeof obj['to'] === 'string')
|
|
73
|
+
msg.to = obj['to'];
|
|
74
|
+
if (obj['payload'] !== undefined) {
|
|
75
|
+
msg.payload = plainToRecord(obj['payload']);
|
|
76
|
+
}
|
|
77
|
+
return msg;
|
|
78
|
+
}
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Internal helpers
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
/** Convert a CRDTRecord (null-prototype) to a plain JSON-safe object. */
|
|
83
|
+
function recordToPlain(record) {
|
|
84
|
+
const out = {};
|
|
85
|
+
for (const [key, entry] of Object.entries(record)) {
|
|
86
|
+
out[key] = fieldEntryToPlain(entry);
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
function fieldEntryToPlain(entry) {
|
|
91
|
+
const out = {
|
|
92
|
+
value: entry.value,
|
|
93
|
+
hlc: hlcToJSON(entry.hlc),
|
|
94
|
+
origin: entry.origin,
|
|
95
|
+
};
|
|
96
|
+
if (entry.deleted === true)
|
|
97
|
+
out['deleted'] = true;
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
/** Reconstruct a CRDTRecord from a plain decoded object. */
|
|
101
|
+
function plainToRecord(raw) {
|
|
102
|
+
assertObject(raw, 'CRDTRecord');
|
|
103
|
+
const obj = raw;
|
|
104
|
+
// Use null-prototype object to match core's convention
|
|
105
|
+
const record = Object.create(null);
|
|
106
|
+
for (const [key, rawEntry] of Object.entries(obj)) {
|
|
107
|
+
record[key] = plainToFieldEntry(rawEntry, key);
|
|
108
|
+
}
|
|
109
|
+
return record;
|
|
110
|
+
}
|
|
111
|
+
function plainToFieldEntry(raw, key) {
|
|
112
|
+
assertObject(raw, `FieldEntry[${key}]`);
|
|
113
|
+
const obj = raw;
|
|
114
|
+
const value = obj['value'] ?? null;
|
|
115
|
+
if (value !== null &&
|
|
116
|
+
typeof value !== 'string' &&
|
|
117
|
+
typeof value !== 'number' &&
|
|
118
|
+
typeof value !== 'boolean') {
|
|
119
|
+
throw new Error(`FieldEntry[${key}].value has invalid type: ${typeof value}`);
|
|
120
|
+
}
|
|
121
|
+
const hlc = hlcFromJSON(obj['hlc']);
|
|
122
|
+
const origin = obj['origin'];
|
|
123
|
+
if (typeof origin !== 'string') {
|
|
124
|
+
throw new Error(`FieldEntry[${key}].origin must be a string`);
|
|
125
|
+
}
|
|
126
|
+
const entry = { value, hlc, origin };
|
|
127
|
+
if (obj['deleted'] === true) {
|
|
128
|
+
return { ...entry, deleted: true };
|
|
129
|
+
}
|
|
130
|
+
return entry;
|
|
131
|
+
}
|
|
132
|
+
function assertObject(val, label) {
|
|
133
|
+
if (typeof val !== 'object' || val === null || Array.isArray(val)) {
|
|
134
|
+
throw new Error(`Expected object for ${label}, got: ${JSON.stringify(val)}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=serialization.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serialization.js","sourceRoot":"","sources":["../src/serialization.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAGlD,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AAItE,8EAA8E;AAC9E,2BAA2B;AAC3B,8EAA8E;AAE9E;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,MAAkB;IAChD,MAAM,KAAK,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC;IACpC,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAiB;IACjD,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAC5B,OAAO,aAAa,CAAC,KAAK,CAAC,CAAC;AAC9B,CAAC;AAED,8EAA8E;AAC9E,4DAA4D;AAC5D,8EAA8E;AAE9E;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAgB;IAC/C,MAAM,KAAK,GAA4B;QACrC,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,IAAI,EAAE,GAAG,CAAC,IAAI;KACf,CAAC;IACF,IAAI,GAAG,CAAC,EAAE,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,EAAE,CAAC;IAC/C,IAAI,GAAG,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;QAC9B,wEAAwE;QACxE,KAAK,CAAC,SAAS,CAAC,GAAG,aAAa,CAAC,GAAG,CAAC,OAAqB,CAAC,CAAC;IAC9D,CAAC;IACD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAiB;IAClD,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAC1B,YAAY,CAAC,GAAG,EAAE,aAAa,CAAC,CAAC;IAEjC,MAAM,GAAG,GAAG,GAA8B,CAAC;IAC3C,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;IACzB,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC;IAEzB,IAAI,IAAI,KAAK,MAAM,IAAI,IAAI,KAAK,MAAM,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;QAC1D,MAAM,IAAI,KAAK,CAAC,6BAA6B,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAC/D,CAAC;IACD,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,MAAM,IAAI,KAAK,CAAC,mCAAmC,CAAC,CAAC;IACvD,CAAC;IAED,MAAM,GAAG,GAAgB,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAExC,IAAI,OAAO,GAAG,CAAC,IAAI,CAAC,KAAK,QAAQ;QAAE,GAAG,CAAC,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC;IACtD,IAAI,GAAG,CAAC,SAAS,CAAC,KAAK,SAAS,EAAE,CAAC;QACjC,GAAG,CAAC,OAAO,GAAG,aAAa,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC;IAC9C,CAAC;IAED,OAAO,GAAG,CAAC;AACb,CAAC;AAED,8EAA8E;AAC9E,mBAAmB;AACnB,8EAA8E;AAE9E,yEAAyE;AACzE,SAAS,aAAa,CAAC,MAAkB;IACvC,MAAM,GAAG,GAA4B,EAAE,CAAC;IACxC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAClD,GAAG,CAAC,GAAG,CAAC,GAAG,iBAAiB,CAAC,KAAK,CAAC,CAAC;IACtC,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,SAAS,iBAAiB,CAAC,KAAiB;IAC1C,MAAM,GAAG,GAA4B;QACnC,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,GAAG,EAAE,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC;QACzB,MAAM,EAAE,KAAK,CAAC,MAAM;KACrB,CAAC;IACF,IAAI,KAAK,CAAC,OAAO,KAAK,IAAI;QAAE,GAAG,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC;IAClD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,4DAA4D;AAC5D,SAAS,aAAa,CAAC,GAAY;IACjC,YAAY,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;IAChC,MAAM,GAAG,GAAG,GAA8B,CAAC;IAC3C,uDAAuD;IACvD,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAe,CAAC;IAEjD,KAAK,MAAM,CAAC,GAAG,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QAClD,MAAM,CAAC,GAAG,CAAC,GAAG,iBAAiB,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IACjD,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,iBAAiB,CAAC,GAAY,EAAE,GAAW;IAClD,YAAY,CAAC,GAAG,EAAE,cAAc,GAAG,GAAG,CAAC,CAAC;IACxC,MAAM,GAAG,GAAG,GAA8B,CAAC;IAE3C,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,IAAI,CAAC;IACnC,IACE,KAAK,KAAK,IAAI;QACd,OAAO,KAAK,KAAK,QAAQ;QACzB,OAAO,KAAK,KAAK,QAAQ;QACzB,OAAO,KAAK,KAAK,SAAS,EAC1B,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,cAAc,GAAG,6BAA6B,OAAO,KAAK,EAAE,CAAC,CAAC;IAChF,CAAC;IAED,MAAM,GAAG,GAAQ,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;IACzC,MAAM,MAAM,GAAG,GAAG,CAAC,QAAQ,CAAC,CAAC;IAC7B,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CAAC,cAAc,GAAG,2BAA2B,CAAC,CAAC;IAChE,CAAC;IAED,MAAM,KAAK,GAAe,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC;IACjD,IAAI,GAAG,CAAC,SAAS,CAAC,KAAK,IAAI,EAAE,CAAC;QAC5B,OAAO,EAAE,GAAG,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IACrC,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,YAAY,CAAC,GAAY,EAAE,KAAa;IAC/C,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QAClE,MAAM,IAAI,KAAK,CAAC,uBAAuB,KAAK,UAAU,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC/E,CAAC;AACH,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport abstraction for the CRDT sync engine.
|
|
3
|
+
*
|
|
4
|
+
* The SyncTransport interface is the *only* contract the core engine needs.
|
|
5
|
+
* Any transport implementation (WebSocket, WebRTC, Bluetooth, SMS) plugs in
|
|
6
|
+
* by implementing this interface — core never changes.
|
|
7
|
+
*/
|
|
8
|
+
/** Unique identifier for a peer/device on the network. */
|
|
9
|
+
export type PeerId = string;
|
|
10
|
+
/**
|
|
11
|
+
* The pluggable transport interface.
|
|
12
|
+
*
|
|
13
|
+
* Payloads are raw bytes (Uint8Array) — the transport is intentionally
|
|
14
|
+
* unaware of the CRDT serialization format. Serialization lives in
|
|
15
|
+
* serialization.ts, transport just moves bytes.
|
|
16
|
+
*/
|
|
17
|
+
export interface SyncTransport {
|
|
18
|
+
/**
|
|
19
|
+
* Connect to the relay server (or directly to a peer).
|
|
20
|
+
* @param peerId - This device's own identifier on the network.
|
|
21
|
+
*/
|
|
22
|
+
connect(peerId: PeerId): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Send a binary payload to a specific peer.
|
|
25
|
+
* If the transport is disconnected, implementations should queue the
|
|
26
|
+
* message and deliver it on reconnect.
|
|
27
|
+
*/
|
|
28
|
+
send(toPeerId: PeerId, payload: Uint8Array): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Register a callback invoked whenever a message arrives from any peer.
|
|
31
|
+
*/
|
|
32
|
+
onReceive(callback: (fromPeerId: PeerId, payload: Uint8Array) => void): void;
|
|
33
|
+
/**
|
|
34
|
+
* Gracefully disconnect from the network.
|
|
35
|
+
*/
|
|
36
|
+
disconnect(): Promise<void>;
|
|
37
|
+
/**
|
|
38
|
+
* Returns true if the transport is currently connected.
|
|
39
|
+
*/
|
|
40
|
+
readonly isConnected: boolean;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Wire message format exchanged between peers via the relay server.
|
|
44
|
+
* Serialized with MessagePack before going onto the wire.
|
|
45
|
+
*/
|
|
46
|
+
export interface WireMessage {
|
|
47
|
+
/** Message type discriminant. */
|
|
48
|
+
type: 'sync' | 'ping' | 'pong';
|
|
49
|
+
/** Sender's device/peer ID. */
|
|
50
|
+
from: PeerId;
|
|
51
|
+
/** Recipient's device/peer ID (undefined = broadcast). */
|
|
52
|
+
to?: PeerId;
|
|
53
|
+
/** CRDT record payload (only present on 'sync' messages). */
|
|
54
|
+
payload?: unknown;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Relay server → client envelope.
|
|
58
|
+
* The relay server wraps incoming peer messages with the sender's ID.
|
|
59
|
+
*/
|
|
60
|
+
export interface RelayEnvelope {
|
|
61
|
+
from: PeerId;
|
|
62
|
+
payload: Uint8Array;
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,0DAA0D;AAC1D,MAAM,MAAM,MAAM,GAAG,MAAM,CAAC;AAE5B;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEvC;;;;OAIG;IACH,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE3D;;OAEG;IACH,SAAS,CAAC,QAAQ,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,KAAK,IAAI,GAAG,IAAI,CAAC;IAE7E;;OAEG;IACH,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAE5B;;OAEG;IACH,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;CAC/B;AAED;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC1B,iCAAiC;IACjC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;IAC/B,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,0DAA0D;IAC1D,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,6DAA6D;IAC7D,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,UAAU,CAAC;CACrB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport abstraction for the CRDT sync engine.
|
|
3
|
+
*
|
|
4
|
+
* The SyncTransport interface is the *only* contract the core engine needs.
|
|
5
|
+
* Any transport implementation (WebSocket, WebRTC, Bluetooth, SMS) plugs in
|
|
6
|
+
* by implementing this interface — core never changes.
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket client transport — implements SyncTransport.
|
|
3
|
+
*
|
|
4
|
+
* Connects to the relay server, registers with a peerId, and
|
|
5
|
+
* handles message routing to/from other peers.
|
|
6
|
+
*
|
|
7
|
+
* Works in both browser (native WebSocket via globalThis.WebSocket) and
|
|
8
|
+
* Node.js (globalThis.WebSocket is available from Node 22+, or via the `ws`
|
|
9
|
+
* polyfill injected at startup).
|
|
10
|
+
*
|
|
11
|
+
* Resilience features:
|
|
12
|
+
* - Exponential backoff reconnection (1s → 2s → 4s → ... → 30s max)
|
|
13
|
+
* - Outbound message queue: messages sent while offline are buffered
|
|
14
|
+
* and flushed on reconnect — this is what makes the transport
|
|
15
|
+
* offline-first rather than just a thin WebSocket wrapper.
|
|
16
|
+
*/
|
|
17
|
+
import type { PeerId, SyncTransport } from './types.js';
|
|
18
|
+
export interface WebSocketTransportOptions {
|
|
19
|
+
/** WebSocket relay server URL, e.g. "ws://localhost:8787" */
|
|
20
|
+
serverUrl: string;
|
|
21
|
+
/** Initial reconnect delay in ms. Doubles each attempt up to maxReconnectDelayMs. */
|
|
22
|
+
initialReconnectDelayMs?: number;
|
|
23
|
+
/** Maximum reconnect delay (default 30 000 ms). */
|
|
24
|
+
maxReconnectDelayMs?: number;
|
|
25
|
+
}
|
|
26
|
+
export declare class WebSocketTransport implements SyncTransport {
|
|
27
|
+
private readonly serverUrl;
|
|
28
|
+
private readonly initialDelay;
|
|
29
|
+
private readonly maxDelay;
|
|
30
|
+
private ws;
|
|
31
|
+
private peerId;
|
|
32
|
+
private _isConnected;
|
|
33
|
+
private destroyed;
|
|
34
|
+
private receiveCallback;
|
|
35
|
+
private outboundQueue;
|
|
36
|
+
private reconnectDelay;
|
|
37
|
+
private reconnectTimer;
|
|
38
|
+
constructor(options: WebSocketTransportOptions);
|
|
39
|
+
get isConnected(): boolean;
|
|
40
|
+
connect(peerId: PeerId): Promise<void>;
|
|
41
|
+
send(toPeerId: PeerId, payload: Uint8Array): Promise<void>;
|
|
42
|
+
onReceive(callback: (fromPeerId: PeerId, payload: Uint8Array) => void): void;
|
|
43
|
+
disconnect(): Promise<void>;
|
|
44
|
+
private openSocket;
|
|
45
|
+
private scheduleReconnect;
|
|
46
|
+
private sendRaw;
|
|
47
|
+
private flushQueue;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=ws-transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ws-transport.d.ts","sourceRoot":"","sources":["../src/ws-transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,OAAO,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAExD,MAAM,WAAW,yBAAyB;IACxC,6DAA6D;IAC7D,SAAS,EAAE,MAAM,CAAC;IAClB,qFAAqF;IACrF,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,mDAAmD;IACnD,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAOD,qBAAa,kBAAmB,YAAW,aAAa;IACtD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAElC,OAAO,CAAC,EAAE,CAAqC;IAC/C,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,SAAS,CAAS;IAE1B,OAAO,CAAC,eAAe,CAAoE;IAC3F,OAAO,CAAC,aAAa,CAAuB;IAC5C,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,cAAc,CAA8C;gBAExD,OAAO,EAAE,yBAAyB;IAO9C,IAAI,WAAW,IAAI,OAAO,CAEzB;IAIK,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAOtC,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IAShE,SAAS,CAAC,QAAQ,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,KAAK,IAAI,GAAG,IAAI;IAItE,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAejC,OAAO,CAAC,UAAU;IAoGlB,OAAO,CAAC,iBAAiB;IAczB,OAAO,CAAC,OAAO;IAKf,OAAO,CAAC,UAAU;CASnB"}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket client transport — implements SyncTransport.
|
|
3
|
+
*
|
|
4
|
+
* Connects to the relay server, registers with a peerId, and
|
|
5
|
+
* handles message routing to/from other peers.
|
|
6
|
+
*
|
|
7
|
+
* Works in both browser (native WebSocket via globalThis.WebSocket) and
|
|
8
|
+
* Node.js (globalThis.WebSocket is available from Node 22+, or via the `ws`
|
|
9
|
+
* polyfill injected at startup).
|
|
10
|
+
*
|
|
11
|
+
* Resilience features:
|
|
12
|
+
* - Exponential backoff reconnection (1s → 2s → 4s → ... → 30s max)
|
|
13
|
+
* - Outbound message queue: messages sent while offline are buffered
|
|
14
|
+
* and flushed on reconnect — this is what makes the transport
|
|
15
|
+
* offline-first rather than just a thin WebSocket wrapper.
|
|
16
|
+
*/
|
|
17
|
+
import { decode, encode } from '@msgpack/msgpack';
|
|
18
|
+
export class WebSocketTransport {
|
|
19
|
+
serverUrl;
|
|
20
|
+
initialDelay;
|
|
21
|
+
maxDelay;
|
|
22
|
+
ws = null;
|
|
23
|
+
peerId = null;
|
|
24
|
+
_isConnected = false;
|
|
25
|
+
destroyed = false;
|
|
26
|
+
receiveCallback = null;
|
|
27
|
+
outboundQueue = [];
|
|
28
|
+
reconnectDelay;
|
|
29
|
+
reconnectTimer = null;
|
|
30
|
+
constructor(options) {
|
|
31
|
+
this.serverUrl = options.serverUrl;
|
|
32
|
+
this.initialDelay = options.initialReconnectDelayMs ?? 1_000;
|
|
33
|
+
this.maxDelay = options.maxReconnectDelayMs ?? 30_000;
|
|
34
|
+
this.reconnectDelay = this.initialDelay;
|
|
35
|
+
}
|
|
36
|
+
get isConnected() {
|
|
37
|
+
return this._isConnected;
|
|
38
|
+
}
|
|
39
|
+
// ── SyncTransport API ────────────────────────────────────────────────────
|
|
40
|
+
async connect(peerId) {
|
|
41
|
+
if (this._isConnected)
|
|
42
|
+
return;
|
|
43
|
+
this.peerId = peerId;
|
|
44
|
+
this.destroyed = false;
|
|
45
|
+
await this.openSocket();
|
|
46
|
+
}
|
|
47
|
+
async send(toPeerId, payload) {
|
|
48
|
+
if (!this._isConnected || this.ws === null) {
|
|
49
|
+
// Buffer for delivery on reconnect
|
|
50
|
+
this.outboundQueue.push({ to: toPeerId, payload });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
this.sendRaw(toPeerId, payload);
|
|
54
|
+
}
|
|
55
|
+
onReceive(callback) {
|
|
56
|
+
this.receiveCallback = callback;
|
|
57
|
+
}
|
|
58
|
+
async disconnect() {
|
|
59
|
+
this.destroyed = true;
|
|
60
|
+
if (this.reconnectTimer !== null) {
|
|
61
|
+
clearTimeout(this.reconnectTimer);
|
|
62
|
+
this.reconnectTimer = null;
|
|
63
|
+
}
|
|
64
|
+
if (this.ws !== null) {
|
|
65
|
+
this.ws.close();
|
|
66
|
+
this.ws = null;
|
|
67
|
+
}
|
|
68
|
+
this._isConnected = false;
|
|
69
|
+
}
|
|
70
|
+
// ── Internal ─────────────────────────────────────────────────────────────
|
|
71
|
+
openSocket() {
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
// Use the native browser WebSocket (globalThis.WebSocket).
|
|
74
|
+
// This works in Vite/React without any Node.js polyfill.
|
|
75
|
+
// In Node.js 22+ WebSocket is also available on globalThis.
|
|
76
|
+
const socket = new globalThis.WebSocket(this.serverUrl);
|
|
77
|
+
socket.binaryType = 'arraybuffer';
|
|
78
|
+
this.ws = socket;
|
|
79
|
+
socket.addEventListener('open', () => {
|
|
80
|
+
// Register with the relay server
|
|
81
|
+
socket.send(encode({ type: 'register', peerId: this.peerId }));
|
|
82
|
+
});
|
|
83
|
+
socket.addEventListener('message', (event) => {
|
|
84
|
+
let raw;
|
|
85
|
+
if (event.data instanceof ArrayBuffer) {
|
|
86
|
+
raw = new Uint8Array(event.data);
|
|
87
|
+
}
|
|
88
|
+
else if (event.data instanceof Uint8Array) {
|
|
89
|
+
raw = event.data;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
console.warn('[ws-transport] unexpected message data type:', typeof event.data);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
let msg;
|
|
96
|
+
try {
|
|
97
|
+
const decoded = decode(raw);
|
|
98
|
+
if (typeof decoded !== 'object' || decoded === null)
|
|
99
|
+
return;
|
|
100
|
+
msg = decoded;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
console.warn('[ws-transport] failed to decode message');
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const type = msg['type'];
|
|
107
|
+
if (type === 'registered') {
|
|
108
|
+
// Server confirmed registration
|
|
109
|
+
this._isConnected = true;
|
|
110
|
+
this.reconnectDelay = this.initialDelay; // reset backoff on success
|
|
111
|
+
this.flushQueue();
|
|
112
|
+
resolve();
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (type === 'message') {
|
|
116
|
+
const from = msg['from'];
|
|
117
|
+
const payload = msg['payload'];
|
|
118
|
+
if (typeof from === 'string' &&
|
|
119
|
+
(payload instanceof Uint8Array || payload instanceof ArrayBuffer) &&
|
|
120
|
+
this.receiveCallback !== null) {
|
|
121
|
+
const bytes = payload instanceof ArrayBuffer ? new Uint8Array(payload) : payload;
|
|
122
|
+
this.receiveCallback(from, bytes);
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (type === 'peer_joined') {
|
|
127
|
+
console.log(`[ws-transport] peer joined: ${String(msg['peerId'])}`);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (type === 'peer_left') {
|
|
131
|
+
console.log(`[ws-transport] peer left: ${String(msg['peerId'])}`);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (type === 'error') {
|
|
135
|
+
console.error(`[ws-transport] server error: ${String(msg['message'])}`);
|
|
136
|
+
// If we weren't registered yet, reject the connect() promise
|
|
137
|
+
if (!this._isConnected) {
|
|
138
|
+
reject(new Error(String(msg['message'])));
|
|
139
|
+
}
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
socket.addEventListener('close', () => {
|
|
144
|
+
this._isConnected = false;
|
|
145
|
+
this.ws = null;
|
|
146
|
+
if (!this.destroyed) {
|
|
147
|
+
console.log(`[ws-transport] disconnected. Reconnecting in ${this.reconnectDelay}ms...`);
|
|
148
|
+
this.scheduleReconnect();
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
socket.addEventListener('error', () => {
|
|
152
|
+
console.error('[ws-transport] WebSocket error');
|
|
153
|
+
if (!this._isConnected) {
|
|
154
|
+
reject(new Error('WebSocket connection failed'));
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
scheduleReconnect() {
|
|
160
|
+
if (this.destroyed || this.peerId === null)
|
|
161
|
+
return;
|
|
162
|
+
this.reconnectTimer = setTimeout(() => {
|
|
163
|
+
this.reconnectTimer = null;
|
|
164
|
+
// Exponential backoff with jitter (±10%)
|
|
165
|
+
const jitter = this.reconnectDelay * 0.1 * (Math.random() * 2 - 1);
|
|
166
|
+
this.reconnectDelay = Math.min(this.reconnectDelay * 2 + jitter, this.maxDelay);
|
|
167
|
+
console.log(`[ws-transport] attempting reconnect as "${this.peerId}"...`);
|
|
168
|
+
this.openSocket().catch((err) => {
|
|
169
|
+
console.error('[ws-transport] reconnect failed:', err);
|
|
170
|
+
});
|
|
171
|
+
}, this.reconnectDelay);
|
|
172
|
+
}
|
|
173
|
+
sendRaw(toPeerId, payload) {
|
|
174
|
+
if (this.ws === null || this.ws.readyState !== globalThis.WebSocket.OPEN)
|
|
175
|
+
return;
|
|
176
|
+
this.ws.send(encode({ type: 'send', to: toPeerId, payload }));
|
|
177
|
+
}
|
|
178
|
+
flushQueue() {
|
|
179
|
+
const queued = this.outboundQueue.splice(0);
|
|
180
|
+
for (const { to, payload } of queued) {
|
|
181
|
+
this.sendRaw(to, payload);
|
|
182
|
+
}
|
|
183
|
+
if (queued.length > 0) {
|
|
184
|
+
console.log(`[ws-transport] flushed ${queued.length} queued message(s)`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=ws-transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ws-transport.js","sourceRoot":"","sources":["../src/ws-transport.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shet-anirudh/crdt-sync-transport-ws",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "WebSocket transport adapter and relay server for @shet-anirudh/crdt-sync-core.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"crdt",
|
|
7
|
+
"websocket",
|
|
8
|
+
"transport",
|
|
9
|
+
"sync",
|
|
10
|
+
"offline-first"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"author": "Anirudh Shet",
|
|
14
|
+
"homepage": "https://github.com/shet-anirudh/crdt-sync-engine#readme",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/shet-anirudh/crdt-sync-engine.git",
|
|
18
|
+
"directory": "packages/transport-ws"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/shet-anirudh/crdt-sync-engine/issues"
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"type": "module",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"import": "./dist/index.js",
|
|
30
|
+
"types": "./dist/index.d.ts"
|
|
31
|
+
},
|
|
32
|
+
"./server": {
|
|
33
|
+
"import": "./dist/relay-server.js",
|
|
34
|
+
"types": "./dist/relay-server.d.ts"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"main": "./dist/index.js",
|
|
38
|
+
"types": "./dist/index.d.ts",
|
|
39
|
+
"files": [
|
|
40
|
+
"dist"
|
|
41
|
+
],
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@msgpack/msgpack": "^3.0.0",
|
|
44
|
+
"ws": "^8.18.0",
|
|
45
|
+
"@shet-anirudh/crdt-sync-core": "0.1.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/ws": "^8.5.12",
|
|
49
|
+
"rimraf": "^6.0.1",
|
|
50
|
+
"vitest": "^2.0.5"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsc --project tsconfig.json",
|
|
54
|
+
"build:watch": "tsc --project tsconfig.json --watch",
|
|
55
|
+
"test": "vitest run",
|
|
56
|
+
"test:watch": "vitest",
|
|
57
|
+
"lint": "eslint src --ext .ts",
|
|
58
|
+
"clean": "rimraf dist",
|
|
59
|
+
"start:relay": "node dist/relay-server.js"
|
|
60
|
+
}
|
|
61
|
+
}
|