meridian-sdk 0.2.1 → 0.2.2

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 (84) hide show
  1. package/biome.json +4 -0
  2. package/dist/auth/token.d.ts +0 -19
  3. package/dist/auth/token.d.ts.map +1 -1
  4. package/dist/auth/token.js +6 -31
  5. package/dist/auth/token.js.map +1 -1
  6. package/dist/client.d.ts +139 -23
  7. package/dist/client.d.ts.map +1 -1
  8. package/dist/client.js +165 -52
  9. package/dist/client.js.map +1 -1
  10. package/dist/codec.d.ts +7 -35
  11. package/dist/codec.d.ts.map +1 -1
  12. package/dist/codec.js +13 -65
  13. package/dist/codec.js.map +1 -1
  14. package/dist/constants.d.ts +7 -0
  15. package/dist/constants.d.ts.map +1 -0
  16. package/dist/constants.js +7 -0
  17. package/dist/constants.js.map +1 -0
  18. package/dist/crdt/gcounter.d.ts +18 -9
  19. package/dist/crdt/gcounter.d.ts.map +1 -1
  20. package/dist/crdt/gcounter.js +16 -13
  21. package/dist/crdt/gcounter.js.map +1 -1
  22. package/dist/crdt/lwwregister.d.ts +24 -11
  23. package/dist/crdt/lwwregister.d.ts.map +1 -1
  24. package/dist/crdt/lwwregister.js +25 -19
  25. package/dist/crdt/lwwregister.js.map +1 -1
  26. package/dist/crdt/orset.d.ts +25 -13
  27. package/dist/crdt/orset.d.ts.map +1 -1
  28. package/dist/crdt/orset.js +31 -23
  29. package/dist/crdt/orset.js.map +1 -1
  30. package/dist/crdt/pncounter.d.ts +22 -4
  31. package/dist/crdt/pncounter.d.ts.map +1 -1
  32. package/dist/crdt/pncounter.js +28 -14
  33. package/dist/crdt/pncounter.js.map +1 -1
  34. package/dist/crdt/presence.d.ts +33 -13
  35. package/dist/crdt/presence.d.ts.map +1 -1
  36. package/dist/crdt/presence.js +36 -20
  37. package/dist/crdt/presence.js.map +1 -1
  38. package/dist/errors.d.ts +0 -4
  39. package/dist/errors.d.ts.map +1 -1
  40. package/dist/errors.js +0 -16
  41. package/dist/errors.js.map +1 -1
  42. package/dist/schema.d.ts +3 -9
  43. package/dist/schema.d.ts.map +1 -1
  44. package/dist/schema.js +3 -34
  45. package/dist/schema.js.map +1 -1
  46. package/dist/sync/clock.d.ts +1 -20
  47. package/dist/sync/clock.d.ts.map +1 -1
  48. package/dist/sync/clock.js +20 -46
  49. package/dist/sync/clock.js.map +1 -1
  50. package/dist/sync/delta.d.ts +5 -22
  51. package/dist/sync/delta.d.ts.map +1 -1
  52. package/dist/sync/delta.js +18 -26
  53. package/dist/sync/delta.js.map +1 -1
  54. package/dist/transport/http.d.ts +1 -14
  55. package/dist/transport/http.d.ts.map +1 -1
  56. package/dist/transport/http.js +3 -21
  57. package/dist/transport/http.js.map +1 -1
  58. package/dist/transport/websocket.d.ts +0 -27
  59. package/dist/transport/websocket.d.ts.map +1 -1
  60. package/dist/transport/websocket.js +9 -37
  61. package/dist/transport/websocket.js.map +1 -1
  62. package/dist/utils/to-hex.d.ts +2 -0
  63. package/dist/utils/to-hex.d.ts.map +1 -0
  64. package/dist/utils/to-hex.js +2 -0
  65. package/dist/utils/to-hex.js.map +1 -0
  66. package/package.json +6 -3
  67. package/src/auth/token.ts +6 -34
  68. package/src/client.ts +165 -65
  69. package/src/codec.ts +13 -71
  70. package/src/constants.ts +6 -0
  71. package/src/crdt/gcounter.ts +18 -20
  72. package/src/crdt/lwwregister.ts +27 -26
  73. package/src/crdt/orset.ts +32 -29
  74. package/src/crdt/pncounter.ts +30 -21
  75. package/src/crdt/presence.ts +37 -26
  76. package/src/errors.ts +0 -21
  77. package/src/schema.ts +3 -44
  78. package/src/sync/clock.ts +18 -50
  79. package/src/sync/delta.ts +20 -58
  80. package/src/transport/http.ts +3 -33
  81. package/src/transport/websocket.ts +15 -52
  82. package/src/utils/to-hex.ts +1 -0
  83. package/test/integration.test.ts +2 -3
  84. package/test/sync.test.ts +1 -2
