meridian-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/README.md +119 -0
  2. package/dist/auth/token.d.ts +27 -0
  3. package/dist/auth/token.d.ts.map +1 -0
  4. package/dist/auth/token.js +75 -0
  5. package/dist/auth/token.js.map +1 -0
  6. package/dist/client.d.ts +60 -0
  7. package/dist/client.d.ts.map +1 -0
  8. package/dist/client.js +166 -0
  9. package/dist/client.js.map +1 -0
  10. package/dist/codec.d.ts +29 -0
  11. package/dist/codec.d.ts.map +1 -0
  12. package/dist/codec.js +72 -0
  13. package/dist/codec.js.map +1 -0
  14. package/dist/crdt/gcounter.d.ts +36 -0
  15. package/dist/crdt/gcounter.d.ts.map +1 -0
  16. package/dist/crdt/gcounter.js +76 -0
  17. package/dist/crdt/gcounter.js.map +1 -0
  18. package/dist/crdt/lwwregister.d.ts +42 -0
  19. package/dist/crdt/lwwregister.d.ts.map +1 -0
  20. package/dist/crdt/lwwregister.js +93 -0
  21. package/dist/crdt/lwwregister.js.map +1 -0
  22. package/dist/crdt/orset.d.ts +40 -0
  23. package/dist/crdt/orset.d.ts.map +1 -0
  24. package/dist/crdt/orset.js +115 -0
  25. package/dist/crdt/orset.js.map +1 -0
  26. package/dist/crdt/pncounter.d.ts +32 -0
  27. package/dist/crdt/pncounter.d.ts.map +1 -0
  28. package/dist/crdt/pncounter.js +77 -0
  29. package/dist/crdt/pncounter.js.map +1 -0
  30. package/dist/crdt/presence.d.ts +45 -0
  31. package/dist/crdt/presence.d.ts.map +1 -0
  32. package/dist/crdt/presence.js +133 -0
  33. package/dist/crdt/presence.js.map +1 -0
  34. package/dist/errors.d.ts +53 -0
  35. package/dist/errors.d.ts.map +1 -0
  36. package/dist/errors.js +30 -0
  37. package/dist/errors.js.map +1 -0
  38. package/dist/index.d.ts +18 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +20 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/schema.d.ts +118 -0
  43. package/dist/schema.d.ts.map +1 -0
  44. package/dist/schema.js +95 -0
  45. package/dist/schema.js.map +1 -0
  46. package/dist/sync/clock.d.ts +42 -0
  47. package/dist/sync/clock.d.ts.map +1 -0
  48. package/dist/sync/clock.js +95 -0
  49. package/dist/sync/clock.js.map +1 -0
  50. package/dist/sync/delta.d.ts +52 -0
  51. package/dist/sync/delta.d.ts.map +1 -0
  52. package/dist/sync/delta.js +35 -0
  53. package/dist/sync/delta.js.map +1 -0
  54. package/dist/transport/http.d.ts +38 -0
  55. package/dist/transport/http.d.ts.map +1 -0
  56. package/dist/transport/http.js +98 -0
  57. package/dist/transport/http.js.map +1 -0
  58. package/dist/transport/websocket.d.ts +60 -0
  59. package/dist/transport/websocket.d.ts.map +1 -0
  60. package/dist/transport/websocket.js +144 -0
  61. package/dist/transport/websocket.js.map +1 -0
  62. package/package.json +31 -0
  63. package/src/auth/token.ts +96 -0
  64. package/src/client.ts +197 -0
  65. package/src/codec.ts +92 -0
  66. package/src/crdt/gcounter.ts +101 -0
  67. package/src/crdt/lwwregister.ts +113 -0
  68. package/src/crdt/orset.ts +131 -0
  69. package/src/crdt/pncounter.ts +93 -0
  70. package/src/crdt/presence.ts +162 -0
  71. package/src/errors.ts +51 -0
  72. package/src/index.ts +59 -0
  73. package/src/schema.ts +144 -0
  74. package/src/sync/clock.ts +117 -0
  75. package/src/sync/delta.ts +104 -0
  76. package/src/transport/http.ts +150 -0
  77. package/src/transport/websocket.ts +189 -0
  78. package/test/crdt.test.ts +290 -0
  79. package/test/sync.test.ts +178 -0
  80. package/test/tsconfig.json +8 -0
  81. package/tsconfig.json +22 -0
