@tinycloud/replica 0.2.3-beta.0 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tinycloud/replica",
3
- "version": "0.2.3-beta.0",
3
+ "version": "0.2.3",
4
4
  "description": "Durable, read-only local replicas of a TinyCloud KV prefix over the tinycloud.kv/sync change feed.",
5
5
  "author": "TinyCloud, Inc.",
6
6
  "license": "EGPL",
@@ -45,7 +45,7 @@
45
45
  "dependencies": {
46
46
  "@noble/curves": "^1.9.2",
47
47
  "@noble/hashes": "^1.8.0",
48
- "@tinycloud/sdk-services": "3.2.5-beta.0"
48
+ "@tinycloud/sdk-services": "3.2.5"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@tinycloud/web-sdk-wasm": "1.7.6",
package/dist/browser.d.ts DELETED
@@ -1,336 +0,0 @@
1
- /**
2
- * Typed replica errors. Every failure a caller can act on has its own code;
3
- * hosts (CLI, worker) map codes to their own exit codes or UI states.
4
- */
5
- declare const ReplicaErrorCode: {
6
- /** Another process holds the sync lease. */
7
- readonly BUSY: "REPLICA_BUSY";
8
- /** No replica with this name exists. */
9
- readonly NOT_FOUND: "REPLICA_NOT_FOUND";
10
- /** The replica exists but its stored configuration differs from the request. */
11
- readonly CONFIG_MISMATCH: "REPLICA_CONFIG_MISMATCH";
12
- /** The runtime has no supported durable store (e.g. Node < 22.13). */
13
- readonly RUNTIME_UNSUPPORTED: "RUNTIME_UNSUPPORTED";
14
- /** A required open() argument is missing or malformed (e.g. `principal`). */
15
- readonly INVALID_ARGUMENT: "REPLICA_INVALID_ARGUMENT";
16
- /** The local store failed (I/O, corruption, schema). */
17
- readonly STORAGE_ERROR: "STORAGE_ERROR";
18
- /** The local disk is full. */
19
- readonly STORAGE_FULL: "STORAGE_FULL";
20
- /** The key is outside the replica's prefix. */
21
- readonly NOT_COVERED: "NOT_COVERED";
22
- /** Syncing the secrets space or the vault namespace needs an explicit opt-in. */
23
- readonly SECRETS_OPT_IN_REQUIRED: "SECRETS_OPT_IN_REQUIRED";
24
- /** No grant is installed, or none covers the requested scope. */
25
- readonly GRANT_MISSING: "GRANT_MISSING";
26
- /** The grant bytes are not a verifiable UCAN, or its signature is wrong. */
27
- readonly GRANT_INVALID: "GRANT_INVALID";
28
- /** SIWE/CACAO and other non-UCAN grants cannot be used for a replica. */
29
- readonly GRANT_FORMAT_UNSUPPORTED: "GRANT_FORMAT_UNSUPPORTED";
30
- /** The grant was issued to a different device. */
31
- readonly GRANT_AUDIENCE_MISMATCH: "GRANT_AUDIENCE_MISMATCH";
32
- /** The grant lacks `tinycloud.kv/sync` or `tinycloud.kv/get` on the prefix. */
33
- readonly GRANT_NOT_COVERING: "GRANT_NOT_COVERING";
34
- readonly GRANT_NOT_YET_VALID: "GRANT_NOT_YET_VALID";
35
- readonly GRANT_EXPIRED: "GRANT_EXPIRED";
36
- /** The configured node does not advertise whole-space KV sync support. */
37
- readonly REPLICATION_ROOT_UNSUPPORTED: "REPLICATION_ROOT_UNSUPPORTED";
38
- readonly GRANT_REVOKED: "GRANT_REVOKED";
39
- /** The node refused the invocation for a reason other than revocation. */
40
- readonly GRANT_UNAUTHORIZED: "GRANT_UNAUTHORIZED";
41
- /** The node refused the retention grant. */
42
- readonly RETENTION_GRANT_REFUSED: "RETENTION_GRANT_REFUSED";
43
- /** The node could not be reached. */
44
- readonly NETWORK_ERROR: "NETWORK_ERROR";
45
- /** The node answered with an unexpected status. */
46
- readonly NODE_ERROR: "NODE_ERROR";
47
- /** The node's answer violates the wire contract. */
48
- readonly PROTOCOL_ERROR: "PROTOCOL_ERROR";
49
- /** The node asked for a fresh bootstrap (410). Handled inside sync. */
50
- readonly RESET_REQUIRED: "RESET_REQUIRED";
51
- /** The feed now comes from a different node than the one pinned. */
52
- readonly SOURCE_CHANGED: "SOURCE_CHANGED";
53
- /** The feed delivered a key outside the replica's prefix. */
54
- readonly SCOPE_VIOLATION: "SCOPE_VIOLATION";
55
- /** Fetched bytes do not hash to the ETag the node attested for them. */
56
- readonly CONTENT_MISMATCH: "CONTENT_MISMATCH";
57
- /** A stored blob no longer hashes to its name. */
58
- readonly INTEGRITY_ERROR: "INTEGRITY_ERROR";
59
- /** The replica handle was closed; in-flight calls settle with this code. */
60
- readonly CLOSED: "REPLICA_CLOSED";
61
- };
62
- type ReplicaErrorCode = (typeof ReplicaErrorCode)[keyof typeof ReplicaErrorCode];
63
- /**
64
- * The package ships separate entry bundles (`.`, `./sqlite`), each with its own
65
- * copy of this class, so identity is a registered symbol, not `instanceof`.
66
- */
67
- declare const REPLICA_ERROR: unique symbol;
68
- declare class ReplicaError extends Error {
69
- readonly [REPLICA_ERROR] = true;
70
- readonly code: ReplicaErrorCode;
71
- readonly detail?: Record<string, unknown>;
72
- constructor(code: ReplicaErrorCode, message: string, detail?: Record<string, unknown>, options?: {
73
- cause?: unknown;
74
- });
75
- }
76
-
77
- type LocalReadPolicy = "whileGrantValid" | "retainAfterExpiry";
78
- type Coverage = "empty" | "bootstrapping" | "complete";
79
- type AuthorityState = "valid" | "expired" | "revoked" | "not-yet-valid";
80
- type ReplicaLastError = {
81
- at: string;
82
- code: string;
83
- message: string;
84
- };
85
- type ReplicaStatus = {
86
- name: string;
87
- replicaId: string;
88
- source: {
89
- host: string;
90
- nodeDid: string | null;
91
- space: string;
92
- prefix: string;
93
- };
94
- device: {
95
- did: string;
96
- delegationCid: string | null;
97
- pendingDelegationCid: string | null;
98
- pendingDelegationError: ReplicaLastError | null;
99
- };
100
- authority: {
101
- state: AuthorityState;
102
- notBefore: string | null;
103
- expiresAt: string | null;
104
- retainUntil: string | null;
105
- localReadPolicy: LocalReadPolicy;
106
- retentionGrantCid: string | null;
107
- revokedDetail: string | null;
108
- retentionRevokedDetail: string | null;
109
- };
110
- consistency: "observed";
111
- coverage: Coverage;
112
- lastSyncAt: string | null;
113
- lastCompleteAt: string | null;
114
- counts: {
115
- keys: number;
116
- contentMissing: number;
117
- tombstones: number;
118
- };
119
- bytes: number;
120
- durability: string;
121
- syncing: boolean;
122
- /** A revocation purge has not removed every content file yet; the next open retries it. */
123
- purgePending: boolean;
124
- lastError: ReplicaLastError | null;
125
- lastReset: {
126
- at: string;
127
- reason: string;
128
- } | null;
129
- };
130
-
131
- type SyncReport = {
132
- pages: number;
133
- changes: number;
134
- deleted: number;
135
- fetched: number;
136
- contentMissing: number;
137
- repaired: number;
138
- resets: number;
139
- coverage: Coverage;
140
- cursorAdvanced: boolean;
141
- promotedGrant: boolean;
142
- blobsCollected: number;
143
- };
144
-
145
- /**
146
- * The main-thread ↔ worker RPC contract (TC-19). Requests are `{id, op, …}`;
147
- * replies are `{id, ok:true, result}` or `{id, ok:false, err}`; the worker
148
- * posts unsolicited `{event}` messages. Values cross as transferable
149
- * ArrayBuffers, never as broadcast events.
150
- */
151
-
152
- type OpenResult = {
153
- replicaId: string;
154
- deviceDid: string;
155
- /** The verification method (DID fragment form) the app delegates to. */
156
- verificationMethod: string;
157
- status: ReplicaStatus | null;
158
- created: boolean;
159
- };
160
- type SyncResult = {
161
- status: "synced";
162
- } & SyncReport | {
163
- status: "busy";
164
- };
165
- type GetResult = {
166
- status: "present";
167
- key: string;
168
- value: ArrayBuffer;
169
- etag: string;
170
- metadata: Record<string, string>;
171
- } | {
172
- status: "content_missing";
173
- key: string;
174
- etag: string;
175
- metadata: Record<string, string>;
176
- } | {
177
- status: "deleted" | "absent" | "coverage_incomplete" | "not_covered";
178
- key: string;
179
- };
180
- type ListResult = {
181
- entries: Array<{
182
- key: string;
183
- etag: string;
184
- metadata: Record<string, string>;
185
- content: boolean;
186
- }>;
187
- };
188
- type StatusResult = {
189
- status: ReplicaStatus;
190
- persistence: {
191
- persisted: boolean | null;
192
- locksSupported: boolean;
193
- };
194
- };
195
-
196
- /**
197
- * The main-thread browser API (TC-19): a thin RPC client over the replica
198
- * worker. IndexedDB, the network and the device key live in the worker; the
199
- * client's local reads never touch the network, and its sync calls are the
200
- * only path that does.
201
- *
202
- * The worker ships as `dist/replica.worker.js`, a self-contained ESM bundle
203
- * spawned via `new Worker(new URL("./replica.worker.js", import.meta.url),
204
- * { type: "module" })` — webpack 5 and Vite detect that form. A `worker`
205
- * option accepts a `Worker` or `URL` escape hatch. In Vite dev mode, list
206
- * `@tinycloud/replica` in `optimizeDeps.exclude` so the worker URL resolves
207
- * inside the package (vitejs/vite#20859).
208
- *
209
- * Trust boundary: the browser origin, exactly as the OS user is the CLI's.
210
- * Run one app per origin — any same-origin script can open the IndexedDB
211
- * databases directly. `principal` (required) is the signed-in user's
212
- * identity DID; it partitions replicas per user on that origin, it is not
213
- * an authorization check — the node authorizes at sync time, and local
214
- * reads are gated by the stored grant's node-attested window, like the CLI.
215
- */
216
-
217
- /**
218
- * `reset()` result. With `purge: true` the replica's contents are erased in
219
- * a single commit — the database file itself is never deleted: a small
220
- * empty database named by a hash remains (holding no user data) and the
221
- * next `openReplica` reinitializes it in place. Once the commit lands the
222
- * result is final `{ purged: true }` — nothing after it can change the
223
- * outcome.
224
- */
225
- type ResetResult = {
226
- reset: true;
227
- purged: boolean;
228
- };
229
- type BrowserReplicaOpenOptions = {
230
- /** The TinyCloud host to sync from; pinned for the replica's life. */
231
- host: string;
232
- /** The space id (native form) the replica covers. */
233
- space: string;
234
- /** The KV prefix: `/` for the whole space, or a folder such as `notes/`. */
235
- prefix: string;
236
- /** A display name; defaults to the prefix. */
237
- name?: string;
238
- /**
239
- * The signed-in user's identity DID (for example `did:pkh:eip155:1:0x…`),
240
- * as the app knows it. Replicas for different principals get different
241
- * databases on the same origin; signing in again and installing the new
242
- * session's grant continues the same replica from its cursor. This is an
243
- * app-asserted partition label, not an authorization check — the trust
244
- * boundary is the browser origin, and the node authorizes at sync time.
245
- */
246
- principal: string;
247
- /** Replicating the `secrets` space or a `vault/` prefix needs this opt-in. */
248
- allowSecrets?: boolean;
249
- /**
250
- * Spawn this worker instead of the bundled one (custom bundler layouts).
251
- * A factory (`() => Worker`) is also accepted: it runs after `principal`
252
- * validation, so a bad open never spawns a worker — useful for tests.
253
- */
254
- worker?: Worker | URL | string | (() => Worker);
255
- };
256
- type BrowserReplicaGetResult = GetResult;
257
- type BrowserReplicaListResult = ListResult;
258
- type BrowserReplicaStatus = StatusResult;
259
- type BrowserReplicaSyncResult = SyncResult;
260
- type BrowserReplicaOpenResult = OpenResult;
261
- /** Subscriptions for worker events (other tabs' commits, resets). */
262
- type BrowserReplicaEvents = {
263
- /** Another tab's worker committed a page (`serial` is monotonic). */
264
- onCommitted?: (serial: number) => void;
265
- /** The replica's authority state changed. */
266
- onAuthority?: (state: string) => void;
267
- /** The replica was reset in this tab or observed elsewhere. */
268
- onReset?: (reason: string) => void;
269
- };
270
- /**
271
- * Open (or create) a replica in a dedicated worker. The first call also asks
272
- * the browser for durable storage (`navigator.storage.persist()`), so an
273
- * eviction under pressure is less likely; `status()` reports the outcome.
274
- * In environments without IndexedDB the returned promise rejects with
275
- * `RUNTIME_UNSUPPORTED` — there is no in-memory fallback, reads would lose
276
- * their durability guarantee.
277
- *
278
- * `principal` partitions the replica per signed-in user and is required: the
279
- * same host/space/prefix under two principals yields two databases. A device
280
- * grant chained to a web session (about one hour under an OpenKey session)
281
- * gives about that session's length of offline reads; after sign-in rotates
282
- * the session key, install the new grant and the replica continues from its
283
- * cursor.
284
- */
285
- declare function openReplica(options: BrowserReplicaOpenOptions, events?: BrowserReplicaEvents): Promise<BrowserReplica>;
286
- declare class BrowserReplica {
287
- #private;
288
- /** The open() result: replicaId, deviceDid and the status at open time. */
289
- opened: OpenResult | null;
290
- /** Settles when the worker reports it is listening (or fails to start). */
291
- readonly ready: Promise<void>;
292
- /** For tests and advanced use: drive an already-spawned worker. */
293
- constructor(worker: Worker, events?: BrowserReplicaEvents);
294
- /** Open or create the replica; `openReplica` calls this for you. */
295
- open(options: BrowserReplicaOpenOptions & {
296
- persisted?: boolean;
297
- }): Promise<BrowserReplica>;
298
- /** The DID the app delegates to (`tcw.delegateTo(deviceDid, […sync…])`). */
299
- get deviceDid(): string;
300
- /** Install a signed grant: the compact UCAN (`delegation.delegationHeader.Authorization`). */
301
- installGrant(delegation: string): Promise<{
302
- cid: string;
303
- audience: string;
304
- expiresAt: number | null;
305
- }>;
306
- /** Pull the feed and commit each page atomically. `busy` when another tab syncs. */
307
- sync(options?: {
308
- limit?: number;
309
- }): Promise<SyncResult>;
310
- /** Local read: never touches the network. */
311
- get(key: string): Promise<GetResult>;
312
- /** Local read: never touches the network. */
313
- list(options?: {
314
- prefix?: string;
315
- after?: string;
316
- limit?: number;
317
- }): Promise<ListResult>;
318
- /** Replica status plus browser persistence facts. */
319
- status(): Promise<StatusResult>;
320
- /** Opt in to post-expiry reads under a `tinycloud.kv/retain` grant. */
321
- setRetentionGrant(cid: string | null): Promise<{
322
- retentionGrantCid: string | null;
323
- }>;
324
- /**
325
- * Clear entries and cursor (keeps config and grant); `purge` erases the
326
- * replica permanently in place — the database file is never deleted, so
327
- * the result is final the moment it resolves (see {@link ResetResult}).
328
- */
329
- reset(options?: {
330
- purge?: boolean;
331
- }): Promise<ResetResult>;
332
- /** Close the worker-side handles and terminate the worker. */
333
- close(): Promise<void>;
334
- }
335
-
336
- export { BrowserReplica, type BrowserReplicaEvents, type BrowserReplicaGetResult, type BrowserReplicaListResult, type BrowserReplicaOpenOptions, type BrowserReplicaOpenResult, type BrowserReplicaStatus, type BrowserReplicaSyncResult, ReplicaError, ReplicaErrorCode, type ResetResult, openReplica };