meridian-sdk 0.2.0 → 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 (85) 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
  85. package/tsconfig.json +2 -15
@@ -1,16 +1,5 @@
1
- /**
2
- * Presence handle — TTL-aware presence tracking.
3
- *
4
- * `heartbeat(data, ttlMs)` upserts the local client's entry.
5
- * `leave()` creates an explicit tombstone (ttl = 0).
6
- * Background GC on the server prunes expired entries every 5s.
7
- *
8
- * Pass a `schema` to get runtime validation of incoming delta data:
9
- * client.presence("id", Schema.Struct({ cursor: Schema.Tuple(Schema.Number, Schema.Number) }))
10
- */
11
-
12
1
  import { Schema } from "effect";
13
- import { encode, wallMsToBigInt } from "../codec.js";
2
+ import { encode } from "../codec.js";
14
3
  import type { WsTransport } from "../transport/websocket.js";
15
4
  import type { PresenceDelta, PresenceEntryDelta } from "../sync/delta.js";
16
5
 
@@ -20,6 +9,13 @@ export interface PresenceEntry<T> {
20
9
  expiresAtMs: number;
21
10
  }
22
11
 
12
+ /**
13
+ * Low-level handle for a presence channel CRDT.
14
+ *
15
+ * Obtained via `MeridianClient.presence()`. Prefer the `usePresence` React hook
16
+ * for component-level usage (it manages heartbeat timers and cleanup automatically);
17
+ * use this handle directly in non-React environments.
18
+ */
23
19
  export class PresenceHandle<T> {
24
20
  private readonly entries = new Map<string, PresenceEntry<T>>();
25
21
  private readonly crdtId: string;
@@ -41,9 +37,10 @@ export class PresenceHandle<T> {
41
37
  this.schema = opts.schema ?? null;
42
38
  }
43
39
 
44
- // ---- Read ----
45
-
46
- /** Returns all live entries (expired entries are filtered out). */
40
+ /**
41
+ * Returns all currently online entries — those whose TTL has not yet elapsed
42
+ * as of the call time.
43
+ */
47
44
  online(): PresenceEntry<T>[] {
48
45
  const now = Date.now();
49
46
  const live: PresenceEntry<T>[] = [];
@@ -53,17 +50,30 @@ export class PresenceHandle<T> {
53
50
  return live;
54
51
  }
55
52
 
53
+ /**
54
+ * Registers a listener that is called whenever the online entries change.
55
+ *
56
+ * @returns An unsubscribe function — call it to stop receiving updates.
57
+ */
56
58
  onChange(listener: (entries: PresenceEntry<T>[]) => void): () => void {
57
59
  this.listeners.add(listener);
58
60
  return () => { this.listeners.delete(listener); };
59
61
  }
60
62
 
61
- // ---- Write ----
62
-
63
- /** Send a presence heartbeat. Call every `ttlMs / 2` to stay alive. Default ttl: 30s. */
63
+ /**
64
+ * Broadcasts a heartbeat that marks the current client as online for `ttlMs`
65
+ * milliseconds (default `30 000`).
66
+ *
67
+ * Call this periodically (at least once per `ttlMs`) to remain visible to
68
+ * other clients. The `usePresence` hook manages this automatically when
69
+ * `opts.data` is provided.
70
+ *
71
+ * @param data - Arbitrary payload broadcast to other clients.
72
+ * @param ttlMs - Time-to-live in milliseconds before the entry is considered stale.
73
+ */
64
74
  heartbeat(data: T, ttlMs: number = 30_000): void {
65
75
  const wallMs = Date.now();
66
- const hlc = { wall_ms: wallMsToBigInt(wallMs), logical: 0, node_id: this.clientId };
76
+ const hlc = { wall_ms: wallMs, logical: 0, node_id: this.clientId };
67
77
 
68
78
  this.entries.set(String(this.clientId), {
69
79
  clientId: this.clientId,
@@ -82,10 +92,15 @@ export class PresenceHandle<T> {
82
92
  });
83
93
  }
84
94
 
85
- /** Explicitly leave — entry expires immediately for all peers. */
95
+ /**
96
+ * Marks the current client as offline and broadcasts a leave operation.
97
+ *
98
+ * After calling this, the client's entry is removed from the `online()` list
99
+ * on all peers. The `usePresence` hook calls this automatically on unmount.
100
+ */
86
101
  leave(): void {
87
102
  const wallMs = Date.now();
88
- const hlc = { wall_ms: wallMsToBigInt(wallMs), logical: 0, node_id: this.clientId };
103
+ const hlc = { wall_ms: wallMs, logical: 0, node_id: this.clientId };
89
104
 
90
105
  this.entries.delete(String(this.clientId));
91
106
  this.emit();
@@ -98,8 +113,6 @@ export class PresenceHandle<T> {
98
113
  });
99
114
  }
100
115
 
101
- // ---- Delta application ----
102
-
103
116
  applyDelta(delta: PresenceDelta): void {
104
117
  let changed = false;
105
118
  for (const [clientIdStr, entry] of Object.entries(delta.changes)) {
@@ -115,8 +128,6 @@ export class PresenceHandle<T> {
115
128
  if (changed) this.emit();
116
129
  }
117
130
 
118
- // ---- Internal ----
119
-
120
131
  private decode(raw: unknown): T {
121
132
  if (this.schema !== null) {
122
133
  return Schema.decodeUnknownSync(this.schema)(raw);
@@ -157,6 +168,6 @@ export class PresenceHandle<T> {
157
168
 
158
169
  private emit(): void {
159
170
  const live = this.online();
160
- for (const l of this.listeners) l(live);
171
+ for (const listener of this.listeners) listener(live);
161
172
  }
162
173
  }
package/src/errors.ts CHANGED
@@ -1,24 +1,11 @@
1
- /**
2
- * Typed errors for the Meridian SDK — all extend Data.TaggedError so they
3
- * can be matched with Effect.catchTag / Effect.catchAll.
4
- */
5
-
6
1
  import { Data } from "effect";
7
2
  import type { ErrorResponse } from "./schema.js";
8
3
 
9
- // ---------------------------------------------------------------------------
10
- // Codec
11
- // ---------------------------------------------------------------------------
12
-
13
4
  export class CodecError extends Data.TaggedError("CodecError")<{
14
5
  readonly message: string;
15
6
  readonly raw: Uint8Array;
16
7
  }> {}
17
8
 
18
- // ---------------------------------------------------------------------------
19
- // Auth / Token
20
- // ---------------------------------------------------------------------------
21
-
22
9
  export class TokenParseError extends Data.TaggedError("TokenParseError")<{
23
10
  readonly message: string;
24
11
  }> {}
@@ -27,10 +14,6 @@ export class TokenExpiredError extends Data.TaggedError("TokenExpiredError")<{
27
14
  readonly expiredAt: number;
28
15
  }> {}
29
16
 
30
- // ---------------------------------------------------------------------------
31
- // HTTP
32
- // ---------------------------------------------------------------------------
33
-
34
17
  export class HttpError extends Data.TaggedError("HttpError")<{
35
18
  readonly status: number;
36
19
  readonly body: ErrorResponse;
@@ -41,10 +24,6 @@ export class NetworkError extends Data.TaggedError("NetworkError")<{
41
24
  readonly cause?: unknown;
42
25
  }> {}
43
26
 
44
- // ---------------------------------------------------------------------------
45
- // WebSocket / Transport
46
- // ---------------------------------------------------------------------------
47
-
48
27
  export class TransportError extends Data.TaggedError("TransportError")<{
49
28
  readonly message: string;
50
29
  readonly cause?: unknown;
package/src/schema.ts CHANGED
@@ -7,34 +7,20 @@
7
7
 
8
8
  import { Schema } from "effect";
9
9
 
10
- // ---------------------------------------------------------------------------
11
- // Shared primitives
12
- // ---------------------------------------------------------------------------
13
-
14
- /**
15
- * Unix timestamp in milliseconds.
16
- * msgpackr decodes Rust u64 as BigInt — accept both for compatibility.
17
- */
10
+ /** Unix timestamp in milliseconds. Rust u64 decodes as bigint when > Number.MAX_SAFE_INTEGER — normalise to number. */
18
11
  export const TimestampMs = Schema.Union(Schema.Number, Schema.BigIntFromSelf.pipe(Schema.transform(
19
12
  Schema.Number,
20
13
  { decode: (n) => Number(n), encode: (n) => BigInt(n) },
21
14
  )));
22
15
  export type TimestampMs = number;
23
16
 
24
- /**
25
- * client_id / author — Rust u64, decoded by msgpackr as BigInt or number.
26
- * Accept both and normalise to number.
27
- */
17
+ /** client_id / author — Rust u64, may decode as bigint for large values. Normalise to number. */
28
18
  export const ClientId = Schema.Union(Schema.Number, Schema.BigIntFromSelf.pipe(Schema.transform(
29
19
  Schema.Number,
30
20
  { decode: (n) => Number(n), encode: (n) => BigInt(n) },
31
21
  )));
32
22
  export type ClientId = number;
33
23
 
34
- // ---------------------------------------------------------------------------
35
- // Auth / Token claims
36
- // ---------------------------------------------------------------------------
37
-
38
24
  export const Permissions = Schema.Struct({
39
25
  read: Schema.Array(Schema.String),
40
26
  write: Schema.Array(Schema.String),
@@ -50,18 +36,10 @@ export const TokenClaims = Schema.Struct({
50
36
  });
51
37
  export type TokenClaims = typeof TokenClaims.Type;
52
38
 
53
- // ---------------------------------------------------------------------------
54
- // Vector clock
55
- // ---------------------------------------------------------------------------
56
-
57
39
  /** BTreeMap<client_id, version> — matches server VectorClock.entries */
58
40
  export const VectorClock = Schema.Record({ key: Schema.String, value: Schema.Number });
59
41
  export type VectorClock = typeof VectorClock.Type;
60
42
 
61
- // ---------------------------------------------------------------------------
62
- // Client → Server WebSocket messages
63
- // ---------------------------------------------------------------------------
64
-
65
43
  export const ClientMsg = Schema.Union(
66
44
  Schema.Struct({ Subscribe: Schema.Struct({ crdt_id: Schema.String }) }),
67
45
  Schema.Struct({
@@ -73,22 +51,11 @@ export const ClientMsg = Schema.Union(
73
51
  );
74
52
  export type ClientMsg = typeof ClientMsg.Type;
75
53
 
76
- // ---------------------------------------------------------------------------
77
- // Server → Client WebSocket messages
78
- // ---------------------------------------------------------------------------
79
-
80
54
  export const ServerMsg = Schema.Union(
81
55
  Schema.Struct({
82
56
  Delta: Schema.Struct({
83
57
  crdt_id: Schema.String,
84
- // msgpackr may decode bin as Uint8Array or number[] depending on context — normalise to Uint8Array
85
- delta_bytes: Schema.Union(
86
- Schema.Uint8ArrayFromSelf,
87
- Schema.Array(Schema.Number).pipe(Schema.transform(Schema.Uint8ArrayFromSelf, {
88
- decode: (arr) => new Uint8Array(arr),
89
- encode: (u8) => Array.from(u8),
90
- })),
91
- ),
58
+ delta_bytes: Schema.Uint8ArrayFromSelf,
92
59
  }),
93
60
  }),
94
61
  Schema.Struct({ Ack: Schema.Struct({ seq: Schema.Number }) }),
@@ -98,10 +65,6 @@ export const ServerMsg = Schema.Union(
98
65
  );
99
66
  export type ServerMsg = typeof ServerMsg.Type;
100
67
 
101
- // ---------------------------------------------------------------------------
102
- // CRDT value types (returned by GET /v1/namespaces/:ns/crdts/:id)
103
- // ---------------------------------------------------------------------------
104
-
105
68
  export const GCounterValue = Schema.Struct({
106
69
  value: Schema.Number,
107
70
  counts: Schema.Record({ key: Schema.String, value: Schema.Number }),
@@ -136,10 +99,6 @@ export const PresenceValue = Schema.Struct({
136
99
  });
137
100
  export type PresenceValue = typeof PresenceValue.Type;
138
101
 
139
- // ---------------------------------------------------------------------------
140
- // HTTP response envelopes
141
- // ---------------------------------------------------------------------------
142
-
143
102
  /** GET /v1/namespaces/:ns/crdts/:id — server returns the raw JSON value directly. */
144
103
  export const CrdtGetResponse = Schema.Unknown;
145
104
  export type CrdtGetResponse = unknown;
package/src/sync/clock.ts CHANGED
@@ -1,25 +1,10 @@
1
- /**
2
- * Client-side VectorClock.
3
- *
4
- * Tracks the latest version seen per client_id. Used to request
5
- * delta-since on reconnect (Sync message).
6
- *
7
- * Persisted to localStorage (browser) or a JSON file (Node/Bun) via
8
- * the optional `ClockStorage` adapter injected at construction.
9
- */
10
-
11
1
  import type { VectorClock } from "../schema.js";
12
2
 
13
- // ---------------------------------------------------------------------------
14
- // ClockStorage adapter
15
- // ---------------------------------------------------------------------------
16
-
17
3
  export interface ClockStorage {
18
4
  load(key: string): VectorClock | null;
19
5
  save(key: string, vc: VectorClock): void;
20
6
  }
21
7
 
22
- /** In-memory only — useful for tests and SSR. */
23
8
  export const memoryStorage: ClockStorage = (() => {
24
9
  const store = new Map<string, VectorClock>();
25
10
  return {
@@ -28,39 +13,26 @@ export const memoryStorage: ClockStorage = (() => {
28
13
  };
29
14
  })();
30
15
 
31
- /** localStorage-backed storage (browser). Falls back to memory if unavailable. */
32
- export function localStorageAdapter(prefix = "meridian:"): ClockStorage {
33
- return {
34
- load(key) {
35
- try {
36
- const raw = localStorage.getItem(`${prefix}${key}`);
37
- if (raw === null) return null;
38
- return JSON.parse(raw) as VectorClock;
39
- } catch {
40
- return null;
41
- }
42
- },
43
- save(key, vc) {
44
- try {
45
- localStorage.setItem(`${prefix}${key}`, JSON.stringify(vc));
46
- } catch {
47
- // quota exceeded — ignore
48
- }
49
- },
50
- };
51
- }
52
-
53
- // ---------------------------------------------------------------------------
54
- // VectorClockTracker
55
- // ---------------------------------------------------------------------------
16
+ export const localStorageAdapter = (prefix = "meridian:"): ClockStorage => ({
17
+ load(key) {
18
+ try {
19
+ const raw = localStorage.getItem(`${prefix}${key}`);
20
+ if (raw === null) return null;
21
+ return JSON.parse(raw) as VectorClock;
22
+ } catch {
23
+ return null;
24
+ }
25
+ },
26
+ save(key, vc) {
27
+ try {
28
+ localStorage.setItem(`${prefix}${key}`, JSON.stringify(vc));
29
+ } catch {
30
+ }
31
+ },
32
+ });
56
33
 
57
- /**
58
- * Mutable wrapper around VectorClock that tracks seen versions and
59
- * optionally persists them across page reloads.
60
- */
61
34
  export class VectorClockTracker {
62
- // Mutable internal copy — VectorClock (Schema type) is readonly, so we
63
- // keep a plain mutable Record internally and expose snapshots.
35
+ // HACK: VectorClock (Schema type) is readonly, so we keep a plain mutable Record internally and expose snapshots.
64
36
  private readonly clock: Record<string, number>;
65
37
  private readonly storage: ClockStorage | null;
66
38
  private readonly storageKey: string;
@@ -78,7 +50,6 @@ export class VectorClockTracker {
78
50
  this.clock = { ...(opts.initial ?? {}), ...(persisted ?? {}) };
79
51
  }
80
52
 
81
- /** Observe a version from a peer. Updates clock if newer. */
82
53
  observe(clientId: string, version: number): void {
83
54
  const current = this.clock[clientId] ?? 0;
84
55
  if (version > current) {
@@ -87,7 +58,6 @@ export class VectorClockTracker {
87
58
  }
88
59
  }
89
60
 
90
- /** Merge an entire remote VectorClock (take max per entry). */
91
61
  merge(remote: VectorClock): void {
92
62
  let changed = false;
93
63
  for (const [id, ver] of Object.entries(remote)) {
@@ -102,12 +72,10 @@ export class VectorClockTracker {
102
72
  }
103
73
  }
104
74
 
105
- /** Returns an immutable snapshot suitable for the Sync message. */
106
75
  snapshot(): VectorClock {
107
76
  return { ...this.clock };
108
77
  }
109
78
 
110
- /** Reset to empty (e.g. when the namespace is cleared). */
111
79
  reset(): void {
112
80
  for (const key of Object.keys(this.clock)) {
113
81
  delete this.clock[key];
package/src/sync/delta.ts CHANGED
@@ -1,40 +1,21 @@
1
- /**
2
- * Delta application helpers.
3
- *
4
- * The server sends deltas as opaque msgpack bytes inside `ServerMsg.Delta`.
5
- * Each CRDT type decodes its own delta format matching Rust serde output.
6
- *
7
- * Field names mirror the Rust structs exactly (rmp-serde named encoding).
8
- */
1
+ import { decode } from "@msgpack/msgpack";
9
2
 
10
- import { unpack } from "msgpackr";
11
-
12
- // ---------------------------------------------------------------------------
13
- // GCounter delta
14
- // ---------------------------------------------------------------------------
15
-
16
- /** GCounterDelta { counters: BTreeMap<u64, u64> } */
17
3
  export interface GCounterDelta {
18
4
  counters: Record<string, number>;
19
5
  }
20
6
 
21
- export function decodeGCounterDelta(bytes: Uint8Array): GCounterDelta {
22
- const raw = unpack(bytes) as { counters?: Record<string, number> };
7
+ export const decodeGCounterDelta = (bytes: Uint8Array): GCounterDelta => {
8
+ const raw = decode(bytes) as { counters?: Record<string, number> };
23
9
  return { counters: raw.counters ?? {} };
24
- }
10
+ };
25
11
 
26
- // ---------------------------------------------------------------------------
27
- // PNCounter delta
28
- // ---------------------------------------------------------------------------
29
-
30
- /** PNCounterDelta { pos: Option<GCounterDelta>, neg: Option<GCounterDelta> } */
31
12
  export interface PNCounterDelta {
32
13
  pos: GCounterDelta | null;
33
14
  neg: GCounterDelta | null;
34
15
  }
35
16
 
36
- export function decodePNCounterDelta(bytes: Uint8Array): PNCounterDelta {
37
- const raw = unpack(bytes) as {
17
+ export const decodePNCounterDelta = (bytes: Uint8Array): PNCounterDelta => {
18
+ const raw = decode(bytes) as {
38
19
  pos?: { counters?: Record<string, number> } | null;
39
20
  neg?: { counters?: Record<string, number> } | null;
40
21
  };
@@ -42,57 +23,39 @@ export function decodePNCounterDelta(bytes: Uint8Array): PNCounterDelta {
42
23
  pos: raw.pos ? { counters: raw.pos.counters ?? {} } : null,
43
24
  neg: raw.neg ? { counters: raw.neg.counters ?? {} } : null,
44
25
  };
45
- }
46
-
47
- // ---------------------------------------------------------------------------
48
- // ORSet delta
49
- // ---------------------------------------------------------------------------
26
+ };
50
27
 
51
- /**
52
- * ORSetDelta { adds: HashMap<String, HashSet<Uuid>>, removes: HashMap<String, HashSet<Uuid>> }
53
- * Uuid bytes are decoded by msgpackr as Uint8Array or number[].
54
- */
55
28
  export interface ORSetDelta {
56
29
  adds: Record<string, Uint8Array[]>;
57
30
  removes: Record<string, Uint8Array[]>;
58
31
  }
59
32
 
60
- export function decodeORSetDelta(bytes: Uint8Array): ORSetDelta {
61
- const raw = unpack(bytes) as {
33
+ export const decodeORSetDelta = (bytes: Uint8Array): ORSetDelta => {
34
+ const raw = decode(bytes) as {
62
35
  adds?: Record<string, unknown[]>;
63
36
  removes?: Record<string, unknown[]>;
64
37
  };
65
- const toBytes = (v: unknown): Uint8Array =>
66
- v instanceof Uint8Array ? v : new Uint8Array(v as number[]);
67
- const decodeMap = (m?: Record<string, unknown[]>) =>
68
- Object.fromEntries(Object.entries(m ?? {}).map(([k, tags]) => [k, tags.map(toBytes)]));
38
+ const toBytes = (value: unknown): Uint8Array =>
39
+ value instanceof Uint8Array ? value : new Uint8Array(value as number[]);
40
+ const decodeMap = (map?: Record<string, unknown[]>) =>
41
+ Object.fromEntries(Object.entries(map ?? {}).map(([key, tags]) => [key, tags.map(toBytes)]));
69
42
  return { adds: decodeMap(raw.adds), removes: decodeMap(raw.removes) };
70
- }
71
-
72
- // ---------------------------------------------------------------------------
73
- // LWW Register delta
74
- // ---------------------------------------------------------------------------
43
+ };
75
44
 
76
- /** LwwEntry { value, hlc: HybridLogicalClock, author: u64 } */
77
45
  export interface LwwEntry {
78
46
  value: unknown;
79
47
  hlc: { wall_ms: number | bigint; logical: number; node_id: number | bigint };
80
48
  author: number | bigint;
81
49
  }
82
50
 
83
- /** LwwDelta { entry: Option<LwwEntry> } */
84
51
  export interface LwwDelta {
85
52
  entry: LwwEntry | null;
86
53
  }
87
54
 
88
- export function decodeLwwDelta(bytes: Uint8Array): LwwDelta {
89
- const raw = unpack(bytes) as { entry?: LwwEntry | null };
55
+ export const decodeLwwDelta = (bytes: Uint8Array): LwwDelta => {
56
+ const raw = decode(bytes) as { entry?: LwwEntry | null };
90
57
  return { entry: raw.entry ?? null };
91
- }
92
-
93
- // ---------------------------------------------------------------------------
94
- // Presence delta
95
- // ---------------------------------------------------------------------------
58
+ };
96
59
 
97
60
  export interface PresenceEntryDelta {
98
61
  data: unknown;
@@ -100,12 +63,11 @@ export interface PresenceEntryDelta {
100
63
  ttl_ms: number | bigint;
101
64
  }
102
65
 
103
- /** PresenceDelta — changes per client_id */
104
66
  export interface PresenceDelta {
105
67
  changes: Record<string, PresenceEntryDelta | null>;
106
68
  }
107
69
 
108
- export function decodePresenceDelta(bytes: Uint8Array): PresenceDelta {
109
- const raw = unpack(bytes) as { changes?: Record<string, PresenceEntryDelta | null> };
70
+ export const decodePresenceDelta = (bytes: Uint8Array): PresenceDelta => {
71
+ const raw = decode(bytes) as { changes?: Record<string, PresenceEntryDelta | null> };
110
72
  return { changes: raw.changes ?? {} };
111
- }
73
+ };
@@ -1,10 +1,3 @@
1
- /**
2
- * HTTP transport — REST API client for Meridian.
3
- *
4
- * All methods return Effect<T, HttpError | NetworkError> so callers
5
- * can handle network failures and server errors as typed values.
6
- */
7
-
8
1
  import { encode, decode } from "../codec.js";
9
2
  import { Effect, Schema } from "effect";
10
3
  import { HttpError, NetworkError } from "../errors.js";
@@ -13,27 +6,16 @@ import {
13
6
  CrdtOpResponse,
14
7
  TokenIssueResponse,
15
8
  ErrorResponse,
16
- VectorClock,
9
+ type VectorClock,
17
10
  } from "../schema.js";
18
11
  import type { Permissions } from "../schema.js";
19
12
 
20
- // ---------------------------------------------------------------------------
21
- // Config
22
- // ---------------------------------------------------------------------------
23
-
24
13
  export interface HttpClientConfig {
25
- /** Base URL of the Meridian server, e.g. "http://localhost:3000" */
26
14
  baseUrl: string;
27
- /** Bearer token. Can be updated after construction (e.g. after refresh). */
28
15
  token: string;
29
- /** Request timeout in ms. Default: 10_000 */
30
16
  timeoutMs?: number;
31
17
  }
32
18
 
33
- // ---------------------------------------------------------------------------
34
- // HttpClient
35
- // ---------------------------------------------------------------------------
36
-
37
19
  export class HttpClient {
38
20
  private readonly baseUrl: string;
39
21
  private readonly timeoutMs: number;
@@ -45,19 +27,14 @@ export class HttpClient {
45
27
  this.timeoutMs = config.timeoutMs ?? 10_000;
46
28
  }
47
29
 
48
- // ---- CRDT REST ----
49
-
50
- /** GET /v1/namespaces/:ns/crdts/:id */
51
30
  getCrdt(ns: string, id: string): Effect.Effect<CrdtGetResponse, HttpError | NetworkError> {
52
31
  return this.request(CrdtGetResponse, "GET", `/v1/namespaces/${ns}/crdts/${id}`);
53
32
  }
54
33
 
55
- /** POST /v1/namespaces/:ns/crdts/:id/ops */
56
34
  postOp(ns: string, id: string, op: unknown): Effect.Effect<CrdtOpResponse, HttpError | NetworkError> {
57
35
  return this.request(CrdtOpResponse, "POST", `/v1/namespaces/${ns}/crdts/${id}/ops`, op);
58
36
  }
59
37
 
60
- /** GET /v1/namespaces/:ns/crdts/:id/sync?since=<base64url(msgpack(vc))> */
61
38
  syncCrdt(
62
39
  ns: string,
63
40
  id: string,
@@ -71,7 +48,6 @@ export class HttpClient {
71
48
  return this.request(CrdtGetResponse, "GET", path);
72
49
  }
73
50
 
74
- /** POST /v1/namespaces/:ns/tokens (admin-only) */
75
51
  issueToken(
76
52
  ns: string,
77
53
  opts: { client_id: number; ttl_ms: number; permissions: Permissions },
@@ -79,8 +55,6 @@ export class HttpClient {
79
55
  return this.request(TokenIssueResponse, "POST", `/v1/namespaces/${ns}/tokens`, opts);
80
56
  }
81
57
 
82
- // ---- Internal ----
83
-
84
58
  private request<A, I>(
85
59
  responseSchema: Schema.Schema<A, I>,
86
60
  method: string,
@@ -140,14 +114,10 @@ export class HttpClient {
140
114
  }
141
115
  }
142
116
 
143
- // ---------------------------------------------------------------------------
144
- // base64url (no-padding) encoder
145
- // ---------------------------------------------------------------------------
146
-
147
- function base64urlEncode(bytes: Uint8Array): string {
117
+ const base64urlEncode = (bytes: Uint8Array): string => {
148
118
  let binary = "";
149
119
  for (const b of bytes) {
150
120
  binary += String.fromCharCode(b);
151
121
  }
152
122
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
153
- }
123
+ };