meridian-sdk 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +52 -0
- package/dist/auth/permissions.d.ts +1 -0
- package/dist/auth/permissions.d.ts.map +1 -1
- package/dist/auth/permissions.js +1 -1
- package/dist/auth/permissions.js.map +1 -1
- package/dist/client.d.ts +54 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +147 -42
- package/dist/client.js.map +1 -1
- package/dist/crdt/lwwregister.d.ts +4 -0
- package/dist/crdt/lwwregister.d.ts.map +1 -1
- package/dist/crdt/lwwregister.js +7 -1
- package/dist/crdt/lwwregister.js.map +1 -1
- package/dist/crdt/presence.d.ts +4 -0
- package/dist/crdt/presence.d.ts.map +1 -1
- package/dist/crdt/presence.js +7 -3
- package/dist/crdt/presence.js.map +1 -1
- package/dist/crypto/aes-gcm.d.ts +24 -0
- package/dist/crypto/aes-gcm.d.ts.map +1 -0
- package/dist/crypto/aes-gcm.js +49 -0
- package/dist/crypto/aes-gcm.js.map +1 -0
- package/dist/crypto/ed25519.d.ts +30 -0
- package/dist/crypto/ed25519.d.ts.map +1 -0
- package/dist/crypto/ed25519.js +68 -0
- package/dist/crypto/ed25519.js.map +1 -0
- package/dist/crypto/index.d.ts +5 -0
- package/dist/crypto/index.d.ts.map +1 -0
- package/dist/crypto/index.js +3 -0
- package/dist/crypto/index.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/schema.d.ts +4 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +4 -0
- package/dist/schema.js.map +1 -1
- package/dist/sync/tab-sync.d.ts +22 -0
- package/dist/sync/tab-sync.d.ts.map +1 -0
- package/dist/sync/tab-sync.js +32 -0
- package/dist/sync/tab-sync.js.map +1 -0
- package/dist/transport/websocket.d.ts +8 -0
- package/dist/transport/websocket.d.ts.map +1 -1
- package/dist/transport/websocket.js +25 -3
- package/dist/transport/websocket.js.map +1 -1
- package/package.json +1 -1
- package/src/auth/permissions.ts +1 -1
- package/src/client.ts +180 -41
- package/src/crdt/lwwregister.ts +14 -1
- package/src/crdt/presence.ts +14 -3
- package/src/crypto/aes-gcm.ts +65 -0
- package/src/crypto/ed25519.ts +89 -0
- package/src/crypto/index.ts +15 -0
- package/src/index.ts +20 -0
- package/src/schema.ts +4 -0
- package/src/sync/tab-sync.ts +40 -0
- package/src/transport/websocket.ts +31 -3
- package/test/crypto.test.ts +150 -0
- package/test/tab-sync.test.ts +164 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# meridian-sdk
|
|
2
2
|
|
|
3
|
+
## 1.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 664b1e4: Add E2E encryption, BFT signing, and cross-SDK feature parity.
|
|
8
|
+
|
|
9
|
+
- AES-GCM-256 envelope encryption and Ed25519 BFT signing in the TypeScript SDK
|
|
10
|
+
- New `packages/python/` — `meridian-crdt` Python package with full CRDT support (GCounter, PNCounter, LwwRegister, Presence), optional AES-GCM-256 encryption and Ed25519 signing, asyncio-native transport with auto-reconnect
|
|
11
|
+
- Rust client SDK (`meridian-client`): AES-GCM-256 encryption and Ed25519 BFT signing on LwwRegister and Presence handles (`--features crypto`), HTTP client for REST endpoints (`--features http`), auth token parsing and permission checks
|
|
12
|
+
- CRDT compactor background task for RGA and Tree tombstone cleanup
|
|
13
|
+
|
|
3
14
|
## 1.6.0
|
|
4
15
|
|
|
5
16
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -83,6 +83,9 @@ await Effect.runPromise(
|
|
|
83
83
|
| `namespace` | `string` | Namespace to connect to |
|
|
84
84
|
| `token` | `string` | Meridian token |
|
|
85
85
|
| `autoConnect` | `boolean?` | Open WebSocket immediately (default: `true`) |
|
|
86
|
+
| `offline` | `boolean?` | Start without connecting — operate from cache, call `client.connect()` later |
|
|
87
|
+
| `persistence` | `PersistenceConfig?` | Persist CRDT snapshots and the pending op queue across page loads |
|
|
88
|
+
| `tabSync` | `boolean?` | Broadcast deltas to other tabs in the same namespace via `BroadcastChannel` |
|
|
86
89
|
|
|
87
90
|
### CRDT handles
|
|
88
91
|
|
|
@@ -203,6 +206,55 @@ unsubDelta();
|
|
|
203
206
|
|
|
204
207
|
The queue holds up to 500 ops. If the limit is reached, the oldest op is dropped to make room for the newest.
|
|
205
208
|
|
|
209
|
+
### Persistence (offline-first)
|
|
210
|
+
|
|
211
|
+
Enable persistence to cache CRDT state across page loads and survive disconnections:
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
import {
|
|
215
|
+
MeridianClient,
|
|
216
|
+
indexedDbStateStorage,
|
|
217
|
+
localStorageSyncOpsAdapter,
|
|
218
|
+
} from "meridian-sdk";
|
|
219
|
+
|
|
220
|
+
const client = await Effect.runPromise(
|
|
221
|
+
MeridianClient.create({
|
|
222
|
+
url: "http://localhost:3000",
|
|
223
|
+
namespace: "my-room",
|
|
224
|
+
token,
|
|
225
|
+
persistence: {
|
|
226
|
+
state: indexedDbStateStorage(), // CRDT snapshots → IndexedDB
|
|
227
|
+
ops: localStorageSyncOpsAdapter(), // pending op queue → localStorage (sync, survives tab close)
|
|
228
|
+
},
|
|
229
|
+
})
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
const views = client.gcounter("gc:views");
|
|
233
|
+
const cart = client.orset("or:cart");
|
|
234
|
+
await client.waitForRestore(); // wait for IndexedDB reads to complete
|
|
235
|
+
|
|
236
|
+
console.log(views.value()); // populated from cache before the WebSocket connects
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
The pending op queue is also flushed when the tab becomes hidden (`visibilitychange`), covering most crash and background-kill scenarios.
|
|
240
|
+
|
|
241
|
+
### Multi-tab sync
|
|
242
|
+
|
|
243
|
+
Pass `tabSync: true` to broadcast CRDT deltas to other open tabs in the same origin and namespace via the [BroadcastChannel API](https://developer.mozilla.org/en-US/docs/Web/API/BroadcastChannel):
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
const client = await Effect.runPromise(
|
|
247
|
+
MeridianClient.create({
|
|
248
|
+
url: "http://localhost:3000",
|
|
249
|
+
namespace: "my-room",
|
|
250
|
+
token,
|
|
251
|
+
tabSync: true,
|
|
252
|
+
})
|
|
253
|
+
);
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
When Tab A receives a delta from the server it immediately forwards it to Tab B — Tab B applies it without waiting for its own server round-trip. All CRDTs are convergent so applying the same delta twice is safe.
|
|
257
|
+
|
|
206
258
|
### Op latency
|
|
207
259
|
|
|
208
260
|
`client.getLatencyStats()` returns P50 and P99 round-trip latency in milliseconds, computed from the last 128 acknowledged ops. Returns `null` if fewer than 2 samples have been collected.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"permissions.d.ts","sourceRoot":"","sources":["../../src/auth/permissions.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAa,MAAM,cAAc,CAAC;AAE3D,eAAO,MAAM,OAAO;IAClB,yDAAyD;;;;;;;;;;;;;;;;CA8BjD,CAAC;AAEX,MAAM,MAAM,MAAM,GAAG,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"permissions.d.ts","sourceRoot":"","sources":["../../src/auth/permissions.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAa,MAAM,cAAc,CAAC;AAE3D,eAAO,MAAM,OAAO;IAClB,yDAAyD;;;;;;;;;;;;;;;;CA8BjD,CAAC;AAEX,MAAM,MAAM,MAAM,GAAG,MAAM,CAAC;AAE5B,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAcjE;AA4BD;;;;;;;GAOG;AACH,wBAAgB,OAAO,CACrB,WAAW,EAAE,WAAW,EACxB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,KAAK,SAAa,GACjB,OAAO,CAOT;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CACtB,WAAW,EAAE,WAAW,EACxB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,MAAM,GAAE,MAAoB,EAC5B,KAAK,SAAa,GACjB,OAAO,CAOT"}
|
package/dist/auth/permissions.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"permissions.js","sourceRoot":"","sources":["../../src/auth/permissions.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,MAAM,CAAC,MAAM,OAAO,GAAG;IACrB,yDAAyD;IACzD,GAAG,EAAE,CAAC;IAEN,uBAAuB;IACvB,YAAY,EAAE,WAAW;IACzB,YAAY,EAAE,WAAW;IACzB,YAAY,EAAE,WAAW;IAEzB,QAAQ;IACR,MAAM,EAAK,WAAW;IACtB,SAAS,EAAE,WAAW;IAEtB,eAAe;IACf,OAAO,EAAE,WAAW;IAEpB,WAAW;IACX,eAAe,EAAE,WAAW;IAE5B,MAAM;IACN,UAAU,EAAE,WAAW;IACvB,UAAU,EAAE,WAAW;IAEvB,OAAO;IACP,QAAQ,EAAK,WAAW;IACxB,SAAS,EAAI,WAAW;IACxB,WAAW,EAAE,WAAW;IACxB,WAAW,EAAE,WAAW;IAExB,UAAU;IACV,SAAS,EAAE,WAAW;CACd,CAAC;AAIX,
|
|
1
|
+
{"version":3,"file":"permissions.js","sourceRoot":"","sources":["../../src/auth/permissions.ts"],"names":[],"mappings":"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"}
|
package/dist/client.d.ts
CHANGED
|
@@ -105,6 +105,43 @@ export interface MeridianClientConfig {
|
|
|
105
105
|
offline?: boolean;
|
|
106
106
|
/** Persistence configuration for offline-first support. */
|
|
107
107
|
persistence?: PersistenceConfig;
|
|
108
|
+
/**
|
|
109
|
+
* If `true`, broadcast CRDT deltas to other tabs in the same origin and namespace
|
|
110
|
+
* via `BroadcastChannel`. Other tabs with the same namespace apply updates instantly
|
|
111
|
+
* without a server round-trip. Safe to enable — re-applying a delta is idempotent.
|
|
112
|
+
*/
|
|
113
|
+
tabSync?: boolean;
|
|
114
|
+
/**
|
|
115
|
+
* Client-side AES-256-GCM encryption keys, keyed by CRDT ID pattern (glob supported).
|
|
116
|
+
* Matching CRDTs encrypt their value fields before sending — the server stores and
|
|
117
|
+
* forwards ciphertext and never sees plaintext.
|
|
118
|
+
*
|
|
119
|
+
* Supported CRDT types: `LwwRegister`, `Presence`.
|
|
120
|
+
* Not supported: `GCounter`, `PNCounter`, `RGA` (numeric/structural merge requires plaintext).
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```ts
|
|
124
|
+
* import { importAesGcmKey } from "meridian-sdk";
|
|
125
|
+
* const key = await importAesGcmKey(base64Key);
|
|
126
|
+
* MeridianClient.create({ ..., encryption: { "lw:profile": key, "pr:room-*": key2 } });
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
encryption?: Record<string, CryptoKey>;
|
|
130
|
+
/**
|
|
131
|
+
* If `true`, generate (or load from `localStorage`) a per-client Ed25519 keypair and
|
|
132
|
+
* sign every outgoing op before sending.
|
|
133
|
+
*
|
|
134
|
+
* The server verifies the signature against `client_pubkey` embedded in the token.
|
|
135
|
+
* This provides Byzantine fault tolerance in cluster mode: a compromised node cannot
|
|
136
|
+
* forge ops that appear to originate from legitimate clients.
|
|
137
|
+
*
|
|
138
|
+
* **Setup**: when issuing the token (via your app server), include `client_pubkey`
|
|
139
|
+
* (base64url-encoded 32-byte public key) in the token issuance request. Obtain the
|
|
140
|
+
* public key from `client.publicKeyBytes` after `create()` completes.
|
|
141
|
+
*
|
|
142
|
+
* Requires Chrome 113+, Firefox 130+, or Node.js 20+.
|
|
143
|
+
*/
|
|
144
|
+
signing?: boolean;
|
|
108
145
|
}
|
|
109
146
|
/**
|
|
110
147
|
* The main entry point for the Meridian real-time CRDT SDK.
|
|
@@ -138,6 +175,12 @@ export declare class MeridianClient {
|
|
|
138
175
|
readonly claims: TokenClaims;
|
|
139
176
|
private readonly transport;
|
|
140
177
|
readonly http: HttpClient;
|
|
178
|
+
/**
|
|
179
|
+
* Raw 32-byte Ed25519 public key for this client, or `null` if `signing` is disabled.
|
|
180
|
+
* Pass this (base64url-encoded) to your app server when issuing tokens so the server
|
|
181
|
+
* can verify op signatures.
|
|
182
|
+
*/
|
|
183
|
+
readonly publicKeyBytes: Uint8Array | null;
|
|
141
184
|
private readonly gcHandles;
|
|
142
185
|
private readonly pnHandles;
|
|
143
186
|
private readonly orHandles;
|
|
@@ -156,6 +199,8 @@ export declare class MeridianClient {
|
|
|
156
199
|
private readonly snapStore;
|
|
157
200
|
private readonly opsStore;
|
|
158
201
|
private readonly restorePromises;
|
|
202
|
+
private readonly tabSyncChannel;
|
|
203
|
+
private readonly encKeys;
|
|
159
204
|
private constructor();
|
|
160
205
|
/** Explicitly connect to the server. Use when `autoConnect: false` or `offline: true`. */
|
|
161
206
|
connect(): void;
|
|
@@ -178,6 +223,7 @@ export declare class MeridianClient {
|
|
|
178
223
|
private scheduleSnapRestore;
|
|
179
224
|
private saveSnap;
|
|
180
225
|
private readonly flushOpsToStorage;
|
|
226
|
+
private readonly onVisibilityChange;
|
|
181
227
|
/**
|
|
182
228
|
* Creates and validates a new `MeridianClient` from the supplied configuration.
|
|
183
229
|
*
|
|
@@ -200,6 +246,8 @@ export declare class MeridianClient {
|
|
|
200
246
|
* ```
|
|
201
247
|
*/
|
|
202
248
|
static create(config: MeridianClientConfig): Effect.Effect<MeridianClient, TokenParseError | TokenExpiredError>;
|
|
249
|
+
private resolveEncKey;
|
|
250
|
+
private encryptFnFor;
|
|
203
251
|
/**
|
|
204
252
|
* Returns a handle for a grow-only counter (GCounter) CRDT.
|
|
205
253
|
*
|
|
@@ -480,6 +528,12 @@ export declare class MeridianClient {
|
|
|
480
528
|
private resubscribeLiveQueries;
|
|
481
529
|
private notifyAnyChange;
|
|
482
530
|
private handleServerMsg;
|
|
531
|
+
/**
|
|
532
|
+
* Apply a CRDT delta to the appropriate local handle.
|
|
533
|
+
* Called from the server message handler and from tab-sync broadcasts.
|
|
534
|
+
* When `broadcast` is true, the delta is forwarded to other tabs via BroadcastChannel.
|
|
535
|
+
*/
|
|
536
|
+
private applyDelta;
|
|
483
537
|
private notifyDelta;
|
|
484
538
|
}
|
|
485
539
|
//# sourceMappingURL=client.d.ts.map
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC7C,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC3E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AAExD,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/client.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Effect } from "effect";
|
|
2
2
|
import { WsTransport } from "./transport/websocket.js";
|
|
3
|
+
import { TabSync } from "./sync/tab-sync.js";
|
|
3
4
|
import { snapshotFromHandle, snapshotToBytes, bytesToSnapshot, restoreSnapshotToHandle, } from "./persistence/snapshot.js";
|
|
4
5
|
import { encode, decode } from "./codec.js";
|
|
5
6
|
import { HttpClient } from "./transport/http.js";
|
|
@@ -14,7 +15,9 @@ import { RGAHandle } from "./crdt/rga.js";
|
|
|
14
15
|
import { TreeHandle } from "./crdt/tree.js";
|
|
15
16
|
import { decodeGCounterDelta, decodePNCounterDelta, decodeORSetDelta, decodeLwwDelta, decodePresenceDelta, decodeCRDTMapDelta, decodeRGADelta, decodeTreeDelta, } from "./sync/delta.js";
|
|
16
17
|
import { parseAndValidateToken } from "./auth/token.js";
|
|
17
|
-
import { canRead as evalCanRead, canWrite as evalCanWrite } from "./auth/permissions.js";
|
|
18
|
+
import { canRead as evalCanRead, canWrite as evalCanWrite, globMatch } from "./auth/permissions.js";
|
|
19
|
+
import { encryptJson, decryptJson, isEncryptedValue } from "./crypto/aes-gcm.js";
|
|
20
|
+
import { signOp, loadOrGenerateKeypair } from "./crypto/ed25519.js";
|
|
18
21
|
/**
|
|
19
22
|
* The main entry point for the Meridian real-time CRDT SDK.
|
|
20
23
|
*
|
|
@@ -47,6 +50,12 @@ export class MeridianClient {
|
|
|
47
50
|
claims;
|
|
48
51
|
transport;
|
|
49
52
|
http;
|
|
53
|
+
/**
|
|
54
|
+
* Raw 32-byte Ed25519 public key for this client, or `null` if `signing` is disabled.
|
|
55
|
+
* Pass this (base64url-encoded) to your app server when issuing tokens so the server
|
|
56
|
+
* can verify op signatures.
|
|
57
|
+
*/
|
|
58
|
+
publicKeyBytes;
|
|
50
59
|
// HACK: Generic params are erased at storage level; factories restore them via typed get+cast on retrieval.
|
|
51
60
|
gcHandles = new Map();
|
|
52
61
|
pnHandles = new Map();
|
|
@@ -68,10 +77,14 @@ export class MeridianClient {
|
|
|
68
77
|
snapStore;
|
|
69
78
|
opsStore;
|
|
70
79
|
restorePromises = new Set();
|
|
71
|
-
|
|
80
|
+
tabSyncChannel;
|
|
81
|
+
encKeys;
|
|
82
|
+
constructor(config, claims, keypair) {
|
|
72
83
|
this.namespace = config.namespace;
|
|
73
84
|
this.claims = claims;
|
|
74
85
|
this.clientId = claims.client_id;
|
|
86
|
+
this.publicKeyBytes = keypair?.publicKeyBytes ?? null;
|
|
87
|
+
this.encKeys = new Map(Object.entries(config.encryption ?? {}));
|
|
75
88
|
this.snapStore = config.persistence?.state ?? null;
|
|
76
89
|
this.opsStore = config.persistence?.ops ?? null;
|
|
77
90
|
const httpBase = config.url
|
|
@@ -83,7 +96,16 @@ export class MeridianClient {
|
|
|
83
96
|
url: wsUrl,
|
|
84
97
|
token: config.token,
|
|
85
98
|
onMessage: (msg) => { this.handleServerMsg(msg); },
|
|
99
|
+
...(keypair !== null && { signFn: (opBytes) => signOp(keypair.privateKey, opBytes) }),
|
|
86
100
|
});
|
|
101
|
+
this.tabSyncChannel = (config.tabSync === true && typeof BroadcastChannel !== "undefined")
|
|
102
|
+
? new TabSync(config.namespace)
|
|
103
|
+
: null;
|
|
104
|
+
if (this.tabSyncChannel !== null) {
|
|
105
|
+
this.tabSyncChannel.onDelta(({ crdtId, deltaBytes }) => {
|
|
106
|
+
void this.applyDelta(crdtId, deltaBytes, false);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
87
109
|
// Restore persisted op queue before connecting.
|
|
88
110
|
if (this.opsStore !== null) {
|
|
89
111
|
const bytes = this.opsStore.load(`ops:${this.namespace}`);
|
|
@@ -98,6 +120,10 @@ export class MeridianClient {
|
|
|
98
120
|
this.opsStore.delete(`ops:${this.namespace}`);
|
|
99
121
|
}
|
|
100
122
|
globalThis.addEventListener("beforeunload", this.flushOpsToStorage);
|
|
123
|
+
// Flush when tab is hidden (covers most crash/kill scenarios without IDB complexity).
|
|
124
|
+
if (typeof document !== "undefined") {
|
|
125
|
+
document.addEventListener("visibilitychange", this.onVisibilityChange);
|
|
126
|
+
}
|
|
101
127
|
}
|
|
102
128
|
if (config.autoConnect !== false && config.offline !== true) {
|
|
103
129
|
this.transport.connect();
|
|
@@ -161,6 +187,11 @@ export class MeridianClient {
|
|
|
161
187
|
}
|
|
162
188
|
this.opsStore.save(key, encode(msgs));
|
|
163
189
|
};
|
|
190
|
+
onVisibilityChange = () => {
|
|
191
|
+
if (typeof document !== "undefined" && document.visibilityState === "hidden") {
|
|
192
|
+
this.flushOpsToStorage();
|
|
193
|
+
}
|
|
194
|
+
};
|
|
164
195
|
/**
|
|
165
196
|
* Creates and validates a new `MeridianClient` from the supplied configuration.
|
|
166
197
|
*
|
|
@@ -183,7 +214,26 @@ export class MeridianClient {
|
|
|
183
214
|
* ```
|
|
184
215
|
*/
|
|
185
216
|
static create(config) {
|
|
186
|
-
return
|
|
217
|
+
return Effect.gen(function* () {
|
|
218
|
+
const claims = yield* parseAndValidateToken(config.token);
|
|
219
|
+
const keypair = config.signing === true
|
|
220
|
+
? yield* Effect.promise(() => loadOrGenerateKeypair())
|
|
221
|
+
: null;
|
|
222
|
+
return new MeridianClient(config, claims, keypair);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
resolveEncKey(crdtId) {
|
|
226
|
+
if (this.encKeys.size === 0)
|
|
227
|
+
return null;
|
|
228
|
+
for (const [pattern, key] of this.encKeys) {
|
|
229
|
+
if (globMatch(pattern, crdtId))
|
|
230
|
+
return key;
|
|
231
|
+
}
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
encryptFnFor(crdtId) {
|
|
235
|
+
const key = this.resolveEncKey(crdtId);
|
|
236
|
+
return key !== null ? (v) => encryptJson(key, v) : undefined;
|
|
187
237
|
}
|
|
188
238
|
/**
|
|
189
239
|
* Returns a handle for a grow-only counter (GCounter) CRDT.
|
|
@@ -289,7 +339,8 @@ export class MeridianClient {
|
|
|
289
339
|
lwwregister(crdtId, schema) {
|
|
290
340
|
let handle = this.lwHandles.get(crdtId);
|
|
291
341
|
if (!handle) {
|
|
292
|
-
const
|
|
342
|
+
const encryptFn = this.encryptFnFor(crdtId);
|
|
343
|
+
const base = { ns: this.namespace, crdtId, clientId: this.clientId, transport: this.transport, ...(encryptFn !== undefined && { encryptFn }) };
|
|
293
344
|
handle = schema ? new LwwRegisterHandle({ ...base, schema }) : new LwwRegisterHandle(base);
|
|
294
345
|
this.lwHandles.set(crdtId, handle);
|
|
295
346
|
this.transport.subscribe(crdtId);
|
|
@@ -320,7 +371,8 @@ export class MeridianClient {
|
|
|
320
371
|
presence(crdtId, schema) {
|
|
321
372
|
let handle = this.prHandles.get(crdtId);
|
|
322
373
|
if (!handle) {
|
|
323
|
-
const
|
|
374
|
+
const encryptFn = this.encryptFnFor(crdtId);
|
|
375
|
+
const base = { ns: this.namespace, crdtId, clientId: this.clientId, transport: this.transport, ...(encryptFn !== undefined && { encryptFn }) };
|
|
324
376
|
handle = schema ? new PresenceHandle({ ...base, schema }) : new PresenceHandle(base);
|
|
325
377
|
this.prHandles.set(crdtId, handle);
|
|
326
378
|
this.transport.subscribe(crdtId);
|
|
@@ -570,7 +622,11 @@ export class MeridianClient {
|
|
|
570
622
|
this.flushOpsToStorage();
|
|
571
623
|
if (this.opsStore !== null) {
|
|
572
624
|
globalThis.removeEventListener("beforeunload", this.flushOpsToStorage);
|
|
625
|
+
if (typeof document !== "undefined") {
|
|
626
|
+
document.removeEventListener("visibilitychange", this.onVisibilityChange);
|
|
627
|
+
}
|
|
573
628
|
}
|
|
629
|
+
this.tabSyncChannel?.close();
|
|
574
630
|
for (const unsub of this.handleUnsubs)
|
|
575
631
|
unsub();
|
|
576
632
|
this.handleUnsubs.length = 0;
|
|
@@ -678,87 +734,136 @@ export class MeridianClient {
|
|
|
678
734
|
}
|
|
679
735
|
return;
|
|
680
736
|
}
|
|
681
|
-
if (
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
737
|
+
if ("Delta" in msg) {
|
|
738
|
+
const { crdt_id, delta_bytes } = msg.Delta;
|
|
739
|
+
void this.applyDelta(crdt_id, delta_bytes, true);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Apply a CRDT delta to the appropriate local handle.
|
|
744
|
+
* Called from the server message handler and from tab-sync broadcasts.
|
|
745
|
+
* When `broadcast` is true, the delta is forwarded to other tabs via BroadcastChannel.
|
|
746
|
+
*/
|
|
747
|
+
async applyDelta(crdtId, deltaBytes, broadcast) {
|
|
748
|
+
const gcHandle = this.gcHandles.get(crdtId);
|
|
685
749
|
if (gcHandle) {
|
|
686
750
|
try {
|
|
687
|
-
gcHandle.applyDelta(decodeGCounterDelta(
|
|
751
|
+
gcHandle.applyDelta(decodeGCounterDelta(deltaBytes));
|
|
688
752
|
}
|
|
689
753
|
catch { /* stale */ }
|
|
690
|
-
this.saveSnap(`snap:gc:${this.namespace}:${this.clientId}:${
|
|
691
|
-
this.notifyDelta(
|
|
754
|
+
this.saveSnap(`snap:gc:${this.namespace}:${this.clientId}:${crdtId}`, gcHandle);
|
|
755
|
+
this.notifyDelta(crdtId, "gcounter");
|
|
756
|
+
if (broadcast)
|
|
757
|
+
this.tabSyncChannel?.broadcast(crdtId, deltaBytes);
|
|
692
758
|
return;
|
|
693
759
|
}
|
|
694
|
-
const pnHandle = this.pnHandles.get(
|
|
760
|
+
const pnHandle = this.pnHandles.get(crdtId);
|
|
695
761
|
if (pnHandle) {
|
|
696
762
|
try {
|
|
697
|
-
pnHandle.applyDelta(decodePNCounterDelta(
|
|
763
|
+
pnHandle.applyDelta(decodePNCounterDelta(deltaBytes));
|
|
698
764
|
}
|
|
699
765
|
catch { /* stale */ }
|
|
700
|
-
this.saveSnap(`snap:pn:${this.namespace}:${this.clientId}:${
|
|
701
|
-
this.notifyDelta(
|
|
766
|
+
this.saveSnap(`snap:pn:${this.namespace}:${this.clientId}:${crdtId}`, pnHandle);
|
|
767
|
+
this.notifyDelta(crdtId, "pncounter");
|
|
768
|
+
if (broadcast)
|
|
769
|
+
this.tabSyncChannel?.broadcast(crdtId, deltaBytes);
|
|
702
770
|
return;
|
|
703
771
|
}
|
|
704
|
-
const orHandle = this.orHandles.get(
|
|
772
|
+
const orHandle = this.orHandles.get(crdtId);
|
|
705
773
|
if (orHandle) {
|
|
706
774
|
try {
|
|
707
|
-
orHandle.applyDelta(decodeORSetDelta(
|
|
775
|
+
orHandle.applyDelta(decodeORSetDelta(deltaBytes));
|
|
708
776
|
}
|
|
709
777
|
catch { /* stale */ }
|
|
710
|
-
this.saveSnap(`snap:or:${this.namespace}:${this.clientId}:${
|
|
711
|
-
this.notifyDelta(
|
|
778
|
+
this.saveSnap(`snap:or:${this.namespace}:${this.clientId}:${crdtId}`, orHandle);
|
|
779
|
+
this.notifyDelta(crdtId, "orset");
|
|
780
|
+
if (broadcast)
|
|
781
|
+
this.tabSyncChannel?.broadcast(crdtId, deltaBytes);
|
|
712
782
|
return;
|
|
713
783
|
}
|
|
714
|
-
const lwHandle = this.lwHandles.get(
|
|
784
|
+
const lwHandle = this.lwHandles.get(crdtId);
|
|
715
785
|
if (lwHandle) {
|
|
716
786
|
try {
|
|
717
|
-
|
|
787
|
+
let delta = decodeLwwDelta(deltaBytes);
|
|
788
|
+
if (delta.entry !== null && isEncryptedValue(delta.entry.value)) {
|
|
789
|
+
const key = this.resolveEncKey(crdtId);
|
|
790
|
+
if (key !== null) {
|
|
791
|
+
const plain = await decryptJson(key, delta.entry.value);
|
|
792
|
+
delta = { entry: { ...delta.entry, value: plain } };
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
lwHandle.applyDelta(delta);
|
|
718
796
|
}
|
|
719
|
-
catch { /* stale */ }
|
|
720
|
-
this.saveSnap(`snap:lw:${this.namespace}:${this.clientId}:${
|
|
721
|
-
this.notifyDelta(
|
|
797
|
+
catch { /* stale or decrypt failure */ }
|
|
798
|
+
this.saveSnap(`snap:lw:${this.namespace}:${this.clientId}:${crdtId}`, lwHandle);
|
|
799
|
+
this.notifyDelta(crdtId, "lwwregister");
|
|
800
|
+
if (broadcast)
|
|
801
|
+
this.tabSyncChannel?.broadcast(crdtId, deltaBytes);
|
|
722
802
|
return;
|
|
723
803
|
}
|
|
724
|
-
const prHandle = this.prHandles.get(
|
|
804
|
+
const prHandle = this.prHandles.get(crdtId);
|
|
725
805
|
if (prHandle) {
|
|
726
806
|
try {
|
|
727
|
-
|
|
807
|
+
const delta = decodePresenceDelta(deltaBytes);
|
|
808
|
+
const key = this.resolveEncKey(crdtId);
|
|
809
|
+
if (key !== null) {
|
|
810
|
+
const decrypted = {};
|
|
811
|
+
for (const [clientIdStr, entry] of Object.entries(delta.changes)) {
|
|
812
|
+
if (entry !== null && isEncryptedValue(entry.data)) {
|
|
813
|
+
const plain = await decryptJson(key, entry.data);
|
|
814
|
+
decrypted[clientIdStr] = { ...entry, data: plain };
|
|
815
|
+
}
|
|
816
|
+
else {
|
|
817
|
+
decrypted[clientIdStr] = entry;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
prHandle.applyDelta({ changes: decrypted });
|
|
821
|
+
}
|
|
822
|
+
else {
|
|
823
|
+
prHandle.applyDelta(delta);
|
|
824
|
+
}
|
|
728
825
|
}
|
|
729
|
-
catch { /* stale */ }
|
|
730
|
-
this.saveSnap(`snap:pr:${this.namespace}:${this.clientId}:${
|
|
731
|
-
this.notifyDelta(
|
|
826
|
+
catch { /* stale or decrypt failure */ }
|
|
827
|
+
this.saveSnap(`snap:pr:${this.namespace}:${this.clientId}:${crdtId}`, prHandle);
|
|
828
|
+
this.notifyDelta(crdtId, "presence");
|
|
829
|
+
if (broadcast)
|
|
830
|
+
this.tabSyncChannel?.broadcast(crdtId, deltaBytes);
|
|
732
831
|
return;
|
|
733
832
|
}
|
|
734
|
-
const cmHandle = this.cmHandles.get(
|
|
833
|
+
const cmHandle = this.cmHandles.get(crdtId);
|
|
735
834
|
if (cmHandle) {
|
|
736
835
|
try {
|
|
737
|
-
cmHandle.applyDelta(decodeCRDTMapDelta(
|
|
836
|
+
cmHandle.applyDelta(decodeCRDTMapDelta(deltaBytes));
|
|
738
837
|
}
|
|
739
838
|
catch { /* stale */ }
|
|
740
|
-
this.saveSnap(`snap:cm:${this.namespace}:${this.clientId}:${
|
|
741
|
-
this.notifyDelta(
|
|
839
|
+
this.saveSnap(`snap:cm:${this.namespace}:${this.clientId}:${crdtId}`, cmHandle);
|
|
840
|
+
this.notifyDelta(crdtId, "crdtmap");
|
|
841
|
+
if (broadcast)
|
|
842
|
+
this.tabSyncChannel?.broadcast(crdtId, deltaBytes);
|
|
742
843
|
return;
|
|
743
844
|
}
|
|
744
|
-
const rgaHandle = this.rgaHandles.get(
|
|
845
|
+
const rgaHandle = this.rgaHandles.get(crdtId);
|
|
745
846
|
if (rgaHandle) {
|
|
746
847
|
try {
|
|
747
|
-
rgaHandle.applyDelta(decodeRGADelta(
|
|
848
|
+
rgaHandle.applyDelta(decodeRGADelta(deltaBytes));
|
|
748
849
|
}
|
|
749
850
|
catch { /* stale */ }
|
|
750
|
-
this.saveSnap(`snap:rg:${this.namespace}:${this.clientId}:${
|
|
751
|
-
this.notifyDelta(
|
|
851
|
+
this.saveSnap(`snap:rg:${this.namespace}:${this.clientId}:${crdtId}`, rgaHandle);
|
|
852
|
+
this.notifyDelta(crdtId, "rga");
|
|
853
|
+
if (broadcast)
|
|
854
|
+
this.tabSyncChannel?.broadcast(crdtId, deltaBytes);
|
|
752
855
|
return;
|
|
753
856
|
}
|
|
754
|
-
const treeHandle = this.treeHandles.get(
|
|
857
|
+
const treeHandle = this.treeHandles.get(crdtId);
|
|
755
858
|
if (treeHandle) {
|
|
756
859
|
try {
|
|
757
|
-
treeHandle.applyDelta(decodeTreeDelta(
|
|
860
|
+
treeHandle.applyDelta(decodeTreeDelta(deltaBytes));
|
|
758
861
|
}
|
|
759
862
|
catch { /* stale */ }
|
|
760
|
-
this.saveSnap(`snap:tr:${this.namespace}:${this.clientId}:${
|
|
761
|
-
this.notifyDelta(
|
|
863
|
+
this.saveSnap(`snap:tr:${this.namespace}:${this.clientId}:${crdtId}`, treeHandle);
|
|
864
|
+
this.notifyDelta(crdtId, "tree");
|
|
865
|
+
if (broadcast)
|
|
866
|
+
this.tabSyncChannel?.broadcast(crdtId, deltaBytes);
|
|
762
867
|
}
|
|
763
868
|
}
|
|
764
869
|
notifyDelta(crdtId, type) {
|