@@ -1,63 +1,40 @@
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
1
  import { Effect } from "effect";
13
2
  import { encodeClientMsg, decodeServerMsg, encodeVectorClock } from "../codec.js";
14
- // ---------------------------------------------------------------------------
15
- // WsTransport
16
- // ---------------------------------------------------------------------------
3
+ import { BACKOFF_INITIAL_MS, BACKOFF_MAX_MS, BACKOFF_MULTIPLIER, JITTER_MULTIPLIER, DEFAULT_TIMEOUT_MS, } from "../constants.js";
17
4
  export class WsTransport {
18
5
  config;
19
6
  maxBackoffMs;
20
7
  ws = null;
21
8
  state = "DISCONNECTED";
22
- backoffMs = 100;
9
+ backoffMs = BACKOFF_INITIAL_MS;
23
10
  reconnectTimer = null;
24
11
  closed = false;
25
- /** CRDTs to re-subscribe on reconnect: crdt_id → last known VectorClock */
26
12
  subscriptions = new Map();
27
13
  constructor(config) {
28
14
  this.config = config;
29
- this.maxBackoffMs = config.maxBackoffMs ?? 30_000;
15
+ this.maxBackoffMs = config.maxBackoffMs ?? BACKOFF_MAX_MS;
30
16
  }
31
- // ---- Public API ----
32
17
  connect() {
33
18
  if (this.closed)
34
19
  return;
35
20
  this.closed = false;
36
21
  this.doConnect();
37
22
  }
38
- /** Gracefully close — will not reconnect. */
39
23
  close() {
40
24
  this.closed = true;
41
25
  this.clearReconnectTimer();
42
26
  this.transitionTo("CLOSING");
43
27
  this.ws?.close(1000, "client close");
44
28
  }
45
- /**
46
- * Subscribe to a CRDT's deltas.
47
- * If already connected, sends Subscribe immediately.
48
- * On reconnect, the subscription is re-sent automatically.
49
- */
50
29
  subscribe(crdtId, sinceVc = {}) {
51
30
  this.subscriptions.set(crdtId, sinceVc);
52
31
  if (this.state === "CONNECTED") {
53
32
  this.sendSubscribe(crdtId, sinceVc);
54
33
  }
55
34
  }
56
- /** Update the local vector clock for a CRDT (used for reconnect Sync). */
57
35
  updateClock(crdtId, vc) {
58
36
  this.subscriptions.set(crdtId, vc);
59
37
  }
60
- /** Send a raw ClientMsg. Throws if not connected. */
61
38
  send(msg) {
62
39
  if (this.state !== "CONNECTED" || this.ws === null) {
63
40
  throw new Error("WsTransport: not connected");
@@ -67,8 +44,7 @@ export class WsTransport {
67
44
  get currentState() {
68
45
  return this.state;
69
46
  }
70
- /** Resolves when the transport reaches CONNECTED state (or rejects on timeout). */
71
- waitForConnected(timeoutMs = 5_000) {
47
+ waitForConnected(timeoutMs = DEFAULT_TIMEOUT_MS) {
72
48
  if (this.state === "CONNECTED")
73
49
  return Promise.resolve();
74
50
  return new Promise((resolve, reject) => {
@@ -95,7 +71,6 @@ export class WsTransport {
95
71
  };
96
72
  });
97
73
  }
98
- // ---- FSM internals ----
99
74
  doConnect() {
100
75
  if (this.closed)
101
76
  return;
@@ -106,8 +81,8 @@ export class WsTransport {
106
81
  this.ws = ws;
107
82
  ws.addEventListener("open", () => {
108
83
  if (ws !== this.ws)
109
- return; // stale socket
110
- this.backoffMs = 100; // reset backoff on successful connect
84
+ return;
85
+ this.backoffMs = BACKOFF_INITIAL_MS;
111
86
  this.transitionTo("CONNECTED");
112
87
  this.resubscribeAll();
113
88
  });
@@ -127,19 +102,19 @@ export class WsTransport {
127
102
  }
128
103
  });
129
104
  ws.addEventListener("error", () => {
130
- // The "close" event fires right after — let that handle reconnect.
105
+ // HACK: The "close" event fires right after an error — let that handler drive reconnect logic.
131
106
  });
132
107
  }
133
108
  scheduleReconnect() {
134
109
  if (this.closed)
135
110
  return;
136
- const jitter = this.backoffMs * 0.2 * (Math.random() * 2 - 1); // ±20%
111
+ const jitter = this.backoffMs * JITTER_MULTIPLIER * (Math.random() * 2 - 1);
137
112
  const delay = Math.round(this.backoffMs + jitter);
138
113
  this.reconnectTimer = setTimeout(() => {
139
114
  this.reconnectTimer = null;
140
115
  this.doConnect();
141
116
  }, delay);
142
- this.backoffMs = Math.min(this.backoffMs * 2, this.maxBackoffMs);
117
+ this.backoffMs = Math.min(this.backoffMs * BACKOFF_MULTIPLIER, this.maxBackoffMs);
143
118
  }
144
119
  clearReconnectTimer() {
145
120
  if (this.reconnectTimer !== null) {
@@ -153,7 +128,6 @@ export class WsTransport {
153
128
  this.state = next;
154
129
  this.config.onStateChange?.(next);
155
130
  }
156
- /** On reconnect: re-subscribe and send Sync for each known CRDT. */
157
131
  resubscribeAll() {
158
132
  for (const [crdtId, vc] of this.subscriptions) {
159
133
  this.sendSubscribe(crdtId, vc);
@@ -162,9 +136,7 @@ export class WsTransport {
162
136
  sendSubscribe(crdtId, vc) {
163
137
  if (this.ws === null || this.state !== "CONNECTED")
164
138
  return;
165
- // First subscribe so the server starts pushing future deltas
166
139
  this.ws.send(encodeClientMsg({ Subscribe: { crdt_id: crdtId } }));
167
- // Then sync to get missed deltas since our last known VC
168
140
  const vcBytes = encodeVectorClock(vc);
169
141
  this.ws.send(encodeClientMsg({ Sync: { crdt_id: crdtId, since_vc: vcBytes } }));
170
142
  }
@@ -1 +1 @@
1
- {"version":3,"file":"websocket.js","sourceRoot":"","sources":["../../src/transport/websocket.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"websocket.js","sourceRoot":"","sources":["../../src/transport/websocket.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare const toHex: (value: number) => string;
2
+ //# sourceMappingURL=to-hex.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"to-hex.d.ts","sourceRoot":"","sources":["../../src/utils/to-hex.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,KAAK,GAAI,OAAO,MAAM,KAAG,MAA6C,CAAC"}
@@ -0,0 +1,2 @@
1
+ export const toHex = (value) => value.toString(16).padStart(2, "0");
2
+ //# sourceMappingURL=to-hex.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"to-hex.js","sourceRoot":"","sources":["../../src/utils/to-hex.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,KAAa,EAAU,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meridian-sdk",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "TypeScript SDK for Meridian CRDT server",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -15,15 +15,18 @@
15
15
  "build": "tsc",
16
16
  "test": "bun test",
17
17
  "test:watch": "bun test --watch",
18
- "typecheck": "tsc --noEmit"
18
+ "typecheck": "tsc --noEmit",
19
+ "lint": "biome lint ./src",
20
+ "format": "biome format --write ./src"
19
21
  },
20
22
  "devDependencies": {
23
+ "@biomejs/biome": "^2.4.7",
21
24
  "@types/bun": "latest",
22
25
  "typescript": "^5.7.0"
23
26
  },
24
27
  "dependencies": {
25
28
  "effect": "^3.19.0",
26
- "msgpackr": "^1.11.0"
29
+ "@msgpack/msgpack": "^3.0.0"
27
30
  },
28
31
  "peerDependencies": {
29
32
  "typescript": ">=5.0.0"
package/src/auth/token.ts CHANGED
@@ -1,23 +1,9 @@
1
- /**
2
- * Token parsing and expiry check (client-side only — no signature verification).
3
- *
4
- * Wire format: `base64url_no_pad(msgpack(TokenClaims)) + "." + base64url_no_pad(sig[64B])`
5
- */
6
-
7
- import { unpack } from "msgpackr";
1
+ import { decode } from "@msgpack/msgpack";
8
2
  import { Effect, Schema } from "effect";
9
3
  import { TokenParseError, TokenExpiredError } from "../errors.js";
10
4
  import { TokenClaims } from "../schema.js";
5
+ import { TOKEN_SKEW_MS } from "../constants.js";
11
6
 
12
- // ---------------------------------------------------------------------------
13
- // Parse
14
- // ---------------------------------------------------------------------------
15
-
16
- /**
17
- * Parse a Meridian token string and return the decoded claims.
18
- * Returns Effect<TokenClaims, TokenParseError>.
19
- * Does NOT verify the ed25519 signature — the server enforces that.
20
- */
21
7
  export const parseToken = (token: string): Effect.Effect<TokenClaims, TokenParseError> =>
22
8
  Effect.gen(function* () {
23
9
  const dotIndex = token.indexOf(".");
@@ -38,7 +24,7 @@ export const parseToken = (token: string): Effect.Effect<TokenClaims, TokenParse
38
24
 
39
25
  let raw: unknown;
40
26
  try {
41
- raw = unpack(bytes);
27
+ raw = decode(bytes);
42
28
  } catch {
43
29
  yield* Effect.fail(new TokenParseError({ message: "Invalid token format: msgpack decode failed" }));
44
30
  return undefined as never;
@@ -51,39 +37,25 @@ export const parseToken = (token: string): Effect.Effect<TokenClaims, TokenParse
51
37
  );
52
38
  });
53
39
 
54
- /**
55
- * Check token expiry. Returns Effect<TokenClaims, TokenExpiredError>.
56
- * Clock-skew tolerance: ±5s.
57
- */
58
40
  export const checkTokenExpiry = (
59
41
  claims: TokenClaims,
60
42
  nowMs = Date.now(),
61
43
  ): Effect.Effect<TokenClaims, TokenExpiredError> => {
62
- const SKEW_MS = 5_000;
63
- if (nowMs >= claims.expires_at + SKEW_MS) {
44
+ if (nowMs >= claims.expires_at + TOKEN_SKEW_MS) {
64
45
  return Effect.fail(new TokenExpiredError({ expiredAt: claims.expires_at }));
65
46
  }
66
47
  return Effect.succeed(claims);
67
48
  };
68
49
 
69
- /**
70
- * Parse and check expiry in one step.
71
- * Returns Effect<TokenClaims, TokenParseError | TokenExpiredError>.
72
- */
73
50
  export const parseAndValidateToken = (
74
51
  token: string,
75
52
  ): Effect.Effect<TokenClaims, TokenParseError | TokenExpiredError> =>
76
53
  parseToken(token).pipe(Effect.flatMap(checkTokenExpiry));
77
54
 
78
- /** Returns milliseconds until the token expires (negative if already expired). */
79
55
  export const tokenTtlMs = (claims: TokenClaims, nowMs = Date.now()): number =>
80
56
  claims.expires_at - nowMs;
81
57
 
82
- // ---------------------------------------------------------------------------
83
- // Base64url (no-padding) decoder — no external dep
84
- // ---------------------------------------------------------------------------
85
-
86
- function base64urlDecode(input: string): Uint8Array {
58
+ const base64urlDecode = (input: string): Uint8Array => {
87
59
  const padded = input.replace(/-/g, "+").replace(/_/g, "/");
88
60
  const padLen = (4 - (padded.length % 4)) % 4;
89
61
  const b64 = padded + "=".repeat(padLen);
@@ -93,4 +65,4 @@ function base64urlDecode(input: string): Uint8Array {
93
65
  bytes[i] = binary.charCodeAt(i);
94
66
  }
95
67
  return bytes;
96
- }
68
+ };
package/src/client.ts CHANGED
@@ -1,20 +1,4 @@
1
- /**
2
- * MeridianClient — top-level SDK entry point.
3
- *
4
- * Use `MeridianClient.create(config)` (returns Effect) to parse the token
5
- * and validate it before connecting.
6
- *
7
- * ```ts
8
- * const client = await Effect.runPromise(
9
- * MeridianClient.create({ url: "ws://localhost:3000", namespace: "room", token })
10
- * );
11
- * const counter = client.gcounter("gc:page-views");
12
- * counter.increment();
13
- * counter.onChange(v => console.log("views:", v));
14
- * ```
15
- */
16
-
17
- import { Effect, Schema } from "effect";
1
+ import { Effect, type Schema } from "effect";
18
2
  import { WsTransport } from "./transport/websocket.js";
19
3
  import { HttpClient } from "./transport/http.js";
20
4
  import { GCounterHandle } from "./crdt/gcounter.js";
@@ -33,25 +17,39 @@ import { parseAndValidateToken } from "./auth/token.js";
33
17
  import type { ServerMsg, TokenClaims } from "./schema.js";
34
18
  import type { TokenParseError, TokenExpiredError } from "./errors.js";
35
19
 
36
- // ---------------------------------------------------------------------------
37
- // Config
38
- // ---------------------------------------------------------------------------
39
-
40
20
  export interface MeridianClientConfig {
41
- /** Base URL of the Meridian server, e.g. "http://localhost:3000" */
42
21
  url: string;
43
- /** Namespace to connect to. */
44
22
  namespace: string;
45
- /** Meridian token for this namespace. */
46
23
  token: string;
47
- /** If true, open the WebSocket immediately. Default: true */
48
24
  autoConnect?: boolean;
49
25
  }
50
26
 
51
- // ---------------------------------------------------------------------------
52
- // MeridianClient
53
- // ---------------------------------------------------------------------------
54
-
27
+ /**
28
+ * The main entry point for the Meridian real-time CRDT SDK.
29
+ *
30
+ * Create an instance with the static `MeridianClient.create()` factory, then
31
+ * use the handle methods to obtain typed CRDT handles that sync automatically
32
+ * over WebSocket.
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * import { Effect } from 'effect';
37
+ * import { MeridianClient } from 'meridian-sdk';
38
+ *
39
+ * const client = await Effect.runPromise(
40
+ * MeridianClient.create({
41
+ * url: 'wss://example.com',
42
+ * namespace: 'my-app',
43
+ * token: '<JWT>',
44
+ * })
45
+ * );
46
+ *
47
+ * const counter = client.gcounter('visitors');
48
+ * counter.increment();
49
+ *
50
+ * client.close();
51
+ * ```
52
+ */
55
53
  export class MeridianClient {
56
54
  readonly namespace: string;
57
55
  readonly clientId: number;
@@ -60,8 +58,7 @@ export class MeridianClient {
60
58
  private readonly transport: WsTransport;
61
59
  readonly http: HttpClient;
62
60
 
63
- // Handle caches — keyed by crdt_id. Generic params erased at storage level;
64
- // factories restore them via typed get+cast on retrieval.
61
+ // HACK: Generic params are erased at storage level; factories restore them via typed get+cast on retrieval.
65
62
  private readonly gcHandles = new Map<string, GCounterHandle>();
66
63
  private readonly pnHandles = new Map<string, PNCounterHandle>();
67
64
  private readonly orHandles = new Map<string, ORSetHandle<unknown>>();
@@ -91,8 +88,25 @@ export class MeridianClient {
91
88
  }
92
89
 
93
90
  /**
94
- * Create a MeridianClient, parsing and validating the token.
95
- * Returns Effect<MeridianClient, TokenParseError | TokenExpiredError>.
91
+ * Creates and validates a new `MeridianClient` from the supplied configuration.
92
+ *
93
+ * The JWT `token` is parsed and validated synchronously inside the Effect; the
94
+ * Effect fails with `TokenParseError` or `TokenExpiredError` if the token is
95
+ * malformed or expired. The WebSocket connection is opened immediately unless
96
+ * `autoConnect` is set to `false`.
97
+ *
98
+ * @param config - Connection configuration including the server `url`,
99
+ * `namespace`, and a signed `token`.
100
+ *
101
+ * @example
102
+ * ```ts
103
+ * import { Effect } from 'effect';
104
+ * import { MeridianClient } from 'meridian-sdk';
105
+ *
106
+ * const client = await Effect.runPromise(
107
+ * MeridianClient.create({ url: 'ws://localhost:8080', namespace: 'demo', token: myToken })
108
+ * );
109
+ * ```
96
110
  */
97
111
  static create(
98
112
  config: MeridianClientConfig,
@@ -102,74 +116,160 @@ export class MeridianClient {
102
116
  );
103
117
  }
104
118
 
105
- // ---- CRDT factory methods ----
106
-
119
+ /**
120
+ * Returns a handle for a grow-only counter (GCounter) CRDT.
121
+ *
122
+ * Handles are cached by `crdtId`; calling this method multiple times with the
123
+ * same id returns the same handle instance and creates only one subscription.
124
+ *
125
+ * @param crdtId - Unique identifier for the CRDT within this namespace.
126
+ *
127
+ * @example
128
+ * ```ts
129
+ * const counter = client.gcounter('page-views');
130
+ * counter.increment(5);
131
+ * console.log(counter.value()); // 5
132
+ * ```
133
+ */
107
134
  gcounter(crdtId: string): GCounterHandle {
108
- let h = this.gcHandles.get(crdtId);
109
- if (!h) {
110
- h = new GCounterHandle({ ns: this.namespace, crdtId, clientId: this.clientId, transport: this.transport });
111
- this.gcHandles.set(crdtId, h);
135
+ let handle = this.gcHandles.get(crdtId);
136
+ if (!handle) {
137
+ handle = new GCounterHandle({ ns: this.namespace, crdtId, clientId: this.clientId, transport: this.transport });
138
+ this.gcHandles.set(crdtId, handle);
112
139
  this.transport.subscribe(crdtId);
113
140
  }
114
- return h;
141
+ return handle;
115
142
  }
116
143
 
144
+ /**
145
+ * Returns a handle for a positive-negative counter (PNCounter) CRDT.
146
+ *
147
+ * Handles are cached by `crdtId`; calling this method multiple times with the
148
+ * same id returns the same handle instance and creates only one subscription.
149
+ *
150
+ * @param crdtId - Unique identifier for the CRDT within this namespace.
151
+ *
152
+ * @example
153
+ * ```ts
154
+ * const score = client.pncounter('game-score');
155
+ * score.increment(10);
156
+ * score.decrement(3);
157
+ * console.log(score.value()); // 7
158
+ * ```
159
+ */
117
160
  pncounter(crdtId: string): PNCounterHandle {
118
- let h = this.pnHandles.get(crdtId);
119
- if (!h) {
120
- h = new PNCounterHandle({ ns: this.namespace, crdtId, clientId: this.clientId, transport: this.transport });
121
- this.pnHandles.set(crdtId, h);
161
+ let handle = this.pnHandles.get(crdtId);
162
+ if (!handle) {
163
+ handle = new PNCounterHandle({ ns: this.namespace, crdtId, clientId: this.clientId, transport: this.transport });
164
+ this.pnHandles.set(crdtId, handle);
122
165
  this.transport.subscribe(crdtId);
123
166
  }
124
- return h;
167
+ return handle;
125
168
  }
126
169
 
170
+ /**
171
+ * Returns a handle for an Observed-Remove Set (OR-Set) CRDT.
172
+ *
173
+ * Handles are cached by `crdtId`; calling this method multiple times with the
174
+ * same id returns the same handle instance and creates only one subscription.
175
+ *
176
+ * @param crdtId - Unique identifier for the CRDT within this namespace.
177
+ * @param schema - Optional Effect schema used to decode elements from the wire format.
178
+ *
179
+ * @example
180
+ * ```ts
181
+ * import { Schema } from 'effect';
182
+ *
183
+ * const tags = client.orset('article-tags', Schema.String);
184
+ * tags.add('typescript');
185
+ * tags.remove('typescript');
186
+ * ```
187
+ */
127
188
  orset<T>(crdtId: string, schema?: Schema.Schema<T>): ORSetHandle<T> {
128
- let h = this.orHandles.get(crdtId) as ORSetHandle<T> | undefined;
129
- if (!h) {
189
+ let handle = this.orHandles.get(crdtId) as ORSetHandle<T> | undefined;
190
+ if (!handle) {
130
191
  const base = { ns: this.namespace, crdtId, clientId: this.clientId, transport: this.transport };
131
- h = schema ? new ORSetHandle<T>({ ...base, schema }) : new ORSetHandle<T>(base);
132
- this.orHandles.set(crdtId, h as ORSetHandle<unknown>);
192
+ handle = schema ? new ORSetHandle<T>({ ...base, schema }) : new ORSetHandle<T>(base);
193
+ this.orHandles.set(crdtId, handle as ORSetHandle<unknown>);
133
194
  this.transport.subscribe(crdtId);
134
195
  }
135
- return h;
196
+ return handle;
136
197
  }
137
198
 
199
+ /**
200
+ * Returns a handle for a Last-Write-Wins register (LWW-Register) CRDT.
201
+ *
202
+ * Handles are cached by `crdtId`; calling this method multiple times with the
203
+ * same id returns the same handle instance and creates only one subscription.
204
+ *
205
+ * @param crdtId - Unique identifier for the CRDT within this namespace.
206
+ * @param schema - Optional Effect schema used to decode the value from the wire format.
207
+ *
208
+ * @example
209
+ * ```ts
210
+ * import { Schema } from 'effect';
211
+ *
212
+ * const theme = client.lwwregister('ui-theme', Schema.Literal('light', 'dark'));
213
+ * theme.set('dark');
214
+ * console.log(theme.value()); // 'dark'
215
+ * ```
216
+ */
138
217
  lwwregister<T>(crdtId: string, schema?: Schema.Schema<T>): LwwRegisterHandle<T> {
139
- let h = this.lwHandles.get(crdtId) as LwwRegisterHandle<T> | undefined;
140
- if (!h) {
218
+ let handle = this.lwHandles.get(crdtId) as LwwRegisterHandle<T> | undefined;
219
+ if (!handle) {
141
220
  const base = { ns: this.namespace, crdtId, clientId: this.clientId, transport: this.transport };
142
- h = schema ? new LwwRegisterHandle<T>({ ...base, schema }) : new LwwRegisterHandle<T>(base);
143
- this.lwHandles.set(crdtId, h as LwwRegisterHandle<unknown>);
221
+ handle = schema ? new LwwRegisterHandle<T>({ ...base, schema }) : new LwwRegisterHandle<T>(base);
222
+ this.lwHandles.set(crdtId, handle as LwwRegisterHandle<unknown>);
144
223
  this.transport.subscribe(crdtId);
145
224
  }
146
- return h;
225
+ return handle;
147
226
  }
148
227
 
228
+ /**
229
+ * Returns a handle for a presence channel CRDT.
230
+ *
231
+ * Handles are cached by `crdtId`; calling this method multiple times with the
232
+ * same id returns the same handle instance and creates only one subscription.
233
+ *
234
+ * @param crdtId - Unique identifier for the presence channel within this namespace.
235
+ * @param schema - Optional Effect schema used to decode peer data from the wire format.
236
+ *
237
+ * @example
238
+ * ```ts
239
+ * import { Schema } from 'effect';
240
+ *
241
+ * const room = client.presence('room-1', Schema.Struct({ name: Schema.String }));
242
+ * room.heartbeat({ name: 'Alice' }, 10_000);
243
+ * console.log(room.online()); // [{ clientId: 1, data: { name: 'Alice' }, expiresAtMs: ... }]
244
+ * room.leave();
245
+ * ```
246
+ */
149
247
  presence<T>(crdtId: string, schema?: Schema.Schema<T>): PresenceHandle<T> {
150
- let h = this.prHandles.get(crdtId) as PresenceHandle<T> | undefined;
151
- if (!h) {
248
+ let handle = this.prHandles.get(crdtId) as PresenceHandle<T> | undefined;
249
+ if (!handle) {
152
250
  const base = { ns: this.namespace, crdtId, clientId: this.clientId, transport: this.transport };
153
- h = schema ? new PresenceHandle<T>({ ...base, schema }) : new PresenceHandle<T>(base);
154
- this.prHandles.set(crdtId, h as PresenceHandle<unknown>);
251
+ handle = schema ? new PresenceHandle<T>({ ...base, schema }) : new PresenceHandle<T>(base);
252
+ this.prHandles.set(crdtId, handle as PresenceHandle<unknown>);
155
253
  this.transport.subscribe(crdtId);
156
254
  }
157
- return h;
255
+ return handle;
158
256
  }
159
257
 
160
- // ---- Lifecycle ----
161
-
162
- /** Resolves when the WebSocket is connected and ready. */
163
258
  waitForConnected(timeoutMs = 5_000): Promise<void> {
164
259
  return this.transport.waitForConnected(timeoutMs);
165
260
  }
166
261
 
262
+ /**
263
+ * Closes the underlying WebSocket connection.
264
+ *
265
+ * Call this when the client is no longer needed to free resources. If you are
266
+ * using `<MeridianProvider>` in React, the provider calls this automatically
267
+ * on unmount.
268
+ */
167
269
  close(): void {
168
270
  this.transport.close();
169
271
  }
170
272
 
171
- // ---- Internal: route ServerMsg.Delta to the right handle ----
172
-
173
273
  private handleServerMsg(msg: ServerMsg): void {
174
274
  if (!("Delta" in msg)) return;
175
275
  const { crdt_id, delta_bytes } = msg.Delta;