package/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # meridian-sdk
2
+
3
+ TypeScript SDK for [Meridian](../README.md) — Effect-based, fully typed, msgpack wire protocol.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ bun add meridian-sdk effect msgpackr
9
+ # or
10
+ npm install meridian-sdk effect msgpackr
11
+ ```
12
+
13
+ ## Quick start
14
+
15
+ ```ts
16
+ import { Effect, Schema } from "effect";
17
+ import { MeridianClient } from "meridian-sdk";
18
+
19
+ // MeridianClient.create() parses + validates the token — returns Effect
20
+ const client = await Effect.runPromise(
21
+ MeridianClient.create({
22
+ url: "http://localhost:3000",
23
+ namespace: "my-room",
24
+ token: process.env.MERIDIAN_TOKEN!,
25
+ })
26
+ );
27
+
28
+ // GCounter
29
+ const views = client.gcounter("gc:views");
30
+ views.increment(1);
31
+ views.onChange(v => console.log("views:", v));
32
+
33
+ // LWW Register with runtime schema validation
34
+ const Profile = Schema.Struct({ name: Schema.String, avatar: Schema.String });
35
+ const profile = client.lwwregister("lw:profile", Profile);
36
+ profile.set({ name: "Chahine", avatar: "https://..." });
37
+ profile.onChange(v => console.log("profile:", v)); // v: { name, avatar } | null
38
+
39
+ // Presence
40
+ const Cursor = Schema.Struct({ x: Schema.Number, y: Schema.Number });
41
+ const room = client.presence("pr:room", Cursor);
42
+ room.heartbeat({ x: 100, y: 200 }, 30_000);
43
+ room.onChange(entries => console.log("online:", entries));
44
+
45
+ // Close WebSocket when done
46
+ client.close();
47
+ ```
48
+
49
+ ## Error handling
50
+
51
+ All errors are `Data.TaggedError` — matchable with `Effect.catchTag`:
52
+
53
+ ```ts
54
+ import { Effect } from "effect";
55
+ import { MeridianClient, TokenExpiredError, HttpError, NetworkError } from "meridian-sdk";
56
+
57
+ await Effect.runPromise(
58
+ MeridianClient.create(config).pipe(
59
+ Effect.catchTag("TokenExpiredError", (e) =>
60
+ Effect.die(`Token expired at ${new Date(e.expiredAt).toISOString()}`)
61
+ ),
62
+ Effect.flatMap(client =>
63
+ client.http.getCrdt("my-room", "gc:views").pipe(
64
+ Effect.catchTag("HttpError", e => Effect.succeed(`HTTP ${e.status}`)),
65
+ Effect.catchTag("NetworkError", e => Effect.succeed(`Network: ${e.message}`)),
66
+ )
67
+ ),
68
+ )
69
+ );
70
+ ```
71
+
72
+ ## API
73
+
74
+ ### `MeridianClient.create(config)` → `Effect<MeridianClient, TokenParseError | TokenExpiredError>`
75
+
76
+ | Option | Type | Description |
77
+ |--------|------|-------------|
78
+ | `url` | `string` | Server base URL (`http://` or `ws://`) |
79
+ | `namespace` | `string` | Namespace to connect to |
80
+ | `token` | `string` | Meridian token |
81
+ | `autoConnect` | `boolean?` | Open WebSocket immediately (default: `true`) |
82
+
83
+ ### CRDT handles
84
+
85
+ | Method | Returns | Schema? |
86
+ |--------|---------|---------|
87
+ | `client.gcounter(id)` | `GCounterHandle` | — |
88
+ | `client.pncounter(id)` | `PNCounterHandle` | — |
89
+ | `client.orset(id, schema?)` | `ORSetHandle<T>` | Optional |
90
+ | `client.lwwregister(id, schema?)` | `LwwRegisterHandle<T>` | Optional |
91
+ | `client.presence(id, schema?)` | `PresenceHandle<T>` | Optional |
92
+
93
+ Without a schema, `T = unknown`. With a schema, incoming deltas are validated at runtime via `Schema.decodeUnknownSync`.
94
+
95
+ ### HTTP client (`client.http`)
96
+
97
+ All methods return `Effect<T, HttpError | NetworkError>`:
98
+
99
+ ```ts
100
+ client.http.getCrdt(ns, id) // → Effect<CrdtGetResponse, ...>
101
+ client.http.postOp(ns, id, op) // → Effect<CrdtOpResponse, ...>
102
+ client.http.syncCrdt(ns, id, sinceVc) // → Effect<CrdtGetResponse, ...>
103
+ client.http.issueToken(ns, opts) // → Effect<TokenIssueResponse, ...>
104
+ ```
105
+
106
+ ## Wire protocol
107
+
108
+ - **Transport**: HTTP + WebSocket
109
+ - **Serialization**: MessagePack (msgpackr)
110
+ - **Auth**: Bearer token in `Authorization` header or `?token=` query param
111
+
112
+ ## Development
113
+
114
+ ```bash
115
+ bun install
116
+ bun test # run tests
117
+ bun run typecheck # tsc --noEmit
118
+ bun run build # compile to dist/
119
+ ```
@@ -0,0 +1,27 @@
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
+ import { Effect } from "effect";
7
+ import { TokenParseError, TokenExpiredError } from "../errors.js";
8
+ import { TokenClaims } from "../schema.js";
9
+ /**
10
+ * Parse a Meridian token string and return the decoded claims.
11
+ * Returns Effect<TokenClaims, TokenParseError>.
12
+ * Does NOT verify the ed25519 signature — the server enforces that.
13
+ */
14
+ export declare const parseToken: (token: string) => Effect.Effect<TokenClaims, TokenParseError>;
15
+ /**
16
+ * Check token expiry. Returns Effect<TokenClaims, TokenExpiredError>.
17
+ * Clock-skew tolerance: ±5s.
18
+ */
19
+ export declare const checkTokenExpiry: (claims: TokenClaims, nowMs?: number) => Effect.Effect<TokenClaims, TokenExpiredError>;
20
+ /**
21
+ * Parse and check expiry in one step.
22
+ * Returns Effect<TokenClaims, TokenParseError | TokenExpiredError>.
23
+ */
24
+ export declare const parseAndValidateToken: (token: string) => Effect.Effect<TokenClaims, TokenParseError | TokenExpiredError>;
25
+ /** Returns milliseconds until the token expires (negative if already expired). */
26
+ export declare const tokenTtlMs: (claims: TokenClaims, nowMs?: number) => number;
27
+ //# sourceMappingURL=token.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token.d.ts","sourceRoot":"","sources":["../../src/auth/token.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAM3C;;;;GAIG;AACH,eAAO,MAAM,UAAU,GAAI,OAAO,MAAM,KAAG,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,eAAe,CA+BjF,CAAC;AAEL;;;GAGG;AACH,eAAO,MAAM,gBAAgB,GAC3B,QAAQ,WAAW,EACnB,cAAkB,KACjB,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,iBAAiB,CAM9C,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,qBAAqB,GAChC,OAAO,MAAM,KACZ,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,eAAe,GAAG,iBAAiB,CACP,CAAC;AAE3D,kFAAkF;AAClF,eAAO,MAAM,UAAU,GAAI,QAAQ,WAAW,EAAE,cAAkB,KAAG,MAC1C,CAAC"}
@@ -0,0 +1,75 @@
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
+ import { unpack } from "msgpackr";
7
+ import { Effect, Schema } from "effect";
8
+ import { TokenParseError, TokenExpiredError } from "../errors.js";
9
+ import { TokenClaims } from "../schema.js";
10
+ // ---------------------------------------------------------------------------
11
+ // Parse
12
+ // ---------------------------------------------------------------------------
13
+ /**
14
+ * Parse a Meridian token string and return the decoded claims.
15
+ * Returns Effect<TokenClaims, TokenParseError>.
16
+ * Does NOT verify the ed25519 signature — the server enforces that.
17
+ */
18
+ export const parseToken = (token) => Effect.gen(function* () {
19
+ const dotIndex = token.indexOf(".");
20
+ if (dotIndex === -1) {
21
+ yield* Effect.fail(new TokenParseError({ message: "Invalid token format: missing '.'" }));
22
+ return undefined;
23
+ }
24
+ const payloadB64 = token.slice(0, dotIndex);
25
+ let bytes;
26
+ try {
27
+ bytes = base64urlDecode(payloadB64);
28
+ }
29
+ catch {
30
+ yield* Effect.fail(new TokenParseError({ message: "Invalid token format: base64url decode failed" }));
31
+ return undefined;
32
+ }
33
+ let raw;
34
+ try {
35
+ raw = unpack(bytes);
36
+ }
37
+ catch {
38
+ yield* Effect.fail(new TokenParseError({ message: "Invalid token format: msgpack decode failed" }));
39
+ return undefined;
40
+ }
41
+ return yield* Schema.decodeUnknown(TokenClaims)(raw).pipe(Effect.mapError((e) => new TokenParseError({ message: `Invalid token format: ${e.message}` })));
42
+ });
43
+ /**
44
+ * Check token expiry. Returns Effect<TokenClaims, TokenExpiredError>.
45
+ * Clock-skew tolerance: ±5s.
46
+ */
47
+ export const checkTokenExpiry = (claims, nowMs = Date.now()) => {
48
+ const SKEW_MS = 5_000;
49
+ if (nowMs >= claims.expires_at + SKEW_MS) {
50
+ return Effect.fail(new TokenExpiredError({ expiredAt: claims.expires_at }));
51
+ }
52
+ return Effect.succeed(claims);
53
+ };
54
+ /**
55
+ * Parse and check expiry in one step.
56
+ * Returns Effect<TokenClaims, TokenParseError | TokenExpiredError>.
57
+ */
58
+ export const parseAndValidateToken = (token) => parseToken(token).pipe(Effect.flatMap(checkTokenExpiry));
59
+ /** Returns milliseconds until the token expires (negative if already expired). */
60
+ export const tokenTtlMs = (claims, nowMs = Date.now()) => claims.expires_at - nowMs;
61
+ // ---------------------------------------------------------------------------
62
+ // Base64url (no-padding) decoder — no external dep
63
+ // ---------------------------------------------------------------------------
64
+ function base64urlDecode(input) {
65
+ const padded = input.replace(/-/g, "+").replace(/_/g, "/");
66
+ const padLen = (4 - (padded.length % 4)) % 4;
67
+ const b64 = padded + "=".repeat(padLen);
68
+ const binary = atob(b64);
69
+ const bytes = new Uint8Array(binary.length);
70
+ for (let i = 0; i < binary.length; i++) {
71
+ bytes[i] = binary.charCodeAt(i);
72
+ }
73
+ return bytes;
74
+ }
75
+ //# sourceMappingURL=token.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"token.js","sourceRoot":"","sources":["../../src/auth/token.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,60 @@
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
+ import { Effect, Schema } from "effect";
17
+ import { HttpClient } from "./transport/http.js";
18
+ import { GCounterHandle } from "./crdt/gcounter.js";
19
+ import { PNCounterHandle } from "./crdt/pncounter.js";
20
+ import { ORSetHandle } from "./crdt/orset.js";
21
+ import { LwwRegisterHandle } from "./crdt/lwwregister.js";
22
+ import { PresenceHandle } from "./crdt/presence.js";
23
+ import type { TokenClaims } from "./schema.js";
24
+ import type { TokenParseError, TokenExpiredError } from "./errors.js";
25
+ export interface MeridianClientConfig {
26
+ /** Base URL of the Meridian server, e.g. "http://localhost:3000" */
27
+ url: string;
28
+ /** Namespace to connect to. */
29
+ namespace: string;
30
+ /** Meridian token for this namespace. */
31
+ token: string;
32
+ /** If true, open the WebSocket immediately. Default: true */
33
+ autoConnect?: boolean;
34
+ }
35
+ export declare class MeridianClient {
36
+ readonly namespace: string;
37
+ readonly clientId: number;
38
+ readonly claims: TokenClaims;
39
+ private readonly transport;
40
+ readonly http: HttpClient;
41
+ private readonly gcHandles;
42
+ private readonly pnHandles;
43
+ private readonly orHandles;
44
+ private readonly lwHandles;
45
+ private readonly prHandles;
46
+ private constructor();
47
+ /**
48
+ * Create a MeridianClient, parsing and validating the token.
49
+ * Returns Effect<MeridianClient, TokenParseError | TokenExpiredError>.
50
+ */
51
+ static create(config: MeridianClientConfig): Effect.Effect<MeridianClient, TokenParseError | TokenExpiredError>;
52
+ gcounter(crdtId: string): GCounterHandle;
53
+ pncounter(crdtId: string): PNCounterHandle;
54
+ orset<T>(crdtId: string, schema?: Schema.Schema<T>): ORSetHandle<T>;
55
+ lwwregister<T>(crdtId: string, schema?: Schema.Schema<T>): LwwRegisterHandle<T>;
56
+ presence<T>(crdtId: string, schema?: Schema.Schema<T>): PresenceHandle<T>;
57
+ close(): void;
58
+ private handleServerMsg;
59
+ }
60
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAExC,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AASpD,OAAO,KAAK,EAAa,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,KAAK,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAMtE,MAAM,WAAW,oBAAoB;IACnC,oEAAoE;IACpE,GAAG,EAAE,MAAM,CAAC;IACZ,+BAA+B;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,yCAAyC;IACzC,KAAK,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAMD,qBAAa,cAAc;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAE7B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAc;IACxC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAI1B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqC;IAC/D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAsC;IAChE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA2C;IACrE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAiD;IAC3E,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA8C;IAExE,OAAO;IAsBP;;;OAGG;IACH,MAAM,CAAC,MAAM,CACX,MAAM,EAAE,oBAAoB,GAC3B,MAAM,CAAC,MAAM,CAAC,cAAc,EAAE,eAAe,GAAG,iBAAiB,CAAC;IAQrE,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,cAAc;IAUxC,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe;IAU1C,KAAK,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,WAAW,CAAC,CAAC,CAAC;IAWnE,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,iBAAiB,CAAC,CAAC,CAAC;IAW/E,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC;IAazE,KAAK,IAAI,IAAI;IAMb,OAAO,CAAC,eAAe;CA6BxB"}
package/dist/client.js ADDED
@@ -0,0 +1,166 @@
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
+ import { Effect, Schema } from "effect";
17
+ import { WsTransport } from "./transport/websocket.js";
18
+ import { HttpClient } from "./transport/http.js";
19
+ import { GCounterHandle } from "./crdt/gcounter.js";
20
+ import { PNCounterHandle } from "./crdt/pncounter.js";
21
+ import { ORSetHandle } from "./crdt/orset.js";
22
+ import { LwwRegisterHandle } from "./crdt/lwwregister.js";
23
+ import { PresenceHandle } from "./crdt/presence.js";
24
+ import { decodeGCounterDelta, decodePNCounterDelta, decodeORSetDelta, decodeLwwDelta, decodePresenceDelta, } from "./sync/delta.js";
25
+ import { parseAndValidateToken } from "./auth/token.js";
26
+ // ---------------------------------------------------------------------------
27
+ // MeridianClient
28
+ // ---------------------------------------------------------------------------
29
+ export class MeridianClient {
30
+ namespace;
31
+ clientId;
32
+ claims;
33
+ transport;
34
+ http;
35
+ // Handle caches — keyed by crdt_id. Generic params erased at storage level;
36
+ // factories restore them via typed get+cast on retrieval.
37
+ gcHandles = new Map();
38
+ pnHandles = new Map();
39
+ orHandles = new Map();
40
+ lwHandles = new Map();
41
+ prHandles = new Map();
42
+ constructor(config, claims) {
43
+ this.namespace = config.namespace;
44
+ this.claims = claims;
45
+ this.clientId = claims.client_id;
46
+ const httpBase = config.url
47
+ .replace(/^wss:\/\//, "https://")
48
+ .replace(/^ws:\/\//, "http://");
49
+ this.http = new HttpClient({ baseUrl: httpBase, token: config.token });
50
+ const wsUrl = `${config.url.replace(/^http/, "ws")}/v1/namespaces/${config.namespace}/connect`;
51
+ this.transport = new WsTransport({
52
+ url: wsUrl,
53
+ token: config.token,
54
+ onMessage: (msg) => { this.handleServerMsg(msg); },
55
+ });
56
+ if (config.autoConnect !== false) {
57
+ this.transport.connect();
58
+ }
59
+ }
60
+ /**
61
+ * Create a MeridianClient, parsing and validating the token.
62
+ * Returns Effect<MeridianClient, TokenParseError | TokenExpiredError>.
63
+ */
64
+ static create(config) {
65
+ return parseAndValidateToken(config.token).pipe(Effect.map((claims) => new MeridianClient(config, claims)));
66
+ }
67
+ // ---- CRDT factory methods ----
68
+ gcounter(crdtId) {
69
+ let h = this.gcHandles.get(crdtId);
70
+ if (!h) {
71
+ h = new GCounterHandle({ ns: this.namespace, crdtId, clientId: this.clientId, transport: this.transport });
72
+ this.gcHandles.set(crdtId, h);
73
+ this.transport.subscribe(crdtId);
74
+ }
75
+ return h;
76
+ }
77
+ pncounter(crdtId) {
78
+ let h = this.pnHandles.get(crdtId);
79
+ if (!h) {
80
+ h = new PNCounterHandle({ ns: this.namespace, crdtId, clientId: this.clientId, transport: this.transport });
81
+ this.pnHandles.set(crdtId, h);
82
+ this.transport.subscribe(crdtId);
83
+ }
84
+ return h;
85
+ }
86
+ orset(crdtId, schema) {
87
+ let h = this.orHandles.get(crdtId);
88
+ if (!h) {
89
+ const base = { ns: this.namespace, crdtId, clientId: this.clientId, transport: this.transport };
90
+ h = schema ? new ORSetHandle({ ...base, schema }) : new ORSetHandle(base);
91
+ this.orHandles.set(crdtId, h);
92
+ this.transport.subscribe(crdtId);
93
+ }
94
+ return h;
95
+ }
96
+ lwwregister(crdtId, schema) {
97
+ let h = this.lwHandles.get(crdtId);
98
+ if (!h) {
99
+ const base = { ns: this.namespace, crdtId, clientId: this.clientId, transport: this.transport };
100
+ h = schema ? new LwwRegisterHandle({ ...base, schema }) : new LwwRegisterHandle(base);
101
+ this.lwHandles.set(crdtId, h);
102
+ this.transport.subscribe(crdtId);
103
+ }
104
+ return h;
105
+ }
106
+ presence(crdtId, schema) {
107
+ let h = this.prHandles.get(crdtId);
108
+ if (!h) {
109
+ const base = { ns: this.namespace, crdtId, clientId: this.clientId, transport: this.transport };
110
+ h = schema ? new PresenceHandle({ ...base, schema }) : new PresenceHandle(base);
111
+ this.prHandles.set(crdtId, h);
112
+ this.transport.subscribe(crdtId);
113
+ }
114
+ return h;
115
+ }
116
+ // ---- Lifecycle ----
117
+ close() {
118
+ this.transport.close();
119
+ }
120
+ // ---- Internal: route ServerMsg.Delta to the right handle ----
121
+ handleServerMsg(msg) {
122
+ if (!("Delta" in msg))
123
+ return;
124
+ const { crdt_id, delta_bytes } = msg.Delta;
125
+ const gcHandle = this.gcHandles.get(crdt_id);
126
+ if (gcHandle) {
127
+ try {
128
+ gcHandle.applyDelta(decodeGCounterDelta(delta_bytes));
129
+ }
130
+ catch { /* stale */ }
131
+ return;
132
+ }
133
+ const pnHandle = this.pnHandles.get(crdt_id);
134
+ if (pnHandle) {
135
+ try {
136
+ pnHandle.applyDelta(decodePNCounterDelta(delta_bytes));
137
+ }
138
+ catch { /* stale */ }
139
+ return;
140
+ }
141
+ const orHandle = this.orHandles.get(crdt_id);
142
+ if (orHandle) {
143
+ try {
144
+ orHandle.applyDelta(decodeORSetDelta(delta_bytes));
145
+ }
146
+ catch { /* stale */ }
147
+ return;
148
+ }
149
+ const lwHandle = this.lwHandles.get(crdt_id);
150
+ if (lwHandle) {
151
+ try {
152
+ lwHandle.applyDelta(decodeLwwDelta(delta_bytes));
153
+ }
154
+ catch { /* stale */ }
155
+ return;
156
+ }
157
+ const prHandle = this.prHandles.get(crdt_id);
158
+ if (prHandle) {
159
+ try {
160
+ prHandle.applyDelta(decodePresenceDelta(delta_bytes));
161
+ }
162
+ catch { /* stale */ }
163
+ }
164
+ }
165
+ }
166
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Msgpack codec — wraps msgpackr with Meridian-specific conventions.
3
+ *
4
+ * The server uses rmp-serde with `to_vec_named` (named fields) so enum
5
+ * variants are encoded as `{ "VariantName": { ...fields } }` maps.
6
+ * msgpackr handles this transparently in JS land.
7
+ */
8
+ import { Effect } from "effect";
9
+ import { CodecError } from "./errors.js";
10
+ import { ServerMsg } from "./schema.js";
11
+ import type { ClientMsg, VectorClock } from "./schema.js";
12
+ /** Encode any value to msgpack bytes. */
13
+ export declare function encode(value: unknown): Uint8Array<ArrayBuffer>;
14
+ /** Decode msgpack bytes to a plain JS value (unsafe — no schema validation). */
15
+ export declare function decode(bytes: Uint8Array): unknown;
16
+ /**
17
+ * Encode a ClientMsg to a binary WebSocket frame.
18
+ * The msgpack encoding matches rmp-serde's "named" enum format:
19
+ * `{ Subscribe: { crdt_id: "foo" } }` → msgpack map with one key.
20
+ */
21
+ export declare function encodeClientMsg(msg: ClientMsg): Uint8Array;
22
+ /**
23
+ * Decode a binary WebSocket frame into a ServerMsg.
24
+ * Returns Effect<ServerMsg, CodecError>.
25
+ */
26
+ export declare function decodeServerMsg(bytes: Uint8Array): Effect.Effect<ServerMsg, CodecError>;
27
+ export declare function encodeVectorClock(vc: VectorClock): Uint8Array;
28
+ export declare function decodeVectorClock(bytes: Uint8Array): Effect.Effect<VectorClock, CodecError>;
29
+ //# sourceMappingURL=codec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codec.d.ts","sourceRoot":"","sources":["../src/codec.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAM1D,yCAAyC;AACzC,wBAAgB,MAAM,CAAC,KAAK,EAAE,OAAO,GAAG,UAAU,CAAC,WAAW,CAAC,CAE9D;AAED,gFAAgF;AAChF,wBAAgB,MAAM,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAEjD;AAMD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,SAAS,GAAG,UAAU,CAE1D;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,UAAU,CAAC,CAiBvF;AAMD,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,WAAW,GAAG,UAAU,CAG7D;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE,UAAU,CAAC,CAkB3F"}
package/dist/codec.js ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Msgpack codec — wraps msgpackr with Meridian-specific conventions.
3
+ *
4
+ * The server uses rmp-serde with `to_vec_named` (named fields) so enum
5
+ * variants are encoded as `{ "VariantName": { ...fields } }` maps.
6
+ * msgpackr handles this transparently in JS land.
7
+ */
8
+ import { pack, unpack } from "msgpackr";
9
+ import { Effect, Schema } from "effect";
10
+ import { CodecError } from "./errors.js";
11
+ import { ServerMsg } from "./schema.js";
12
+ // ---------------------------------------------------------------------------
13
+ // Low-level encode / decode
14
+ // ---------------------------------------------------------------------------
15
+ /** Encode any value to msgpack bytes. */
16
+ export function encode(value) {
17
+ return pack(value);
18
+ }
19
+ /** Decode msgpack bytes to a plain JS value (unsafe — no schema validation). */
20
+ export function decode(bytes) {
21
+ return unpack(bytes);
22
+ }
23
+ // ---------------------------------------------------------------------------
24
+ // Typed helpers for WebSocket frames
25
+ // ---------------------------------------------------------------------------
26
+ /**
27
+ * Encode a ClientMsg to a binary WebSocket frame.
28
+ * The msgpack encoding matches rmp-serde's "named" enum format:
29
+ * `{ Subscribe: { crdt_id: "foo" } }` → msgpack map with one key.
30
+ */
31
+ export function encodeClientMsg(msg) {
32
+ return encode(msg);
33
+ }
34
+ /**
35
+ * Decode a binary WebSocket frame into a ServerMsg.
36
+ * Returns Effect<ServerMsg, CodecError>.
37
+ */
38
+ export function decodeServerMsg(bytes) {
39
+ let raw;
40
+ try {
41
+ raw = unpack(bytes);
42
+ }
43
+ catch {
44
+ return Effect.fail(new CodecError({ message: "msgpack decode failed", raw: bytes }));
45
+ }
46
+ // Schema.decodeUnknown accepts `unknown` input — correct for runtime decode boundaries
47
+ return Schema.decodeUnknown(ServerMsg)(raw).pipe(Effect.mapError((parseError) => new CodecError({
48
+ message: `ServerMsg schema validation failed: ${parseError.message}`,
49
+ raw: bytes,
50
+ })));
51
+ }
52
+ // ---------------------------------------------------------------------------
53
+ // VectorClock <-> msgpack bytes
54
+ // ---------------------------------------------------------------------------
55
+ export function encodeVectorClock(vc) {
56
+ // Server expects { entries: { "client_id": version, ... } }
57
+ return encode({ entries: vc });
58
+ }
59
+ export function decodeVectorClock(bytes) {
60
+ let raw;
61
+ try {
62
+ raw = unpack(bytes);
63
+ }
64
+ catch {
65
+ return Effect.fail(new CodecError({ message: "VectorClock msgpack decode failed", raw: bytes }));
66
+ }
67
+ const entries = raw !== null && typeof raw === "object" && "entries" in raw
68
+ ? raw.entries
69
+ : {};
70
+ return Schema.decodeUnknown(Schema.Record({ key: Schema.String, value: Schema.Number }))(entries ?? {}).pipe(Effect.mapError((e) => new CodecError({ message: `VectorClock schema validation failed: ${e.message}`, raw: bytes })));
71
+ }
72
+ //# sourceMappingURL=codec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codec.js","sourceRoot":"","sources":["../src/codec.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAGxC,8EAA8E;AAC9E,4BAA4B;AAC5B,8EAA8E;AAE9E,yCAAyC;AACzC,MAAM,UAAU,MAAM,CAAC,KAAc;IACnC,OAAO,IAAI,CAAC,KAAK,CAA4B,CAAC;AAChD,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,MAAM,CAAC,KAAiB;IACtC,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;AACvB,CAAC;AAED,8EAA8E;AAC9E,qCAAqC;AACrC,8EAA8E;AAE9E;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,GAAc;IAC5C,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC;AACrB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,KAAiB;IAC/C,IAAI,GAAY,CAAC;IACjB,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,EAAE,OAAO,EAAE,uBAAuB,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;IACvF,CAAC;IAED,uFAAuF;IACvF,OAAO,MAAM,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,CAC9C,MAAM,CAAC,QAAQ,CAAC,CAAC,UAAU,EAAE,EAAE,CAC7B,IAAI,UAAU,CAAC;QACb,OAAO,EAAE,uCAAuC,UAAU,CAAC,OAAO,EAAE;QACpE,GAAG,EAAE,KAAK;KACX,CAAC,CACH,CACF,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,gCAAgC;AAChC,8EAA8E;AAE9E,MAAM,UAAU,iBAAiB,CAAC,EAAe;IAC/C,4DAA4D;IAC5D,OAAO,MAAM,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,CAAC;AACjC,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,KAAiB;IACjD,IAAI,GAAY,CAAC;IACjB,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,UAAU,CAAC,EAAE,OAAO,EAAE,mCAAmC,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;IACnG,CAAC;IAED,MAAM,OAAO,GAAG,GAAG,KAAK,IAAI,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,SAAS,IAAI,GAAG;QACzE,CAAC,CAAE,GAA4B,CAAC,OAAO;QACvC,CAAC,CAAC,EAAE,CAAC;IACP,OAAO,MAAM,CAAC,aAAa,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CACtF,OAAO,IAAI,EAAE,CACd,CAAC,IAAI,CACJ,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,EAAE,CACpB,IAAI,UAAU,CAAC,EAAE,OAAO,EAAE,yCAAyC,CAAC,CAAC,OAAO,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,CAC9F,CACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,36 @@
1
+ /**
2
+ * GCounter handle — increment-only counter.
3
+ *
4
+ * Local state is kept as a sparse map `{ client_id → count }`.
5
+ * `value()` returns the sum. Deltas are merged on incoming ServerMsg.Delta.
6
+ */
7
+ import type { WsTransport } from "../transport/websocket.js";
8
+ import type { GCounterDelta } from "../sync/delta.js";
9
+ export interface GCounterState {
10
+ counts: Record<string, number>;
11
+ }
12
+ export declare class GCounterHandle {
13
+ private state;
14
+ private readonly clientId;
15
+ private readonly crdtId;
16
+ private readonly ns;
17
+ private readonly transport;
18
+ /** Emits on every state change. */
19
+ private readonly listeners;
20
+ constructor(opts: {
21
+ ns: string;
22
+ crdtId: string;
23
+ clientId: number;
24
+ transport: WsTransport;
25
+ initial?: GCounterState;
26
+ });
27
+ value(): number;
28
+ counts(): Readonly<Record<string, number>>;
29
+ /** Subscribe to value changes. Returns an unsubscribe function. */
30
+ onChange(listener: (value: number) => void): () => void;
31
+ /** Increment by `amount` (must be > 0). */
32
+ increment(amount?: number): void;
33
+ applyDelta(delta: GCounterDelta): void;
34
+ private emit;
35
+ }
36
+ //# sourceMappingURL=gcounter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gcounter.d.ts","sourceRoot":"","sources":["../../src/crdt/gcounter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC;AAED,qBAAa,cAAc;IACzB,OAAO,CAAC,KAAK,CAAgB;IAC7B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAS;IAC5B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAc;IAExC,mCAAmC;IACnC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAsC;gBAEpD,IAAI,EAAE;QAChB,EAAE,EAAE,MAAM,CAAC;QACX,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,WAAW,CAAC;QACvB,OAAO,CAAC,EAAE,aAAa,CAAC;KACzB;IAUD,KAAK,IAAI,MAAM;IAIf,MAAM,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAI1C,mEAAmE;IACnE,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI;IAOvD,2CAA2C;IAC3C,SAAS,CAAC,MAAM,GAAE,MAAU,GAAG,IAAI;IAsBnC,UAAU,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;IActC,OAAO,CAAC,IAAI;CAMb"}