meridian-sdk 0.2.1 → 0.2.3
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/biome.json +4 -0
- package/dist/auth/token.d.ts +0 -19
- package/dist/auth/token.d.ts.map +1 -1
- package/dist/auth/token.js +6 -31
- package/dist/auth/token.js.map +1 -1
- package/dist/client.d.ts +139 -23
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +165 -52
- package/dist/client.js.map +1 -1
- package/dist/codec.d.ts +7 -35
- package/dist/codec.d.ts.map +1 -1
- package/dist/codec.js +13 -65
- package/dist/codec.js.map +1 -1
- package/dist/constants.d.ts +7 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +7 -0
- package/dist/constants.js.map +1 -0
- package/dist/crdt/gcounter.d.ts +18 -9
- package/dist/crdt/gcounter.d.ts.map +1 -1
- package/dist/crdt/gcounter.js +16 -13
- package/dist/crdt/gcounter.js.map +1 -1
- package/dist/crdt/lwwregister.d.ts +24 -11
- package/dist/crdt/lwwregister.d.ts.map +1 -1
- package/dist/crdt/lwwregister.js +25 -19
- package/dist/crdt/lwwregister.js.map +1 -1
- package/dist/crdt/orset.d.ts +25 -13
- package/dist/crdt/orset.d.ts.map +1 -1
- package/dist/crdt/orset.js +31 -23
- package/dist/crdt/orset.js.map +1 -1
- package/dist/crdt/pncounter.d.ts +22 -4
- package/dist/crdt/pncounter.d.ts.map +1 -1
- package/dist/crdt/pncounter.js +28 -14
- package/dist/crdt/pncounter.js.map +1 -1
- package/dist/crdt/presence.d.ts +33 -13
- package/dist/crdt/presence.d.ts.map +1 -1
- package/dist/crdt/presence.js +36 -20
- package/dist/crdt/presence.js.map +1 -1
- package/dist/errors.d.ts +0 -4
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +0 -16
- package/dist/errors.js.map +1 -1
- package/dist/schema.d.ts +3 -9
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +3 -34
- package/dist/schema.js.map +1 -1
- package/dist/sync/clock.d.ts +1 -20
- package/dist/sync/clock.d.ts.map +1 -1
- package/dist/sync/clock.js +20 -46
- package/dist/sync/clock.js.map +1 -1
- package/dist/sync/delta.d.ts +5 -22
- package/dist/sync/delta.d.ts.map +1 -1
- package/dist/sync/delta.js +18 -26
- package/dist/sync/delta.js.map +1 -1
- package/dist/transport/http.d.ts +1 -14
- package/dist/transport/http.d.ts.map +1 -1
- package/dist/transport/http.js +3 -21
- package/dist/transport/http.js.map +1 -1
- package/dist/transport/websocket.d.ts +0 -27
- package/dist/transport/websocket.d.ts.map +1 -1
- package/dist/transport/websocket.js +9 -37
- package/dist/transport/websocket.js.map +1 -1
- package/dist/utils/to-hex.d.ts +2 -0
- package/dist/utils/to-hex.d.ts.map +1 -0
- package/dist/utils/to-hex.js +2 -0
- package/dist/utils/to-hex.js.map +1 -0
- package/package.json +6 -3
- package/src/auth/token.ts +6 -34
- package/src/client.ts +165 -65
- package/src/codec.ts +13 -71
- package/src/constants.ts +6 -0
- package/src/crdt/gcounter.ts +18 -20
- package/src/crdt/lwwregister.ts +27 -26
- package/src/crdt/orset.ts +32 -29
- package/src/crdt/pncounter.ts +30 -21
- package/src/crdt/presence.ts +37 -26
- package/src/errors.ts +0 -21
- package/src/schema.ts +3 -44
- package/src/sync/clock.ts +18 -50
- package/src/sync/delta.ts +20 -58
- package/src/transport/http.ts +3 -33
- package/src/transport/websocket.ts +15 -52
- package/src/utils/to-hex.ts +1 -0
- package/test/integration.test.ts +2 -3
- package/test/sync.test.ts +1 -2
|
@@ -1,22 +1,13 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* WebSocket transport — reconnect FSM + Sync on reconnect.
|
|
3
|
-
*
|
|
4
|
-
* States:
|
|
5
|
-
* DISCONNECTED → CONNECTING → AUTHENTICATING → CONNECTED → CLOSING
|
|
6
|
-
*
|
|
7
|
-
* On reconnect: re-subscribes to all known CRDTs and sends Sync(localVectorClock)
|
|
8
|
-
* so the server can push missed deltas.
|
|
9
|
-
*
|
|
10
|
-
* Backoff: 100ms → 200ms → 400ms → … → 30s (±20% jitter).
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
1
|
import { Effect } from "effect";
|
|
14
2
|
import { encodeClientMsg, decodeServerMsg, encodeVectorClock } from "../codec.js";
|
|
15
3
|
import type { ServerMsg, VectorClock } from "../schema.js";
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
4
|
+
import {
|
|
5
|
+
BACKOFF_INITIAL_MS,
|
|
6
|
+
BACKOFF_MAX_MS,
|
|
7
|
+
BACKOFF_MULTIPLIER,
|
|
8
|
+
JITTER_MULTIPLIER,
|
|
9
|
+
DEFAULT_TIMEOUT_MS,
|
|
10
|
+
} from "../constants.js";
|
|
20
11
|
|
|
21
12
|
export type WsState =
|
|
22
13
|
| "DISCONNECTED"
|
|
@@ -25,49 +16,36 @@ export type WsState =
|
|
|
25
16
|
| "CLOSING";
|
|
26
17
|
|
|
27
18
|
export interface WsTransportConfig {
|
|
28
|
-
/** Full WebSocket URL, e.g. "ws://localhost:3000/v1/namespaces/my-room/connect" */
|
|
29
19
|
url: string;
|
|
30
|
-
/** Bearer token passed as ?token= query param (WS can't set headers). */
|
|
31
20
|
token: string;
|
|
32
|
-
/** Called whenever a ServerMsg arrives. */
|
|
33
21
|
onMessage: (msg: ServerMsg) => void;
|
|
34
|
-
/** Called on state transitions. */
|
|
35
22
|
onStateChange?: (state: WsState) => void;
|
|
36
|
-
/** Maximum reconnect delay in ms. Default: 30_000 */
|
|
37
23
|
maxBackoffMs?: number;
|
|
38
24
|
}
|
|
39
25
|
|
|
40
|
-
// ---------------------------------------------------------------------------
|
|
41
|
-
// WsTransport
|
|
42
|
-
// ---------------------------------------------------------------------------
|
|
43
|
-
|
|
44
26
|
export class WsTransport {
|
|
45
27
|
private readonly config: WsTransportConfig;
|
|
46
28
|
private readonly maxBackoffMs: number;
|
|
47
29
|
|
|
48
30
|
private ws: WebSocket | null = null;
|
|
49
31
|
private state: WsState = "DISCONNECTED";
|
|
50
|
-
private backoffMs =
|
|
32
|
+
private backoffMs = BACKOFF_INITIAL_MS;
|
|
51
33
|
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
52
34
|
private closed = false;
|
|
53
35
|
|
|
54
|
-
/** CRDTs to re-subscribe on reconnect: crdt_id → last known VectorClock */
|
|
55
36
|
private readonly subscriptions = new Map<string, VectorClock>();
|
|
56
37
|
|
|
57
38
|
constructor(config: WsTransportConfig) {
|
|
58
39
|
this.config = config;
|
|
59
|
-
this.maxBackoffMs = config.maxBackoffMs ??
|
|
40
|
+
this.maxBackoffMs = config.maxBackoffMs ?? BACKOFF_MAX_MS;
|
|
60
41
|
}
|
|
61
42
|
|
|
62
|
-
// ---- Public API ----
|
|
63
|
-
|
|
64
43
|
connect(): void {
|
|
65
44
|
if (this.closed) return;
|
|
66
45
|
this.closed = false;
|
|
67
46
|
this.doConnect();
|
|
68
47
|
}
|
|
69
48
|
|
|
70
|
-
/** Gracefully close — will not reconnect. */
|
|
71
49
|
close(): void {
|
|
72
50
|
this.closed = true;
|
|
73
51
|
this.clearReconnectTimer();
|
|
@@ -75,11 +53,6 @@ export class WsTransport {
|
|
|
75
53
|
this.ws?.close(1000, "client close");
|
|
76
54
|
}
|
|
77
55
|
|
|
78
|
-
/**
|
|
79
|
-
* Subscribe to a CRDT's deltas.
|
|
80
|
-
* If already connected, sends Subscribe immediately.
|
|
81
|
-
* On reconnect, the subscription is re-sent automatically.
|
|
82
|
-
*/
|
|
83
56
|
subscribe(crdtId: string, sinceVc: VectorClock = {}): void {
|
|
84
57
|
this.subscriptions.set(crdtId, sinceVc);
|
|
85
58
|
if (this.state === "CONNECTED") {
|
|
@@ -87,12 +60,10 @@ export class WsTransport {
|
|
|
87
60
|
}
|
|
88
61
|
}
|
|
89
62
|
|
|
90
|
-
/** Update the local vector clock for a CRDT (used for reconnect Sync). */
|
|
91
63
|
updateClock(crdtId: string, vc: VectorClock): void {
|
|
92
64
|
this.subscriptions.set(crdtId, vc);
|
|
93
65
|
}
|
|
94
66
|
|
|
95
|
-
/** Send a raw ClientMsg. Throws if not connected. */
|
|
96
67
|
send(msg: Parameters<typeof encodeClientMsg>[0]): void {
|
|
97
68
|
if (this.state !== "CONNECTED" || this.ws === null) {
|
|
98
69
|
throw new Error("WsTransport: not connected");
|
|
@@ -104,8 +75,7 @@ export class WsTransport {
|
|
|
104
75
|
return this.state;
|
|
105
76
|
}
|
|
106
77
|
|
|
107
|
-
|
|
108
|
-
waitForConnected(timeoutMs = 5_000): Promise<void> {
|
|
78
|
+
waitForConnected(timeoutMs = DEFAULT_TIMEOUT_MS): Promise<void> {
|
|
109
79
|
if (this.state === "CONNECTED") return Promise.resolve();
|
|
110
80
|
return new Promise((resolve, reject) => {
|
|
111
81
|
const orig = this.config.onStateChange;
|
|
@@ -131,8 +101,6 @@ export class WsTransport {
|
|
|
131
101
|
});
|
|
132
102
|
}
|
|
133
103
|
|
|
134
|
-
// ---- FSM internals ----
|
|
135
|
-
|
|
136
104
|
private doConnect(): void {
|
|
137
105
|
if (this.closed) return;
|
|
138
106
|
this.transitionTo("CONNECTING");
|
|
@@ -143,8 +111,8 @@ export class WsTransport {
|
|
|
143
111
|
this.ws = ws;
|
|
144
112
|
|
|
145
113
|
ws.addEventListener("open", () => {
|
|
146
|
-
if (ws !== this.ws) return;
|
|
147
|
-
this.backoffMs =
|
|
114
|
+
if (ws !== this.ws) return;
|
|
115
|
+
this.backoffMs = BACKOFF_INITIAL_MS;
|
|
148
116
|
this.transitionTo("CONNECTED");
|
|
149
117
|
this.resubscribeAll();
|
|
150
118
|
});
|
|
@@ -168,19 +136,19 @@ export class WsTransport {
|
|
|
168
136
|
});
|
|
169
137
|
|
|
170
138
|
ws.addEventListener("error", () => {
|
|
171
|
-
// The "close" event fires right after — let that
|
|
139
|
+
// HACK: The "close" event fires right after an error — let that handler drive reconnect logic.
|
|
172
140
|
});
|
|
173
141
|
}
|
|
174
142
|
|
|
175
143
|
private scheduleReconnect(): void {
|
|
176
144
|
if (this.closed) return;
|
|
177
|
-
const jitter = this.backoffMs *
|
|
145
|
+
const jitter = this.backoffMs * JITTER_MULTIPLIER * (Math.random() * 2 - 1);
|
|
178
146
|
const delay = Math.round(this.backoffMs + jitter);
|
|
179
147
|
this.reconnectTimer = setTimeout(() => {
|
|
180
148
|
this.reconnectTimer = null;
|
|
181
149
|
this.doConnect();
|
|
182
150
|
}, delay);
|
|
183
|
-
this.backoffMs = Math.min(this.backoffMs *
|
|
151
|
+
this.backoffMs = Math.min(this.backoffMs * BACKOFF_MULTIPLIER, this.maxBackoffMs);
|
|
184
152
|
}
|
|
185
153
|
|
|
186
154
|
private clearReconnectTimer(): void {
|
|
@@ -196,7 +164,6 @@ export class WsTransport {
|
|
|
196
164
|
this.config.onStateChange?.(next);
|
|
197
165
|
}
|
|
198
166
|
|
|
199
|
-
/** On reconnect: re-subscribe and send Sync for each known CRDT. */
|
|
200
167
|
private resubscribeAll(): void {
|
|
201
168
|
for (const [crdtId, vc] of this.subscriptions) {
|
|
202
169
|
this.sendSubscribe(crdtId, vc);
|
|
@@ -205,11 +172,7 @@ export class WsTransport {
|
|
|
205
172
|
|
|
206
173
|
private sendSubscribe(crdtId: string, vc: VectorClock): void {
|
|
207
174
|
if (this.ws === null || this.state !== "CONNECTED") return;
|
|
208
|
-
|
|
209
|
-
// First subscribe so the server starts pushing future deltas
|
|
210
175
|
this.ws.send(encodeClientMsg({ Subscribe: { crdt_id: crdtId } }));
|
|
211
|
-
|
|
212
|
-
// Then sync to get missed deltas since our last known VC
|
|
213
176
|
const vcBytes = encodeVectorClock(vc);
|
|
214
177
|
this.ws.send(encodeClientMsg({ Sync: { crdt_id: crdtId, since_vc: vcBytes } }));
|
|
215
178
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const toHex = (value: number): string => value.toString(16).padStart(2, "0");
|
package/test/integration.test.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { describe, test, expect, beforeAll, afterAll } from "bun:test";
|
|
|
15
15
|
import { Effect } from "effect";
|
|
16
16
|
import { MeridianClient } from "../src/client.js";
|
|
17
17
|
import { HttpClient } from "../src/transport/http.js";
|
|
18
|
-
import { uuidToBytes
|
|
18
|
+
import { uuidToBytes } from "../src/codec.js";
|
|
19
19
|
|
|
20
20
|
// ---------------------------------------------------------------------------
|
|
21
21
|
// Config — matches the dev signing key [0x42; 32]
|
|
@@ -145,8 +145,7 @@ describe("Meridian integration", () => {
|
|
|
145
145
|
|
|
146
146
|
skip("LwwRegister: set and read", async () => {
|
|
147
147
|
const key = `lw:${Date.now()}`;
|
|
148
|
-
|
|
149
|
-
const now = wallMsToBigInt(Date.now());
|
|
148
|
+
const now = Date.now();
|
|
150
149
|
|
|
151
150
|
await Effect.runPromise(http.postOp(NAMESPACE, key, {
|
|
152
151
|
LwwRegister: {
|
package/test/sync.test.ts
CHANGED
|
@@ -8,7 +8,6 @@ import { VectorClockTracker } from "../src/sync/clock.js";
|
|
|
8
8
|
import { encode, decode, encodeClientMsg, decodeServerMsg } from "../src/codec.js";
|
|
9
9
|
import { parseToken, checkTokenExpiry } from "../src/auth/token.js";
|
|
10
10
|
import { CodecError, TokenParseError, TokenExpiredError } from "../src/errors.js";
|
|
11
|
-
import { pack } from "msgpackr";
|
|
12
11
|
|
|
13
12
|
// ---------------------------------------------------------------------------
|
|
14
13
|
// VectorClockTracker
|
|
@@ -124,7 +123,7 @@ describe("codec", () => {
|
|
|
124
123
|
|
|
125
124
|
describe("parseToken", () => {
|
|
126
125
|
function makeToken(claims: object): string {
|
|
127
|
-
const payload =
|
|
126
|
+
const payload = encode(claims);
|
|
128
127
|
const b64 = btoa(String.fromCharCode(...payload))
|
|
129
128
|
.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
130
129
|
return `${b64}.fakesig`;
|