@tinycloud/replica 0.2.4-beta.3 → 0.3.0-beta.5
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/README.md +29 -0
- package/dist/browser.d.ts +6 -0
- package/dist/browser.js +7 -1
- package/dist/browser.js.map +1 -1
- package/dist/index.cjs +20 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -3
- package/dist/index.d.ts +15 -3
- package/dist/index.js +20 -2
- package/dist/index.js.map +1 -1
- package/dist/kv-storage-testing.d.ts +83 -0
- package/dist/kv-storage-testing.js +1145 -0
- package/dist/kv-storage-testing.js.map +1 -0
- package/dist/kv-storage.d.ts +155 -0
- package/dist/kv-storage.js +2587 -0
- package/dist/kv-storage.js.map +1 -0
- package/dist/kv-storage.worker.67e46cebbda277040312a770449123633491901d6cceacea95754a843e01e9c9.js +18996 -0
- package/dist/kv-storage.worker.d.ts +2 -0
- package/dist/kv-storage.worker.js +18996 -0
- package/dist/kv-storage.worker.js.map +1 -0
- package/dist/profile-gate.cjs +560 -0
- package/dist/profile-gate.cjs.map +1 -0
- package/dist/profile-gate.d.cts +89 -0
- package/dist/profile-gate.d.ts +89 -0
- package/dist/profile-gate.js +517 -0
- package/dist/profile-gate.js.map +1 -0
- package/dist/replica.worker.js +673 -295
- package/dist/replica.worker.js.map +1 -1
- package/dist/sqlite.cjs +734 -174
- package/dist/sqlite.cjs.map +1 -1
- package/dist/sqlite.d.cts +77 -8
- package/dist/sqlite.d.ts +77 -8
- package/dist/sqlite.js +710 -152
- package/dist/sqlite.js.map +1 -1
- package/dist/{types-f3-kYLQZ.d.cts → types-CQ61VnsP.d.cts} +1 -1
- package/dist/{types-f3-kYLQZ.d.ts → types-CQ61VnsP.d.ts} +1 -1
- package/package.json +20 -3
package/README.md
CHANGED
|
@@ -77,6 +77,35 @@ try {
|
|
|
77
77
|
|
|
78
78
|
`get()` may return `present`, `content_missing`, `deleted`, `absent`, `coverage_incomplete`, or `not_covered`. Check its status before using a value. `sync()` returns `synced` with a report or `busy` when another tab holds the sync lock. Errors are `ReplicaError` instances with a stable `code`; browser exports `ReplicaError` and `ReplicaErrorCode` from its entry point. Local reads never make network requests, but they remain subject to grant and retention authority.
|
|
79
79
|
|
|
80
|
+
### KV storage worker assets
|
|
81
|
+
|
|
82
|
+
`@tinycloud/replica/kv-storage` starts its own module worker on first use unless
|
|
83
|
+
`indexedDbReplicaStorage({ worker })` supplies a Worker, URL, string, or factory.
|
|
84
|
+
The published ESM client uses a static `new URL("./kv-storage.worker.<sha256>.js", import.meta.url)`
|
|
85
|
+
reference. The package build emits that SHA-256-named file alongside the client;
|
|
86
|
+
it includes the inlined WASM and requires no separate network fetch or runtime
|
|
87
|
+
manifest. Deploy the client and worker together, preserving their relative paths.
|
|
88
|
+
Bundlers such as Vite resolve this literal URL into their own emitted worker
|
|
89
|
+
asset and may transform its bytes and append a separate bundle fingerprint.
|
|
90
|
+
|
|
91
|
+
The package also exports `@tinycloud/replica/kv-storage/worker` as the stable
|
|
92
|
+
`kv-storage.worker.js` path for explicit `?url` imports/custom worker layouts.
|
|
93
|
+
Its bytes match the SHA-256-named file, but its URL is **not** immutable. Hosts
|
|
94
|
+
must serve the hashed worker with `Cache-Control: public, max-age=31536000, immutable`
|
|
95
|
+
and must not apply that policy to `kv-storage.worker.js` or any other stable
|
|
96
|
+
worker URL. Keep old hashed worker assets available while clients referencing
|
|
97
|
+
them may still be cached.
|
|
98
|
+
|
|
99
|
+
In the separate web SDK integration, the first local open loads the optional
|
|
100
|
+
first-import chunk (51,925/67,138 gzip bytes ESM/CJS), deferred IndexedDB
|
|
101
|
+
activation chunk (23,075/24,655), and worker budget (820,108). That is
|
|
102
|
+
approximately 895,108/911,901 gzip bytes beyond the mandatory root on first
|
|
103
|
+
use; subsequent opens reuse cacheable hashed assets when the host honors the
|
|
104
|
+
immutable cache policy above. These optional costs do not waive the separate
|
|
105
|
+
mandatory-root bundle budget. The worker ceiling has zero headroom against the
|
|
106
|
+
GitHub Bun 1.3.14 / Rust 1.99.0 measurement; its checker rejects other
|
|
107
|
+
toolchains before comparing bytes.
|
|
108
|
+
|
|
80
109
|
## Node and SQLite
|
|
81
110
|
|
|
82
111
|
The SQLite store API is exported directly from `@tinycloud/replica/sqlite`:
|
package/dist/browser.d.ts
CHANGED
|
@@ -58,6 +58,12 @@ declare const ReplicaErrorCode: {
|
|
|
58
58
|
readonly INTEGRITY_ERROR: "INTEGRITY_ERROR";
|
|
59
59
|
/** The replica handle was closed; in-flight calls settle with this code. */
|
|
60
60
|
readonly CLOSED: "REPLICA_CLOSED";
|
|
61
|
+
/** The lifecycle gate refused the operation (shared contract `REPLICA_GATED`); `detail.gate` says why. */
|
|
62
|
+
readonly GATED: "REPLICA_GATED";
|
|
63
|
+
/** Quota admission refused a page (`maxBytes` or the free-space floor); nothing committed. */
|
|
64
|
+
readonly STORAGE_BUDGET: "STORAGE_BUDGET";
|
|
65
|
+
/** The consent deadline passed: the grant is not served until the user consents again. */
|
|
66
|
+
readonly CONSENT_EXPIRED: "CONSENT_EXPIRED";
|
|
61
67
|
};
|
|
62
68
|
type ReplicaErrorCode = (typeof ReplicaErrorCode)[keyof typeof ReplicaErrorCode];
|
|
63
69
|
/**
|
package/dist/browser.js
CHANGED
|
@@ -54,7 +54,13 @@ var ReplicaErrorCode = {
|
|
|
54
54
|
/** A stored blob no longer hashes to its name. */
|
|
55
55
|
INTEGRITY_ERROR: "INTEGRITY_ERROR",
|
|
56
56
|
/** The replica handle was closed; in-flight calls settle with this code. */
|
|
57
|
-
CLOSED: "REPLICA_CLOSED"
|
|
57
|
+
CLOSED: "REPLICA_CLOSED",
|
|
58
|
+
/** The lifecycle gate refused the operation (shared contract `REPLICA_GATED`); `detail.gate` says why. */
|
|
59
|
+
GATED: "REPLICA_GATED",
|
|
60
|
+
/** Quota admission refused a page (`maxBytes` or the free-space floor); nothing committed. */
|
|
61
|
+
STORAGE_BUDGET: "STORAGE_BUDGET",
|
|
62
|
+
/** The consent deadline passed: the grant is not served until the user consents again. */
|
|
63
|
+
CONSENT_EXPIRED: "CONSENT_EXPIRED"
|
|
58
64
|
};
|
|
59
65
|
var REPLICA_ERROR = /* @__PURE__ */ Symbol.for("tinycloud.replica.error");
|
|
60
66
|
var ReplicaError = class extends Error {
|
package/dist/browser.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/errors.ts","../../sdk-services/src/kv/replication/prefix.ts","../src/scope.ts","../src/did.ts","../src/browser/index.ts"],"sourcesContent":["/**\n * Typed replica errors. Every failure a caller can act on has its own code;\n * hosts (CLI, worker) map codes to their own exit codes or UI states.\n */\nexport const ReplicaErrorCode = {\n /** Another process holds the sync lease. */\n BUSY: \"REPLICA_BUSY\",\n /** No replica with this name exists. */\n NOT_FOUND: \"REPLICA_NOT_FOUND\",\n /** The replica exists but its stored configuration differs from the request. */\n CONFIG_MISMATCH: \"REPLICA_CONFIG_MISMATCH\",\n /** The runtime has no supported durable store (e.g. Node < 22.13). */\n RUNTIME_UNSUPPORTED: \"RUNTIME_UNSUPPORTED\",\n /** A required open() argument is missing or malformed (e.g. `principal`). */\n INVALID_ARGUMENT: \"REPLICA_INVALID_ARGUMENT\",\n /** The local store failed (I/O, corruption, schema). */\n STORAGE_ERROR: \"STORAGE_ERROR\",\n /** The local disk is full. */\n STORAGE_FULL: \"STORAGE_FULL\",\n /** The key is outside the replica's prefix. */\n NOT_COVERED: \"NOT_COVERED\",\n /** Syncing the secrets space or the vault namespace needs an explicit opt-in. */\n SECRETS_OPT_IN_REQUIRED: \"SECRETS_OPT_IN_REQUIRED\",\n /** No grant is installed, or none covers the requested scope. */\n GRANT_MISSING: \"GRANT_MISSING\",\n /** The grant bytes are not a verifiable UCAN, or its signature is wrong. */\n GRANT_INVALID: \"GRANT_INVALID\",\n /** SIWE/CACAO and other non-UCAN grants cannot be used for a replica. */\n GRANT_FORMAT_UNSUPPORTED: \"GRANT_FORMAT_UNSUPPORTED\",\n /** The grant was issued to a different device. */\n GRANT_AUDIENCE_MISMATCH: \"GRANT_AUDIENCE_MISMATCH\",\n /** The grant lacks `tinycloud.kv/sync` or `tinycloud.kv/get` on the prefix. */\n GRANT_NOT_COVERING: \"GRANT_NOT_COVERING\",\n GRANT_NOT_YET_VALID: \"GRANT_NOT_YET_VALID\",\n GRANT_EXPIRED: \"GRANT_EXPIRED\",\n /** The configured node does not advertise whole-space KV sync support. */\n REPLICATION_ROOT_UNSUPPORTED: \"REPLICATION_ROOT_UNSUPPORTED\",\n GRANT_REVOKED: \"GRANT_REVOKED\",\n /** The node refused the invocation for a reason other than revocation. */\n GRANT_UNAUTHORIZED: \"GRANT_UNAUTHORIZED\",\n /** The node refused the retention grant. */\n RETENTION_GRANT_REFUSED: \"RETENTION_GRANT_REFUSED\",\n /** The node could not be reached. */\n NETWORK_ERROR: \"NETWORK_ERROR\",\n /** The node answered with an unexpected status. */\n NODE_ERROR: \"NODE_ERROR\",\n /** The node's answer violates the wire contract. */\n PROTOCOL_ERROR: \"PROTOCOL_ERROR\",\n /** The node asked for a fresh bootstrap (410). Handled inside sync. */\n RESET_REQUIRED: \"RESET_REQUIRED\",\n /** The feed now comes from a different node than the one pinned. */\n SOURCE_CHANGED: \"SOURCE_CHANGED\",\n /** The feed delivered a key outside the replica's prefix. */\n SCOPE_VIOLATION: \"SCOPE_VIOLATION\",\n /** Fetched bytes do not hash to the ETag the node attested for them. */\n CONTENT_MISMATCH: \"CONTENT_MISMATCH\",\n /** A stored blob no longer hashes to its name. */\n INTEGRITY_ERROR: \"INTEGRITY_ERROR\",\n /** The replica handle was closed; in-flight calls settle with this code. */\n CLOSED: \"REPLICA_CLOSED\",\n} as const;\n\nexport type ReplicaErrorCode = (typeof ReplicaErrorCode)[keyof typeof ReplicaErrorCode];\n\n/**\n * The package ships separate entry bundles (`.`, `./sqlite`), each with its own\n * copy of this class, so identity is a registered symbol, not `instanceof`.\n */\nconst REPLICA_ERROR = Symbol.for(\"tinycloud.replica.error\");\n\nexport class ReplicaError extends Error {\n readonly [REPLICA_ERROR] = true;\n readonly code: ReplicaErrorCode;\n readonly detail?: Record<string, unknown>;\n\n constructor(code: ReplicaErrorCode, message: string, detail?: Record<string, unknown>, options?: { cause?: unknown }) {\n super(message, options);\n this.name = \"ReplicaError\";\n this.code = code;\n if (detail !== undefined) this.detail = detail;\n }\n}\n\nexport function isReplicaError(error: unknown, code?: ReplicaErrorCode): error is ReplicaError {\n return (\n typeof error === \"object\" &&\n error !== null &&\n (error as { [REPLICA_ERROR]?: unknown })[REPLICA_ERROR] === true &&\n (code === undefined || (error as ReplicaError).code === code)\n );\n}\n","/** Shared prefix boundary and configuration rules; no runtime dependencies. */\nexport function kvPrefixCovers(prefix: string, key: string): boolean {\n return prefix === \"/\" || key.startsWith(prefix);\n}\n\n/** Replication scopes are `/` or non-empty, slash-terminated folder paths. */\nexport function assertFolderPrefix(prefix: unknown): asserts prefix is string {\n const validFolder = typeof prefix === \"string\" &&\n prefix.length > 0 &&\n !prefix.startsWith(\"/\") &&\n !prefix.includes(\"//\") &&\n prefix.endsWith(\"/\");\n if (prefix !== \"/\" && !validFolder) {\n const folderHint = typeof prefix === \"string\"\n ? prefix.replace(/^\\/+/, \"\").replace(/\\/+$/, \"\")\n : \"\";\n const suggestion = folderHint.length > 0 && !folderHint.includes(\"//\")\n ? ` Use ${JSON.stringify(`${folderHint}/`)}.`\n : ' Use \"/\" or a slash-terminated folder path such as \"notes/\".';\n throw new TypeError(`Replication prefix must be \"/\" or a non-empty slash-terminated folder path without empty segments.${suggestion}`);\n }\n}\n","import { ReplicaError, ReplicaErrorCode } from \"./errors.js\";\nimport type { AuthorityState, AuthorityWindow, GrantRecord, LocalReadPolicy } from \"./types.js\";\nimport { assertFolderPrefix as assertPureFolderPrefix } from \"@tinycloud/sdk-services/kv/replication/prefix\";\n\n/** Replication prefixes are the whole space (`/`) or a slash-terminated folder. */\nexport function assertFolderPrefix(prefix: unknown): asserts prefix is string {\n try {\n assertPureFolderPrefix(prefix);\n } catch {\n const folderHint = typeof prefix === \"string\"\n ? prefix.replace(/^\\/+/, \"\").replace(/\\/+$/, \"\")\n : \"\";\n const suggestion = prefix === \"\" ? 'use \"/\" for the whole space' :\n folderHint.length > 0 && !folderHint.includes(\"//\")\n ? `use ${JSON.stringify(`${folderHint}/`)}`\n : 'use \"/\" for the whole space or a folder path such as \"notes/\"';\n throw new ReplicaError(\n ReplicaErrorCode.INVALID_ARGUMENT,\n `Replication prefix ${JSON.stringify(prefix)} must be \"/\" or a non-empty folder path ending in / without empty segments; ${suggestion}. Run \\`tc replica reset --replica <name> --purge\\` and recreate it with the folder prefix.`,\n );\n }\n}\n\n/** Prefix coverage shared with the KV replication feed. */\nexport function kvPrefixCovers(prefix: string, key: string): boolean {\n return prefix === \"/\" || key.startsWith(prefix);\n}\n/** Capability containment: exact grants cover exact keys; slash grants cover descendants. */\nexport function authorityPathCovers(grantPath: string, requestedPath: string): boolean {\n if (grantPath === \"/\") return true;\n if (grantPath.endsWith(\"/\")) return requestedPath.startsWith(grantPath);\n return requestedPath === grantPath;\n}\n\n/**\n * Syncing the owner's secrets space, or any prefix overlapping the vault\n * namespace (including bare `vault`), copies encrypted secret material to\n * disk and needs an explicit opt-in (TC-733).\n */\nexport function requiresSecretsOptIn(space: string, prefix: string): boolean {\n if (prefix === \"/\") return true;\n if (space === \"secrets\" || space.endsWith(\":secrets\")) return true;\n const firstSegment = prefix.split(\"/\", 1)[0];\n return firstSegment === \"vault\";\n}\n\n/** `\"blake3-<hex>\"` (quoted or not) → the 64-hex hash, or undefined. */\nexport function hashFromEtag(etag: string): string | undefined {\n const match = /^\"?blake3-([0-9a-f]{64})\"?$/.exec(etag);\n return match?.[1];\n}\n\nconst RFC3339 = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:\\d{2})$/;\n\n/** An RFC 3339 instant that parses: the only form an authority bound may take. */\nexport function isInstant(value: string): boolean {\n return RFC3339.test(value) && Number.isFinite(Date.parse(value));\n}\n\nexport type EffectiveAuthority = {\n state: AuthorityState;\n notBefore: string | null;\n expiresAt: string | null;\n retainUntil: string | null;\n /** The retention grant was revoked: after expiry, reads are revoked rather than expired. */\n retentionRevoked: boolean;\n};\n\nfunction minIso(a: string | null, bSeconds: number | null): string | null {\n if (bSeconds === null) return a;\n const b = new Date(bSeconds * 1000).toISOString();\n if (a === null) return b;\n return Date.parse(a) <= Date.parse(b) ? a : b;\n}\n\nfunction maxIso(a: string | null, bSeconds: number | null): string | null {\n if (bSeconds === null) return a;\n const b = new Date(bSeconds * 1000).toISOString();\n if (a === null) return b;\n return Date.parse(a) >= Date.parse(b) ? a : b;\n}\n\n/**\n * The window local reads are enforced against: the node-attested window from\n * the last successful sync, bounded by the installed leaf grant (an upper\n * bound only — a multi-parent ancestor can end the chain earlier, which only\n * the node can see).\n */\nexport function effectiveAuthority(input: {\n window: AuthorityWindow | null;\n grant: GrantRecord | null;\n revoked: string | null;\n retentionRevoked?: string | null;\n policy: LocalReadPolicy;\n now: number;\n}): EffectiveAuthority {\n const notBefore = maxIso(input.window?.notBefore ?? null, input.grant?.notBefore ?? null);\n const expiresAt = minIso(input.window?.expiresAt ?? null, input.grant?.expiresAt ?? null);\n const retainUntil = input.policy === \"retainAfterExpiry\" ? input.window?.retainUntil ?? null : null;\n let state: AuthorityState = \"valid\";\n if (input.revoked !== null) state = \"revoked\";\n else if (notBefore !== null && input.now < Date.parse(notBefore)) state = \"not-yet-valid\";\n else if (expiresAt !== null && input.now >= Date.parse(expiresAt)) state = \"expired\";\n return { state, notBefore, expiresAt, retainUntil, retentionRevoked: (input.retentionRevoked ?? null) !== null };\n}\n\n/**\n * Throws unless local reads are allowed now. Returns the reported authority\n * state: `valid`, or `expired` while a node-attested retention holds.\n */\nexport function assertReadable(authority: EffectiveAuthority, now: number): AuthorityState {\n switch (authority.state) {\n case \"valid\":\n return \"valid\";\n case \"revoked\":\n throw new ReplicaError(ReplicaErrorCode.GRANT_REVOKED, \"The replica's grant was revoked; local reads are blocked.\");\n case \"not-yet-valid\":\n throw new ReplicaError(\n ReplicaErrorCode.GRANT_NOT_YET_VALID,\n `The replica's grant is not valid before ${authority.notBefore}.`,\n );\n case \"expired\":\n if (authority.retentionRevoked) {\n throw new ReplicaError(\n ReplicaErrorCode.GRANT_REVOKED,\n `The replica's grant expired at ${authority.expiresAt} and its retention grant was revoked; local reads are blocked.`,\n );\n }\n if (authority.retainUntil !== null && now < Date.parse(authority.retainUntil)) return \"expired\";\n throw new ReplicaError(\n ReplicaErrorCode.GRANT_EXPIRED,\n authority.retainUntil === null\n ? `The replica's grant expired at ${authority.expiresAt}; local reads are blocked.`\n : `The replica's retention ended at ${authority.retainUntil}; local reads are blocked.`,\n );\n }\n}\n","/**\n * `did:key` verification method fragment → the principal DID.\n */\nexport function principalOf(did: string): string {\n return did.split(\"#\", 1)[0]!;\n}\n\nconst METHOD = /^[a-z0-9]+$/u;\n// DID method-specific-id: idchars ([A-Za-z0-9._-]) or %XX escapes, with `:`\n// separators allowed; the final segment must be non-empty.\nconst IDCHAR = /^[A-Za-z0-9._-]$/u;\n\n/**\n * Whether `value` is a well-formed DID the replica accepts as a `principal`\n * partition label: `did:` + a lowercase method name + a non-empty\n * method-specific-id whose segments (separated by `:`) consist of idchars or\n * valid `%XX` percent-escapes, and whose last segment is not empty. Shape\n * only — `principal` is an app-asserted label, never a credential.\n */\nexport function isPrincipalDid(value: string): boolean {\n if (typeof value !== \"string\" || !value.startsWith(\"did:\")) return false;\n const rest = value.slice(4);\n const methodEnd = rest.indexOf(\":\");\n if (methodEnd <= 0) return false;\n if (!METHOD.test(rest.slice(0, methodEnd))) return false;\n const msid = rest.slice(methodEnd + 1);\n if (msid.length === 0) return false;\n const segments = msid.split(\":\");\n const last = segments[segments.length - 1]!;\n if (last.length === 0) return false;\n for (const segment of segments) {\n for (let i = 0; i < segment.length; i += 1) {\n const c = segment[i]!;\n if (c === \"%\") {\n // A percent-escape needs exactly two hex digits.\n if (i + 2 >= segment.length || !/^[0-9a-fA-F]{2}$/u.test(segment.slice(i + 1, i + 3))) return false;\n i += 2;\n } else if (!IDCHAR.test(c)) {\n return false;\n }\n }\n }\n return true;\n}\n","/**\n * The main-thread browser API (TC-19): a thin RPC client over the replica\n * worker. IndexedDB, the network and the device key live in the worker; the\n * client's local reads never touch the network, and its sync calls are the\n * only path that does.\n *\n * The worker ships as `dist/replica.worker.js`, a self-contained ESM bundle\n * spawned via `new Worker(new URL(\"./replica.worker.js\", import.meta.url),\n * { type: \"module\" })` — webpack 5 and Vite detect that form. A `worker`\n * option accepts a `Worker` or `URL` escape hatch. In Vite dev mode, list\n * `@tinycloud/replica` in `optimizeDeps.exclude` so the worker URL resolves\n * inside the package (vitejs/vite#20859).\n *\n * Trust boundary: the browser origin, exactly as the OS user is the CLI's.\n * Run one app per origin — any same-origin script can open the IndexedDB\n * databases directly. `principal` (required) is the signed-in user's\n * identity DID; it partitions replicas per user on that origin, it is not\n * an authorization check — the node authorizes at sync time, and local\n * reads are gated by the stored grant's node-attested window, like the CLI.\n */\nimport { ReplicaError, ReplicaErrorCode } from \"../errors.js\";\nimport { assertFolderPrefix } from \"../scope.js\";\nimport { isPrincipalDid } from \"../did.js\";\nimport type {\n GetResult,\n ListResult,\n OpenResult,\n ReplicaReply,\n StatusResult,\n SyncResult,\n} from \"./protocol.js\";\n\nexport { ReplicaError, ReplicaErrorCode };\n\n/**\n * `reset()` result. With `purge: true` the replica's contents are erased in\n * a single commit — the database file itself is never deleted: a small\n * empty database named by a hash remains (holding no user data) and the\n * next `openReplica` reinitializes it in place. Once the commit lands the\n * result is final `{ purged: true }` — nothing after it can change the\n * outcome.\n */\nexport type ResetResult = { reset: true; purged: boolean };\n\nexport type BrowserReplicaOpenOptions = {\n /** The TinyCloud host to sync from; pinned for the replica's life. */\n host: string;\n /** The space id (native form) the replica covers. */\n space: string;\n /** The KV prefix: `/` for the whole space, or a folder such as `notes/`. */\n prefix: string;\n /** A display name; defaults to the prefix. */\n name?: string;\n /**\n * The signed-in user's identity DID (for example `did:pkh:eip155:1:0x…`),\n * as the app knows it. Replicas for different principals get different\n * databases on the same origin; signing in again and installing the new\n * session's grant continues the same replica from its cursor. This is an\n * app-asserted partition label, not an authorization check — the trust\n * boundary is the browser origin, and the node authorizes at sync time.\n */\n principal: string;\n /** Replicating the `secrets` space or a `vault/` prefix needs this opt-in. */\n allowSecrets?: boolean;\n /**\n * Spawn this worker instead of the bundled one (custom bundler layouts).\n * A factory (`() => Worker`) is also accepted: it runs after `principal`\n * validation, so a bad open never spawns a worker — useful for tests.\n */\n worker?: Worker | URL | string | (() => Worker);\n};\n\nexport type BrowserReplicaGetResult = GetResult;\nexport type BrowserReplicaListResult = ListResult;\nexport type BrowserReplicaStatus = StatusResult;\nexport type BrowserReplicaSyncResult = SyncResult;\nexport type BrowserReplicaOpenResult = OpenResult;\n\n/** Subscriptions for worker events (other tabs' commits, resets). */\nexport type BrowserReplicaEvents = {\n /** Another tab's worker committed a page (`serial` is monotonic). */\n onCommitted?: (serial: number) => void;\n /** The replica's authority state changed. */\n onAuthority?: (state: string) => void;\n /** The replica was reset in this tab or observed elsewhere. */\n onReset?: (reason: string) => void;\n};\n\ntype Pending = {\n resolve: (result: unknown) => void;\n reject: (error: Error) => void;\n};\n\nfunction workerFrom(source: Worker | URL | string | (() => Worker) | undefined): Worker {\n if (source !== undefined) {\n if (typeof Worker !== \"undefined\" && source instanceof Worker) return source;\n if (source instanceof URL || typeof source === \"string\") return new Worker(source, { type: \"module\" });\n if (typeof source === \"function\") return source();\n }\n return new Worker(new URL(\"./replica.worker.js\", import.meta.url), { type: \"module\" });\n}\n\n/**\n * Open (or create) a replica in a dedicated worker. The first call also asks\n * the browser for durable storage (`navigator.storage.persist()`), so an\n * eviction under pressure is less likely; `status()` reports the outcome.\n * In environments without IndexedDB the returned promise rejects with\n * `RUNTIME_UNSUPPORTED` — there is no in-memory fallback, reads would lose\n * their durability guarantee.\n *\n * `principal` partitions the replica per signed-in user and is required: the\n * same host/space/prefix under two principals yields two databases. A device\n * grant chained to a web session (about one hour under an OpenKey session)\n * gives about that session's length of offline reads; after sign-in rotates\n * the session key, install the new grant and the replica continues from its\n * cursor.\n */\nexport async function openReplica(options: BrowserReplicaOpenOptions, events: BrowserReplicaEvents = {}): Promise<BrowserReplica> {\n assertFolderPrefix(options.prefix);\n // Validate before workerFrom(): a bad principal or prefix never spawns a worker.\n if (typeof options.principal !== \"string\" || !isPrincipalDid(options.principal)) {\n throw new ReplicaError(\n ReplicaErrorCode.INVALID_ARGUMENT,\n `openReplica() requires \\`principal\\`, the signed-in user's identity DID (got ${JSON.stringify(options.principal)}).`,\n );\n }\n const worker = workerFrom(options.worker);\n const replica = new BrowserReplica(worker, events);\n try {\n await replica.ready;\n const persisted =\n typeof navigator !== \"undefined\" && typeof navigator.storage?.persist === \"function\"\n ? await navigator.storage.persist().catch(() => undefined)\n : undefined;\n // `worker` selects the process, it is not a wire field: do not forward it.\n return await replica.open({ ...options, worker: undefined, persisted });\n } catch (error) {\n // Any startup or open() failure abandons this replica: reject in-flight\n // calls, terminate the worker, and never leak a live Worker the caller\n // cannot reach.\n await replica.close().catch(() => undefined);\n worker.terminate();\n throw error;\n }\n}\n\nexport class BrowserReplica {\n /** The open() result: replicaId, deviceDid and the status at open time. */\n opened: OpenResult | null = null;\n\n readonly #worker: Worker;\n readonly #events: BrowserReplicaEvents;\n /** Settles when the worker reports it is listening (or fails to start). */\n readonly ready: Promise<void>;\n #nextId = 1;\n #pending = new Map<number, Pending>();\n #closed = false;\n\n /** For tests and advanced use: drive an already-spawned worker. */\n constructor(worker: Worker, events: BrowserReplicaEvents = {}) {\n this.#worker = worker;\n this.#events = events;\n const ready = Promise.withResolvers<void>();\n this.ready = ready.promise;\n // A close() before the worker reports ready rejects `ready`; not every\n // consumer awaits it, so mark that rejection handled up front.\n ready.promise.catch(() => undefined);\n this.#readyResolve = ready.resolve;\n this.#readyReject = ready.reject;\n worker.onmessage = (message: MessageEvent) => this.#onMessage(message.data);\n worker.onerror = (event) => {\n const error = new Error(event.message ?? \"The replica worker failed.\");\n this.#readyReject(error);\n this.#failAll(error);\n };\n }\n\n #readyResolve: () => void;\n #readyReject: (error: Error) => void;\n\n #onMessage(data: unknown): void {\n if (typeof data === \"object\" && data !== null && \"event\" in data) {\n const event = (data as { event: { event?: unknown } }).event;\n switch (event.event) {\n case \"ready\":\n this.#readyResolve();\n return;\n case \"committed\":\n if (typeof (event as { serial?: unknown }).serial === \"number\") {\n this.#events.onCommitted?.((event as { serial: number }).serial);\n }\n return;\n case \"authority\":\n if (typeof (event as { state?: unknown }).state === \"string\") {\n this.#events.onAuthority?.((event as { state: string }).state);\n }\n return;\n case \"reset\":\n if (typeof (event as { reason?: unknown }).reason === \"string\") {\n this.#events.onReset?.((event as { reason: string }).reason);\n }\n return;\n default:\n return;\n }\n }\n const reply = data as ReplicaReply;\n if (typeof reply.id !== \"number\") return;\n const pending = this.#pending.get(reply.id);\n if (pending === undefined) return;\n this.#pending.delete(reply.id);\n if (reply.ok) {\n pending.resolve(reply.result);\n } else {\n pending.reject(new ReplicaError(reply.err.code as ReplicaErrorCode, reply.err.message, reply.err.detail));\n }\n }\n\n #failAll(error: Error): void {\n for (const pending of this.#pending.values()) pending.reject(error);\n this.#pending.clear();\n }\n\n async #call<T>(request: Record<string, unknown>): Promise<T> {\n if (this.#closed) throw new ReplicaError(ReplicaErrorCode.CLOSED, \"The replica is closed.\");\n const id = this.#nextId++;\n const { promise, resolve, reject } = Promise.withResolvers<unknown>();\n this.#pending.set(id, { resolve, reject });\n this.#worker.postMessage({ id, ...request });\n return (await promise) as T;\n }\n\n /** Open or create the replica; `openReplica` calls this for you. */\n async open(options: BrowserReplicaOpenOptions & { persisted?: boolean }): Promise<BrowserReplica> {\n this.opened = await this.#call<OpenResult>({\n op: \"open\",\n host: options.host,\n space: options.space,\n prefix: options.prefix,\n principal: options.principal,\n ...(options.name === undefined ? {} : { name: options.name }),\n ...(options.allowSecrets === undefined ? {} : { allowSecrets: options.allowSecrets }),\n ...(options.persisted === undefined ? {} : { persisted: options.persisted }),\n });\n return this;\n }\n\n /** The DID the app delegates to (`tcw.delegateTo(deviceDid, […sync…])`). */\n get deviceDid(): string {\n if (this.opened === null) throw new Error(\"open() has not completed.\");\n return this.opened.deviceDid;\n }\n\n /** Install a signed grant: the compact UCAN (`delegation.delegationHeader.Authorization`). */\n installGrant(delegation: string): Promise<{ cid: string; audience: string; expiresAt: number | null }> {\n return this.#call({ op: \"installGrant\", delegation });\n }\n\n /** Pull the feed and commit each page atomically. `busy` when another tab syncs. */\n sync(options: { limit?: number } = {}): Promise<SyncResult> {\n return this.#call({ op: \"sync\", ...(options.limit === undefined ? {} : { limit: options.limit }) });\n }\n\n /** Local read: never touches the network. */\n get(key: string): Promise<GetResult> {\n return this.#call({ op: \"get\", key });\n }\n\n /** Local read: never touches the network. */\n list(options: { prefix?: string; after?: string; limit?: number } = {}): Promise<ListResult> {\n return this.#call({ op: \"list\", ...options });\n }\n\n /** Replica status plus browser persistence facts. */\n status(): Promise<StatusResult> {\n return this.#call({ op: \"status\" });\n }\n\n /** Opt in to post-expiry reads under a `tinycloud.kv/retain` grant. */\n setRetentionGrant(cid: string | null): Promise<{ retentionGrantCid: string | null }> {\n return this.#call({ op: \"setRetention\", grantCid: cid });\n }\n\n /**\n * Clear entries and cursor (keeps config and grant); `purge` erases the\n * replica permanently in place — the database file is never deleted, so\n * the result is final the moment it resolves (see {@link ResetResult}).\n */\n reset(options: { purge?: boolean } = {}): Promise<ResetResult> {\n return this.#call({ op: \"reset\", purge: options.purge === true });\n }\n\n /** Close the worker-side handles and terminate the worker. */\n async close(): Promise<void> {\n if (this.#closed) return;\n this.#closed = true;\n this.#readyReject(new ReplicaError(ReplicaErrorCode.CLOSED, \"The replica is closed.\"));\n // Shutdown starts now: every request already in flight rejects with\n // REPLICA_CLOSED before the close handshake is even posted, so a reply\n // arriving mid-close finds no entry and is ignored — never resolves with\n // data. The handshake itself is registered only after that sweep.\n this.#failAll(new ReplicaError(ReplicaErrorCode.CLOSED, \"The replica is closed.\"));\n const id = this.#nextId++;\n const { promise, resolve } = Promise.withResolvers<unknown>();\n this.#pending.set(id, { resolve, reject: resolve });\n this.#worker.postMessage({ id, op: \"close\" });\n const timeout = setTimeout(() => {\n this.#pending.delete(id);\n resolve(undefined);\n }, 2_000);\n await promise.catch(() => undefined).finally(() => clearTimeout(timeout));\n this.#worker.terminate();\n this.#failAll(new ReplicaError(ReplicaErrorCode.CLOSED, \"The replica is closed.\"));\n }\n}\n"],"mappings":"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","names":["assertFolderPrefix","assertFolderPrefix"]}
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../../sdk-services/src/kv/replication/prefix.ts","../src/scope.ts","../src/did.ts","../src/browser/index.ts"],"sourcesContent":["/**\n * Typed replica errors. Every failure a caller can act on has its own code;\n * hosts (CLI, worker) map codes to their own exit codes or UI states.\n */\nexport const ReplicaErrorCode = {\n /** Another process holds the sync lease. */\n BUSY: \"REPLICA_BUSY\",\n /** No replica with this name exists. */\n NOT_FOUND: \"REPLICA_NOT_FOUND\",\n /** The replica exists but its stored configuration differs from the request. */\n CONFIG_MISMATCH: \"REPLICA_CONFIG_MISMATCH\",\n /** The runtime has no supported durable store (e.g. Node < 22.13). */\n RUNTIME_UNSUPPORTED: \"RUNTIME_UNSUPPORTED\",\n /** A required open() argument is missing or malformed (e.g. `principal`). */\n INVALID_ARGUMENT: \"REPLICA_INVALID_ARGUMENT\",\n /** The local store failed (I/O, corruption, schema). */\n STORAGE_ERROR: \"STORAGE_ERROR\",\n /** The local disk is full. */\n STORAGE_FULL: \"STORAGE_FULL\",\n /** The key is outside the replica's prefix. */\n NOT_COVERED: \"NOT_COVERED\",\n /** Syncing the secrets space or the vault namespace needs an explicit opt-in. */\n SECRETS_OPT_IN_REQUIRED: \"SECRETS_OPT_IN_REQUIRED\",\n /** No grant is installed, or none covers the requested scope. */\n GRANT_MISSING: \"GRANT_MISSING\",\n /** The grant bytes are not a verifiable UCAN, or its signature is wrong. */\n GRANT_INVALID: \"GRANT_INVALID\",\n /** SIWE/CACAO and other non-UCAN grants cannot be used for a replica. */\n GRANT_FORMAT_UNSUPPORTED: \"GRANT_FORMAT_UNSUPPORTED\",\n /** The grant was issued to a different device. */\n GRANT_AUDIENCE_MISMATCH: \"GRANT_AUDIENCE_MISMATCH\",\n /** The grant lacks `tinycloud.kv/sync` or `tinycloud.kv/get` on the prefix. */\n GRANT_NOT_COVERING: \"GRANT_NOT_COVERING\",\n GRANT_NOT_YET_VALID: \"GRANT_NOT_YET_VALID\",\n GRANT_EXPIRED: \"GRANT_EXPIRED\",\n /** The configured node does not advertise whole-space KV sync support. */\n REPLICATION_ROOT_UNSUPPORTED: \"REPLICATION_ROOT_UNSUPPORTED\",\n GRANT_REVOKED: \"GRANT_REVOKED\",\n /** The node refused the invocation for a reason other than revocation. */\n GRANT_UNAUTHORIZED: \"GRANT_UNAUTHORIZED\",\n /** The node refused the retention grant. */\n RETENTION_GRANT_REFUSED: \"RETENTION_GRANT_REFUSED\",\n /** The node could not be reached. */\n NETWORK_ERROR: \"NETWORK_ERROR\",\n /** The node answered with an unexpected status. */\n NODE_ERROR: \"NODE_ERROR\",\n /** The node's answer violates the wire contract. */\n PROTOCOL_ERROR: \"PROTOCOL_ERROR\",\n /** The node asked for a fresh bootstrap (410). Handled inside sync. */\n RESET_REQUIRED: \"RESET_REQUIRED\",\n /** The feed now comes from a different node than the one pinned. */\n SOURCE_CHANGED: \"SOURCE_CHANGED\",\n /** The feed delivered a key outside the replica's prefix. */\n SCOPE_VIOLATION: \"SCOPE_VIOLATION\",\n /** Fetched bytes do not hash to the ETag the node attested for them. */\n CONTENT_MISMATCH: \"CONTENT_MISMATCH\",\n /** A stored blob no longer hashes to its name. */\n INTEGRITY_ERROR: \"INTEGRITY_ERROR\",\n /** The replica handle was closed; in-flight calls settle with this code. */\n CLOSED: \"REPLICA_CLOSED\",\n /** The lifecycle gate refused the operation (shared contract `REPLICA_GATED`); `detail.gate` says why. */\n GATED: \"REPLICA_GATED\",\n /** Quota admission refused a page (`maxBytes` or the free-space floor); nothing committed. */\n STORAGE_BUDGET: \"STORAGE_BUDGET\",\n /** The consent deadline passed: the grant is not served until the user consents again. */\n CONSENT_EXPIRED: \"CONSENT_EXPIRED\",\n} as const;\n\nexport type ReplicaErrorCode = (typeof ReplicaErrorCode)[keyof typeof ReplicaErrorCode];\n\n/**\n * The package ships separate entry bundles (`.`, `./sqlite`), each with its own\n * copy of this class, so identity is a registered symbol, not `instanceof`.\n */\nconst REPLICA_ERROR = Symbol.for(\"tinycloud.replica.error\");\n\nexport class ReplicaError extends Error {\n readonly [REPLICA_ERROR] = true;\n readonly code: ReplicaErrorCode;\n readonly detail?: Record<string, unknown>;\n\n constructor(code: ReplicaErrorCode, message: string, detail?: Record<string, unknown>, options?: { cause?: unknown }) {\n super(message, options);\n this.name = \"ReplicaError\";\n this.code = code;\n if (detail !== undefined) this.detail = detail;\n }\n}\n\nexport function isReplicaError(error: unknown, code?: ReplicaErrorCode): error is ReplicaError {\n return (\n typeof error === \"object\" &&\n error !== null &&\n (error as { [REPLICA_ERROR]?: unknown })[REPLICA_ERROR] === true &&\n (code === undefined || (error as ReplicaError).code === code)\n );\n}\n\n/**\n * A lifecycle-gate refusal in the shared contract's shape:\n * `Error & { code: \"REPLICA_GATED\"; gate }`, with `gate` also in `detail`\n * so it survives the worker boundary.\n */\nexport function gatedError(gate: string, message: string): ReplicaError & { gate: string } {\n return Object.assign(new ReplicaError(ReplicaErrorCode.GATED, message, { gate }), { gate });\n}\n","/** Shared prefix boundary and configuration rules; no runtime dependencies. */\nexport function kvPrefixCovers(prefix: string, key: string): boolean {\n return prefix === \"/\" || key.startsWith(prefix);\n}\n\n/** Replication scopes are `/` or non-empty, slash-terminated folder paths. */\nexport function assertFolderPrefix(prefix: unknown): asserts prefix is string {\n const validFolder = typeof prefix === \"string\" &&\n prefix.length > 0 &&\n !prefix.startsWith(\"/\") &&\n !prefix.includes(\"//\") &&\n prefix.endsWith(\"/\");\n if (prefix !== \"/\" && !validFolder) {\n const folderHint = typeof prefix === \"string\"\n ? prefix.replace(/^\\/+/, \"\").replace(/\\/+$/, \"\")\n : \"\";\n const suggestion = folderHint.length > 0 && !folderHint.includes(\"//\")\n ? ` Use ${JSON.stringify(`${folderHint}/`)}.`\n : ' Use \"/\" or a slash-terminated folder path such as \"notes/\".';\n throw new TypeError(`Replication prefix must be \"/\" or a non-empty slash-terminated folder path without empty segments.${suggestion}`);\n }\n}\n","import { ReplicaError, ReplicaErrorCode } from \"./errors.js\";\nimport type { AuthorityState, AuthorityWindow, GrantRecord, LocalReadPolicy } from \"./types.js\";\nimport { assertFolderPrefix as assertPureFolderPrefix } from \"@tinycloud/sdk-services/kv/replication/prefix\";\n\n/** Replication prefixes are the whole space (`/`) or a slash-terminated folder. */\nexport function assertFolderPrefix(prefix: unknown): asserts prefix is string {\n try {\n assertPureFolderPrefix(prefix);\n } catch {\n const folderHint = typeof prefix === \"string\"\n ? prefix.replace(/^\\/+/, \"\").replace(/\\/+$/, \"\")\n : \"\";\n const suggestion = prefix === \"\" ? 'use \"/\" for the whole space' :\n folderHint.length > 0 && !folderHint.includes(\"//\")\n ? `use ${JSON.stringify(`${folderHint}/`)}`\n : 'use \"/\" for the whole space or a folder path such as \"notes/\"';\n throw new ReplicaError(\n ReplicaErrorCode.INVALID_ARGUMENT,\n `Replication prefix ${JSON.stringify(prefix)} must be \"/\" or a non-empty folder path ending in / without empty segments; ${suggestion}. Run \\`tc replica reset --replica <name> --purge\\` and recreate it with the folder prefix.`,\n );\n }\n}\n\n/** Prefix coverage shared with the KV replication feed. */\nexport function kvPrefixCovers(prefix: string, key: string): boolean {\n return prefix === \"/\" || key.startsWith(prefix);\n}\n/** Capability containment: exact grants cover exact keys; slash grants cover descendants. */\nexport function authorityPathCovers(grantPath: string, requestedPath: string): boolean {\n if (grantPath === \"/\") return true;\n if (grantPath.endsWith(\"/\")) return requestedPath.startsWith(grantPath);\n return requestedPath === grantPath;\n}\n\n/**\n * Syncing the owner's secrets space, or any prefix overlapping the vault\n * namespace (including bare `vault`), copies encrypted secret material to\n * disk and needs an explicit opt-in (TC-733).\n */\nexport function requiresSecretsOptIn(space: string, prefix: string): boolean {\n if (prefix === \"/\") return true;\n if (space === \"secrets\" || space.endsWith(\":secrets\")) return true;\n const firstSegment = prefix.split(\"/\", 1)[0];\n return firstSegment === \"vault\";\n}\n\n/** Browser read-through replicas never retain secrets or vault data, even with legacy opt-in. */\nexport function assertBrowserReadThroughScope(space: string, prefix: string, allowSecrets: boolean): void {\n if (allowSecrets || requiresSecretsOptIn(space, prefix) || prefix.split(\"/\", 1)[0] === \"secrets\") {\n throw new ReplicaError(\n ReplicaErrorCode.INVALID_ARGUMENT,\n \"Browser read-through replicas require allowSecrets=false and exclude the whole space, secrets and vault prefixes.\",\n );\n }\n}\n\n/** `\"blake3-<hex>\"` (quoted or not) → the 64-hex hash, or undefined. */\nexport function hashFromEtag(etag: string): string | undefined {\n const match = /^\"?blake3-([0-9a-f]{64})\"?$/.exec(etag);\n return match?.[1];\n}\n\nconst RFC3339 = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?(?:Z|[+-]\\d{2}:\\d{2})$/;\n\n/** An RFC 3339 instant that parses: the only form an authority bound may take. */\nexport function isInstant(value: string): boolean {\n return RFC3339.test(value) && Number.isFinite(Date.parse(value));\n}\n\nexport type EffectiveAuthority = {\n state: AuthorityState;\n notBefore: string | null;\n expiresAt: string | null;\n retainUntil: string | null;\n /** The retention grant was revoked: after expiry, reads are revoked rather than expired. */\n retentionRevoked: boolean;\n};\n\nfunction minIso(a: string | null, bSeconds: number | null): string | null {\n if (bSeconds === null) return a;\n const b = new Date(bSeconds * 1000).toISOString();\n if (a === null) return b;\n return Date.parse(a) <= Date.parse(b) ? a : b;\n}\n\nfunction maxIso(a: string | null, bSeconds: number | null): string | null {\n if (bSeconds === null) return a;\n const b = new Date(bSeconds * 1000).toISOString();\n if (a === null) return b;\n return Date.parse(a) >= Date.parse(b) ? a : b;\n}\n\n/**\n * The window local reads are enforced against: the node-attested window from\n * the last successful sync, bounded by the installed leaf grant (an upper\n * bound only — a multi-parent ancestor can end the chain earlier, which only\n * the node can see).\n */\nexport function effectiveAuthority(input: {\n window: AuthorityWindow | null;\n grant: GrantRecord | null;\n revoked: string | null;\n retentionRevoked?: string | null;\n policy: LocalReadPolicy;\n now: number;\n}): EffectiveAuthority {\n const notBefore = maxIso(input.window?.notBefore ?? null, input.grant?.notBefore ?? null);\n const expiresAt = minIso(input.window?.expiresAt ?? null, input.grant?.expiresAt ?? null);\n const retainUntil = input.policy === \"retainAfterExpiry\" ? input.window?.retainUntil ?? null : null;\n let state: AuthorityState = \"valid\";\n if (input.revoked !== null) state = \"revoked\";\n else if (notBefore !== null && input.now < Date.parse(notBefore)) state = \"not-yet-valid\";\n else if (expiresAt !== null && input.now >= Date.parse(expiresAt)) state = \"expired\";\n return { state, notBefore, expiresAt, retainUntil, retentionRevoked: (input.retentionRevoked ?? null) !== null };\n}\n\n/**\n * Throws unless local reads are allowed now. Returns the reported authority\n * state: `valid`, or `expired` while a node-attested retention holds.\n */\nexport function assertReadable(authority: EffectiveAuthority, now: number): AuthorityState {\n switch (authority.state) {\n case \"valid\":\n return \"valid\";\n case \"revoked\":\n throw new ReplicaError(ReplicaErrorCode.GRANT_REVOKED, \"The replica's grant was revoked; local reads are blocked.\");\n case \"not-yet-valid\":\n throw new ReplicaError(\n ReplicaErrorCode.GRANT_NOT_YET_VALID,\n `The replica's grant is not valid before ${authority.notBefore}.`,\n );\n case \"expired\":\n if (authority.retentionRevoked) {\n throw new ReplicaError(\n ReplicaErrorCode.GRANT_REVOKED,\n `The replica's grant expired at ${authority.expiresAt} and its retention grant was revoked; local reads are blocked.`,\n );\n }\n if (authority.retainUntil !== null && now < Date.parse(authority.retainUntil)) return \"expired\";\n throw new ReplicaError(\n ReplicaErrorCode.GRANT_EXPIRED,\n authority.retainUntil === null\n ? `The replica's grant expired at ${authority.expiresAt}; local reads are blocked.`\n : `The replica's retention ended at ${authority.retainUntil}; local reads are blocked.`,\n );\n }\n}\n","/**\n * `did:key` verification method fragment → the principal DID.\n */\nexport function principalOf(did: string): string {\n return did.split(\"#\", 1)[0]!;\n}\n\nconst METHOD = /^[a-z0-9]+$/u;\n// DID method-specific-id: idchars ([A-Za-z0-9._-]) or %XX escapes, with `:`\n// separators allowed; the final segment must be non-empty.\nconst IDCHAR = /^[A-Za-z0-9._-]$/u;\n\n/**\n * Whether `value` is a well-formed DID the replica accepts as a `principal`\n * partition label: `did:` + a lowercase method name + a non-empty\n * method-specific-id whose segments (separated by `:`) consist of idchars or\n * valid `%XX` percent-escapes, and whose last segment is not empty. Shape\n * only — `principal` is an app-asserted label, never a credential.\n */\nexport function isPrincipalDid(value: string): boolean {\n if (typeof value !== \"string\" || !value.startsWith(\"did:\")) return false;\n const rest = value.slice(4);\n const methodEnd = rest.indexOf(\":\");\n if (methodEnd <= 0) return false;\n if (!METHOD.test(rest.slice(0, methodEnd))) return false;\n const msid = rest.slice(methodEnd + 1);\n if (msid.length === 0) return false;\n const segments = msid.split(\":\");\n const last = segments[segments.length - 1]!;\n if (last.length === 0) return false;\n for (const segment of segments) {\n for (let i = 0; i < segment.length; i += 1) {\n const c = segment[i]!;\n if (c === \"%\") {\n // A percent-escape needs exactly two hex digits.\n if (i + 2 >= segment.length || !/^[0-9a-fA-F]{2}$/u.test(segment.slice(i + 1, i + 3))) return false;\n i += 2;\n } else if (!IDCHAR.test(c)) {\n return false;\n }\n }\n }\n return true;\n}\n","/**\n * The main-thread browser API (TC-19): a thin RPC client over the replica\n * worker. IndexedDB, the network and the device key live in the worker; the\n * client's local reads never touch the network, and its sync calls are the\n * only path that does.\n *\n * The worker ships as `dist/replica.worker.js`, a self-contained ESM bundle\n * spawned via `new Worker(new URL(\"./replica.worker.js\", import.meta.url),\n * { type: \"module\" })` — webpack 5 and Vite detect that form. A `worker`\n * option accepts a `Worker` or `URL` escape hatch. In Vite dev mode, list\n * `@tinycloud/replica` in `optimizeDeps.exclude` so the worker URL resolves\n * inside the package (vitejs/vite#20859).\n *\n * Trust boundary: the browser origin, exactly as the OS user is the CLI's.\n * Run one app per origin — any same-origin script can open the IndexedDB\n * databases directly. `principal` (required) is the signed-in user's\n * identity DID; it partitions replicas per user on that origin, it is not\n * an authorization check — the node authorizes at sync time, and local\n * reads are gated by the stored grant's node-attested window, like the CLI.\n */\nimport { ReplicaError, ReplicaErrorCode } from \"../errors.js\";\nimport { assertFolderPrefix } from \"../scope.js\";\nimport { isPrincipalDid } from \"../did.js\";\nimport type {\n GetResult,\n ListResult,\n OpenResult,\n ReplicaReply,\n StatusResult,\n SyncResult,\n} from \"./protocol.js\";\n\nexport { ReplicaError, ReplicaErrorCode };\n\n/**\n * `reset()` result. With `purge: true` the replica's contents are erased in\n * a single commit — the database file itself is never deleted: a small\n * empty database named by a hash remains (holding no user data) and the\n * next `openReplica` reinitializes it in place. Once the commit lands the\n * result is final `{ purged: true }` — nothing after it can change the\n * outcome.\n */\nexport type ResetResult = { reset: true; purged: boolean };\n\nexport type BrowserReplicaOpenOptions = {\n /** The TinyCloud host to sync from; pinned for the replica's life. */\n host: string;\n /** The space id (native form) the replica covers. */\n space: string;\n /** The KV prefix: `/` for the whole space, or a folder such as `notes/`. */\n prefix: string;\n /** A display name; defaults to the prefix. */\n name?: string;\n /**\n * The signed-in user's identity DID (for example `did:pkh:eip155:1:0x…`),\n * as the app knows it. Replicas for different principals get different\n * databases on the same origin; signing in again and installing the new\n * session's grant continues the same replica from its cursor. This is an\n * app-asserted partition label, not an authorization check — the trust\n * boundary is the browser origin, and the node authorizes at sync time.\n */\n principal: string;\n /** Replicating the `secrets` space or a `vault/` prefix needs this opt-in. */\n allowSecrets?: boolean;\n /**\n * Spawn this worker instead of the bundled one (custom bundler layouts).\n * A factory (`() => Worker`) is also accepted: it runs after `principal`\n * validation, so a bad open never spawns a worker — useful for tests.\n */\n worker?: Worker | URL | string | (() => Worker);\n};\n\nexport type BrowserReplicaGetResult = GetResult;\nexport type BrowserReplicaListResult = ListResult;\nexport type BrowserReplicaStatus = StatusResult;\nexport type BrowserReplicaSyncResult = SyncResult;\nexport type BrowserReplicaOpenResult = OpenResult;\n\n/** Subscriptions for worker events (other tabs' commits, resets). */\nexport type BrowserReplicaEvents = {\n /** Another tab's worker committed a page (`serial` is monotonic). */\n onCommitted?: (serial: number) => void;\n /** The replica's authority state changed. */\n onAuthority?: (state: string) => void;\n /** The replica was reset in this tab or observed elsewhere. */\n onReset?: (reason: string) => void;\n};\n\ntype Pending = {\n resolve: (result: unknown) => void;\n reject: (error: Error) => void;\n};\n\nfunction workerFrom(source: Worker | URL | string | (() => Worker) | undefined): Worker {\n if (source !== undefined) {\n if (typeof Worker !== \"undefined\" && source instanceof Worker) return source;\n if (source instanceof URL || typeof source === \"string\") return new Worker(source, { type: \"module\" });\n if (typeof source === \"function\") return source();\n }\n return new Worker(new URL(\"./replica.worker.js\", import.meta.url), { type: \"module\" });\n}\n\n/**\n * Open (or create) a replica in a dedicated worker. The first call also asks\n * the browser for durable storage (`navigator.storage.persist()`), so an\n * eviction under pressure is less likely; `status()` reports the outcome.\n * In environments without IndexedDB the returned promise rejects with\n * `RUNTIME_UNSUPPORTED` — there is no in-memory fallback, reads would lose\n * their durability guarantee.\n *\n * `principal` partitions the replica per signed-in user and is required: the\n * same host/space/prefix under two principals yields two databases. A device\n * grant chained to a web session (about one hour under an OpenKey session)\n * gives about that session's length of offline reads; after sign-in rotates\n * the session key, install the new grant and the replica continues from its\n * cursor.\n */\nexport async function openReplica(options: BrowserReplicaOpenOptions, events: BrowserReplicaEvents = {}): Promise<BrowserReplica> {\n assertFolderPrefix(options.prefix);\n // Validate before workerFrom(): a bad principal or prefix never spawns a worker.\n if (typeof options.principal !== \"string\" || !isPrincipalDid(options.principal)) {\n throw new ReplicaError(\n ReplicaErrorCode.INVALID_ARGUMENT,\n `openReplica() requires \\`principal\\`, the signed-in user's identity DID (got ${JSON.stringify(options.principal)}).`,\n );\n }\n const worker = workerFrom(options.worker);\n const replica = new BrowserReplica(worker, events);\n try {\n await replica.ready;\n const persisted =\n typeof navigator !== \"undefined\" && typeof navigator.storage?.persist === \"function\"\n ? await navigator.storage.persist().catch(() => undefined)\n : undefined;\n // `worker` selects the process, it is not a wire field: do not forward it.\n return await replica.open({ ...options, worker: undefined, persisted });\n } catch (error) {\n // Any startup or open() failure abandons this replica: reject in-flight\n // calls, terminate the worker, and never leak a live Worker the caller\n // cannot reach.\n await replica.close().catch(() => undefined);\n worker.terminate();\n throw error;\n }\n}\n\nexport class BrowserReplica {\n /** The open() result: replicaId, deviceDid and the status at open time. */\n opened: OpenResult | null = null;\n\n readonly #worker: Worker;\n readonly #events: BrowserReplicaEvents;\n /** Settles when the worker reports it is listening (or fails to start). */\n readonly ready: Promise<void>;\n #nextId = 1;\n #pending = new Map<number, Pending>();\n #closed = false;\n\n /** For tests and advanced use: drive an already-spawned worker. */\n constructor(worker: Worker, events: BrowserReplicaEvents = {}) {\n this.#worker = worker;\n this.#events = events;\n const ready = Promise.withResolvers<void>();\n this.ready = ready.promise;\n // A close() before the worker reports ready rejects `ready`; not every\n // consumer awaits it, so mark that rejection handled up front.\n ready.promise.catch(() => undefined);\n this.#readyResolve = ready.resolve;\n this.#readyReject = ready.reject;\n worker.onmessage = (message: MessageEvent) => this.#onMessage(message.data);\n worker.onerror = (event) => {\n const error = new Error(event.message ?? \"The replica worker failed.\");\n this.#readyReject(error);\n this.#failAll(error);\n };\n }\n\n #readyResolve: () => void;\n #readyReject: (error: Error) => void;\n\n #onMessage(data: unknown): void {\n if (typeof data === \"object\" && data !== null && \"event\" in data) {\n const event = (data as { event: { event?: unknown } }).event;\n switch (event.event) {\n case \"ready\":\n this.#readyResolve();\n return;\n case \"committed\":\n if (typeof (event as { serial?: unknown }).serial === \"number\") {\n this.#events.onCommitted?.((event as { serial: number }).serial);\n }\n return;\n case \"authority\":\n if (typeof (event as { state?: unknown }).state === \"string\") {\n this.#events.onAuthority?.((event as { state: string }).state);\n }\n return;\n case \"reset\":\n if (typeof (event as { reason?: unknown }).reason === \"string\") {\n this.#events.onReset?.((event as { reason: string }).reason);\n }\n return;\n default:\n return;\n }\n }\n const reply = data as ReplicaReply;\n if (typeof reply.id !== \"number\") return;\n const pending = this.#pending.get(reply.id);\n if (pending === undefined) return;\n this.#pending.delete(reply.id);\n if (reply.ok) {\n pending.resolve(reply.result);\n } else {\n pending.reject(new ReplicaError(reply.err.code as ReplicaErrorCode, reply.err.message, reply.err.detail));\n }\n }\n\n #failAll(error: Error): void {\n for (const pending of this.#pending.values()) pending.reject(error);\n this.#pending.clear();\n }\n\n async #call<T>(request: Record<string, unknown>): Promise<T> {\n if (this.#closed) throw new ReplicaError(ReplicaErrorCode.CLOSED, \"The replica is closed.\");\n const id = this.#nextId++;\n const { promise, resolve, reject } = Promise.withResolvers<unknown>();\n this.#pending.set(id, { resolve, reject });\n this.#worker.postMessage({ id, ...request });\n return (await promise) as T;\n }\n\n /** Open or create the replica; `openReplica` calls this for you. */\n async open(options: BrowserReplicaOpenOptions & { persisted?: boolean }): Promise<BrowserReplica> {\n this.opened = await this.#call<OpenResult>({\n op: \"open\",\n host: options.host,\n space: options.space,\n prefix: options.prefix,\n principal: options.principal,\n ...(options.name === undefined ? {} : { name: options.name }),\n ...(options.allowSecrets === undefined ? {} : { allowSecrets: options.allowSecrets }),\n ...(options.persisted === undefined ? {} : { persisted: options.persisted }),\n });\n return this;\n }\n\n /** The DID the app delegates to (`tcw.delegateTo(deviceDid, […sync…])`). */\n get deviceDid(): string {\n if (this.opened === null) throw new Error(\"open() has not completed.\");\n return this.opened.deviceDid;\n }\n\n /** Install a signed grant: the compact UCAN (`delegation.delegationHeader.Authorization`). */\n installGrant(delegation: string): Promise<{ cid: string; audience: string; expiresAt: number | null }> {\n return this.#call({ op: \"installGrant\", delegation });\n }\n\n /** Pull the feed and commit each page atomically. `busy` when another tab syncs. */\n sync(options: { limit?: number } = {}): Promise<SyncResult> {\n return this.#call({ op: \"sync\", ...(options.limit === undefined ? {} : { limit: options.limit }) });\n }\n\n /** Local read: never touches the network. */\n get(key: string): Promise<GetResult> {\n return this.#call({ op: \"get\", key });\n }\n\n /** Local read: never touches the network. */\n list(options: { prefix?: string; after?: string; limit?: number } = {}): Promise<ListResult> {\n return this.#call({ op: \"list\", ...options });\n }\n\n /** Replica status plus browser persistence facts. */\n status(): Promise<StatusResult> {\n return this.#call({ op: \"status\" });\n }\n\n /** Opt in to post-expiry reads under a `tinycloud.kv/retain` grant. */\n setRetentionGrant(cid: string | null): Promise<{ retentionGrantCid: string | null }> {\n return this.#call({ op: \"setRetention\", grantCid: cid });\n }\n\n /**\n * Clear entries and cursor (keeps config and grant); `purge` erases the\n * replica permanently in place — the database file is never deleted, so\n * the result is final the moment it resolves (see {@link ResetResult}).\n */\n reset(options: { purge?: boolean } = {}): Promise<ResetResult> {\n return this.#call({ op: \"reset\", purge: options.purge === true });\n }\n\n /** Close the worker-side handles and terminate the worker. */\n async close(): Promise<void> {\n if (this.#closed) return;\n this.#closed = true;\n this.#readyReject(new ReplicaError(ReplicaErrorCode.CLOSED, \"The replica is closed.\"));\n // Shutdown starts now: every request already in flight rejects with\n // REPLICA_CLOSED before the close handshake is even posted, so a reply\n // arriving mid-close finds no entry and is ignored — never resolves with\n // data. The handshake itself is registered only after that sweep.\n this.#failAll(new ReplicaError(ReplicaErrorCode.CLOSED, \"The replica is closed.\"));\n const id = this.#nextId++;\n const { promise, resolve } = Promise.withResolvers<unknown>();\n this.#pending.set(id, { resolve, reject: resolve });\n this.#worker.postMessage({ id, op: \"close\" });\n const timeout = setTimeout(() => {\n this.#pending.delete(id);\n resolve(undefined);\n }, 2_000);\n await promise.catch(() => undefined).finally(() => clearTimeout(timeout));\n this.#worker.terminate();\n this.#failAll(new ReplicaError(ReplicaErrorCode.CLOSED, \"The replica is closed.\"));\n }\n}\n"],"mappings":";AAIO,IAAM,mBAAmB;AAAA;AAAA,EAE9B,MAAM;AAAA;AAAA,EAEN,WAAW;AAAA;AAAA,EAEX,iBAAiB;AAAA;AAAA,EAEjB,qBAAqB;AAAA;AAAA,EAErB,kBAAkB;AAAA;AAAA,EAElB,eAAe;AAAA;AAAA,EAEf,cAAc;AAAA;AAAA,EAEd,aAAa;AAAA;AAAA,EAEb,yBAAyB;AAAA;AAAA,EAEzB,eAAe;AAAA;AAAA,EAEf,eAAe;AAAA;AAAA,EAEf,0BAA0B;AAAA;AAAA,EAE1B,yBAAyB;AAAA;AAAA,EAEzB,oBAAoB;AAAA,EACpB,qBAAqB;AAAA,EACrB,eAAe;AAAA;AAAA,EAEf,8BAA8B;AAAA,EAC9B,eAAe;AAAA;AAAA,EAEf,oBAAoB;AAAA;AAAA,EAEpB,yBAAyB;AAAA;AAAA,EAEzB,eAAe;AAAA;AAAA,EAEf,YAAY;AAAA;AAAA,EAEZ,gBAAgB;AAAA;AAAA,EAEhB,gBAAgB;AAAA;AAAA,EAEhB,gBAAgB;AAAA;AAAA,EAEhB,iBAAiB;AAAA;AAAA,EAEjB,kBAAkB;AAAA;AAAA,EAElB,iBAAiB;AAAA;AAAA,EAEjB,QAAQ;AAAA;AAAA,EAER,OAAO;AAAA;AAAA,EAEP,gBAAgB;AAAA;AAAA,EAEhB,iBAAiB;AACnB;AAQA,IAAM,gBAAgB,uBAAO,IAAI,yBAAyB;AAEnD,IAAM,eAAN,cAA2B,MAAM;AAAA,EACtC,CAAU,aAAa,IAAI;AAAA,EAClB;AAAA,EACA;AAAA,EAET,YAAY,MAAwB,SAAiB,QAAkC,SAA+B;AACpH,UAAM,SAAS,OAAO;AACtB,SAAK,OAAO;AACZ,SAAK,OAAO;AACZ,QAAI,WAAW,OAAW,MAAK,SAAS;AAAA,EAC1C;AACF;;;ACjFO,SAAS,mBAAmB,QAA2C;AAC5E,QAAM,cAAc,OAAO,WAAW,YACpC,OAAO,SAAS,KAChB,CAAC,OAAO,WAAW,GAAG,KACtB,CAAC,OAAO,SAAS,IAAI,KACrB,OAAO,SAAS,GAAG;AACrB,MAAI,WAAW,OAAO,CAAC,aAAa;AAClC,UAAM,aAAa,OAAO,WAAW,WACjC,OAAO,QAAQ,QAAQ,EAAE,EAAE,QAAQ,QAAQ,EAAE,IAC7C;AACJ,UAAM,aAAa,WAAW,SAAS,KAAK,CAAC,WAAW,SAAS,IAAI,IACjE,QAAQ,KAAK,UAAU,GAAG,UAAU,GAAG,CAAC,MACxC;AACJ,UAAM,IAAI,UAAU,qGAAqG,UAAU,EAAE;EACvI;AACF;;;AChBO,SAASA,oBAAmB,QAA2C;AAC5E,MAAI;AACF,uBAAuB,MAAM;AAAA,EAC/B,QAAQ;AACN,UAAM,aAAa,OAAO,WAAW,WACjC,OAAO,QAAQ,QAAQ,EAAE,EAAE,QAAQ,QAAQ,EAAE,IAC7C;AACJ,UAAM,aAAa,WAAW,KAAK,gCACjC,WAAW,SAAS,KAAK,CAAC,WAAW,SAAS,IAAI,IAC9C,OAAO,KAAK,UAAU,GAAG,UAAU,GAAG,CAAC,KACvC;AACN,UAAM,IAAI;AAAA,MACR,iBAAiB;AAAA,MACjB,sBAAsB,KAAK,UAAU,MAAM,CAAC,+EAA+E,UAAU;AAAA,IACvI;AAAA,EACF;AACF;;;ACdA,IAAM,SAAS;AAGf,IAAM,SAAS;AASR,SAAS,eAAe,OAAwB;AACrD,MAAI,OAAO,UAAU,YAAY,CAAC,MAAM,WAAW,MAAM,EAAG,QAAO;AACnE,QAAM,OAAO,MAAM,MAAM,CAAC;AAC1B,QAAM,YAAY,KAAK,QAAQ,GAAG;AAClC,MAAI,aAAa,EAAG,QAAO;AAC3B,MAAI,CAAC,OAAO,KAAK,KAAK,MAAM,GAAG,SAAS,CAAC,EAAG,QAAO;AACnD,QAAM,OAAO,KAAK,MAAM,YAAY,CAAC;AACrC,MAAI,KAAK,WAAW,EAAG,QAAO;AAC9B,QAAM,WAAW,KAAK,MAAM,GAAG;AAC/B,QAAM,OAAO,SAAS,SAAS,SAAS,CAAC;AACzC,MAAI,KAAK,WAAW,EAAG,QAAO;AAC9B,aAAW,WAAW,UAAU;AAC9B,aAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK,GAAG;AAC1C,YAAM,IAAI,QAAQ,CAAC;AACnB,UAAI,MAAM,KAAK;AAEb,YAAI,IAAI,KAAK,QAAQ,UAAU,CAAC,oBAAoB,KAAK,QAAQ,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,EAAG,QAAO;AAC9F,aAAK;AAAA,MACP,WAAW,CAAC,OAAO,KAAK,CAAC,GAAG;AAC1B,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;;;ACkDA,SAAS,WAAW,QAAoE;AACtF,MAAI,WAAW,QAAW;AACxB,QAAI,OAAO,WAAW,eAAe,kBAAkB,OAAQ,QAAO;AACtE,QAAI,kBAAkB,OAAO,OAAO,WAAW,SAAU,QAAO,IAAI,OAAO,QAAQ,EAAE,MAAM,SAAS,CAAC;AACrG,QAAI,OAAO,WAAW,WAAY,QAAO,OAAO;AAAA,EAClD;AACA,SAAO,IAAI,OAAO,IAAI,IAAI,uBAAuB,YAAY,GAAG,GAAG,EAAE,MAAM,SAAS,CAAC;AACvF;AAiBA,eAAsB,YAAY,SAAoC,SAA+B,CAAC,GAA4B;AAChI,EAAAC,oBAAmB,QAAQ,MAAM;AAEjC,MAAI,OAAO,QAAQ,cAAc,YAAY,CAAC,eAAe,QAAQ,SAAS,GAAG;AAC/E,UAAM,IAAI;AAAA,MACR,iBAAiB;AAAA,MACjB,gFAAgF,KAAK,UAAU,QAAQ,SAAS,CAAC;AAAA,IACnH;AAAA,EACF;AACA,QAAM,SAAS,WAAW,QAAQ,MAAM;AACxC,QAAM,UAAU,IAAI,eAAe,QAAQ,MAAM;AACjD,MAAI;AACF,UAAM,QAAQ;AACd,UAAM,YACJ,OAAO,cAAc,eAAe,OAAO,UAAU,SAAS,YAAY,aACtE,MAAM,UAAU,QAAQ,QAAQ,EAAE,MAAM,MAAM,MAAS,IACvD;AAEN,WAAO,MAAM,QAAQ,KAAK,EAAE,GAAG,SAAS,QAAQ,QAAW,UAAU,CAAC;AAAA,EACxE,SAAS,OAAO;AAId,UAAM,QAAQ,MAAM,EAAE,MAAM,MAAM,MAAS;AAC3C,WAAO,UAAU;AACjB,UAAM;AAAA,EACR;AACF;AAEO,IAAM,iBAAN,MAAqB;AAAA;AAAA,EAE1B,SAA4B;AAAA,EAEnB;AAAA,EACA;AAAA;AAAA,EAEA;AAAA,EACT,UAAU;AAAA,EACV,WAAW,oBAAI,IAAqB;AAAA,EACpC,UAAU;AAAA;AAAA,EAGV,YAAY,QAAgB,SAA+B,CAAC,GAAG;AAC7D,SAAK,UAAU;AACf,SAAK,UAAU;AACf,UAAM,QAAQ,QAAQ,cAAoB;AAC1C,SAAK,QAAQ,MAAM;AAGnB,UAAM,QAAQ,MAAM,MAAM,MAAS;AACnC,SAAK,gBAAgB,MAAM;AAC3B,SAAK,eAAe,MAAM;AAC1B,WAAO,YAAY,CAAC,YAA0B,KAAK,WAAW,QAAQ,IAAI;AAC1E,WAAO,UAAU,CAAC,UAAU;AAC1B,YAAM,QAAQ,IAAI,MAAM,MAAM,WAAW,4BAA4B;AACrE,WAAK,aAAa,KAAK;AACvB,WAAK,SAAS,KAAK;AAAA,IACrB;AAAA,EACF;AAAA,EAEA;AAAA,EACA;AAAA,EAEA,WAAW,MAAqB;AAC9B,QAAI,OAAO,SAAS,YAAY,SAAS,QAAQ,WAAW,MAAM;AAChE,YAAM,QAAS,KAAwC;AACvD,cAAQ,MAAM,OAAO;AAAA,QACnB,KAAK;AACH,eAAK,cAAc;AACnB;AAAA,QACF,KAAK;AACH,cAAI,OAAQ,MAA+B,WAAW,UAAU;AAC9D,iBAAK,QAAQ,cAAe,MAA6B,MAAM;AAAA,UACjE;AACA;AAAA,QACF,KAAK;AACH,cAAI,OAAQ,MAA8B,UAAU,UAAU;AAC5D,iBAAK,QAAQ,cAAe,MAA4B,KAAK;AAAA,UAC/D;AACA;AAAA,QACF,KAAK;AACH,cAAI,OAAQ,MAA+B,WAAW,UAAU;AAC9D,iBAAK,QAAQ,UAAW,MAA6B,MAAM;AAAA,UAC7D;AACA;AAAA,QACF;AACE;AAAA,MACJ;AAAA,IACF;AACA,UAAM,QAAQ;AACd,QAAI,OAAO,MAAM,OAAO,SAAU;AAClC,UAAM,UAAU,KAAK,SAAS,IAAI,MAAM,EAAE;AAC1C,QAAI,YAAY,OAAW;AAC3B,SAAK,SAAS,OAAO,MAAM,EAAE;AAC7B,QAAI,MAAM,IAAI;AACZ,cAAQ,QAAQ,MAAM,MAAM;AAAA,IAC9B,OAAO;AACL,cAAQ,OAAO,IAAI,aAAa,MAAM,IAAI,MAA0B,MAAM,IAAI,SAAS,MAAM,IAAI,MAAM,CAAC;AAAA,IAC1G;AAAA,EACF;AAAA,EAEA,SAAS,OAAoB;AAC3B,eAAW,WAAW,KAAK,SAAS,OAAO,EAAG,SAAQ,OAAO,KAAK;AAClE,SAAK,SAAS,MAAM;AAAA,EACtB;AAAA,EAEA,MAAM,MAAS,SAA8C;AAC3D,QAAI,KAAK,QAAS,OAAM,IAAI,aAAa,iBAAiB,QAAQ,wBAAwB;AAC1F,UAAM,KAAK,KAAK;AAChB,UAAM,EAAE,SAAS,SAAS,OAAO,IAAI,QAAQ,cAAuB;AACpE,SAAK,SAAS,IAAI,IAAI,EAAE,SAAS,OAAO,CAAC;AACzC,SAAK,QAAQ,YAAY,EAAE,IAAI,GAAG,QAAQ,CAAC;AAC3C,WAAQ,MAAM;AAAA,EAChB;AAAA;AAAA,EAGA,MAAM,KAAK,SAAuF;AAChG,SAAK,SAAS,MAAM,KAAK,MAAkB;AAAA,MACzC,IAAI;AAAA,MACJ,MAAM,QAAQ;AAAA,MACd,OAAO,QAAQ;AAAA,MACf,QAAQ,QAAQ;AAAA,MAChB,WAAW,QAAQ;AAAA,MACnB,GAAI,QAAQ,SAAS,SAAY,CAAC,IAAI,EAAE,MAAM,QAAQ,KAAK;AAAA,MAC3D,GAAI,QAAQ,iBAAiB,SAAY,CAAC,IAAI,EAAE,cAAc,QAAQ,aAAa;AAAA,MACnF,GAAI,QAAQ,cAAc,SAAY,CAAC,IAAI,EAAE,WAAW,QAAQ,UAAU;AAAA,IAC5E,CAAC;AACD,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,IAAI,YAAoB;AACtB,QAAI,KAAK,WAAW,KAAM,OAAM,IAAI,MAAM,2BAA2B;AACrE,WAAO,KAAK,OAAO;AAAA,EACrB;AAAA;AAAA,EAGA,aAAa,YAA0F;AACrG,WAAO,KAAK,MAAM,EAAE,IAAI,gBAAgB,WAAW,CAAC;AAAA,EACtD;AAAA;AAAA,EAGA,KAAK,UAA8B,CAAC,GAAwB;AAC1D,WAAO,KAAK,MAAM,EAAE,IAAI,QAAQ,GAAI,QAAQ,UAAU,SAAY,CAAC,IAAI,EAAE,OAAO,QAAQ,MAAM,EAAG,CAAC;AAAA,EACpG;AAAA;AAAA,EAGA,IAAI,KAAiC;AACnC,WAAO,KAAK,MAAM,EAAE,IAAI,OAAO,IAAI,CAAC;AAAA,EACtC;AAAA;AAAA,EAGA,KAAK,UAA+D,CAAC,GAAwB;AAC3F,WAAO,KAAK,MAAM,EAAE,IAAI,QAAQ,GAAG,QAAQ,CAAC;AAAA,EAC9C;AAAA;AAAA,EAGA,SAAgC;AAC9B,WAAO,KAAK,MAAM,EAAE,IAAI,SAAS,CAAC;AAAA,EACpC;AAAA;AAAA,EAGA,kBAAkB,KAAmE;AACnF,WAAO,KAAK,MAAM,EAAE,IAAI,gBAAgB,UAAU,IAAI,CAAC;AAAA,EACzD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,UAA+B,CAAC,GAAyB;AAC7D,WAAO,KAAK,MAAM,EAAE,IAAI,SAAS,OAAO,QAAQ,UAAU,KAAK,CAAC;AAAA,EAClE;AAAA;AAAA,EAGA,MAAM,QAAuB;AAC3B,QAAI,KAAK,QAAS;AAClB,SAAK,UAAU;AACf,SAAK,aAAa,IAAI,aAAa,iBAAiB,QAAQ,wBAAwB,CAAC;AAKrF,SAAK,SAAS,IAAI,aAAa,iBAAiB,QAAQ,wBAAwB,CAAC;AACjF,UAAM,KAAK,KAAK;AAChB,UAAM,EAAE,SAAS,QAAQ,IAAI,QAAQ,cAAuB;AAC5D,SAAK,SAAS,IAAI,IAAI,EAAE,SAAS,QAAQ,QAAQ,CAAC;AAClD,SAAK,QAAQ,YAAY,EAAE,IAAI,IAAI,QAAQ,CAAC;AAC5C,UAAM,UAAU,WAAW,MAAM;AAC/B,WAAK,SAAS,OAAO,EAAE;AACvB,cAAQ,MAAS;AAAA,IACnB,GAAG,GAAK;AACR,UAAM,QAAQ,MAAM,MAAM,MAAS,EAAE,QAAQ,MAAM,aAAa,OAAO,CAAC;AACxE,SAAK,QAAQ,UAAU;AACvB,SAAK,SAAS,IAAI,aAAa,iBAAiB,QAAQ,wBAAwB,CAAC;AAAA,EACnF;AACF;","names":["assertFolderPrefix","assertFolderPrefix"]}
|
package/dist/index.cjs
CHANGED
|
@@ -101,7 +101,13 @@ var ReplicaErrorCode = {
|
|
|
101
101
|
/** A stored blob no longer hashes to its name. */
|
|
102
102
|
INTEGRITY_ERROR: "INTEGRITY_ERROR",
|
|
103
103
|
/** The replica handle was closed; in-flight calls settle with this code. */
|
|
104
|
-
CLOSED: "REPLICA_CLOSED"
|
|
104
|
+
CLOSED: "REPLICA_CLOSED",
|
|
105
|
+
/** The lifecycle gate refused the operation (shared contract `REPLICA_GATED`); `detail.gate` says why. */
|
|
106
|
+
GATED: "REPLICA_GATED",
|
|
107
|
+
/** Quota admission refused a page (`maxBytes` or the free-space floor); nothing committed. */
|
|
108
|
+
STORAGE_BUDGET: "STORAGE_BUDGET",
|
|
109
|
+
/** The consent deadline passed: the grant is not served until the user consents again. */
|
|
110
|
+
CONSENT_EXPIRED: "CONSENT_EXPIRED"
|
|
105
111
|
};
|
|
106
112
|
var REPLICA_ERROR = /* @__PURE__ */ Symbol.for("tinycloud.replica.error");
|
|
107
113
|
var ReplicaError = class extends Error {
|
|
@@ -354,6 +360,11 @@ var Replica = class {
|
|
|
354
360
|
* active grant serves: a revoked pending grant is discarded, any other
|
|
355
361
|
* refusal is recorded on it. A revocation of the grant serving the replica,
|
|
356
362
|
* learned anywhere in the sync, purges the replica before this returns.
|
|
363
|
+
*
|
|
364
|
+
* `signal` cancels between commits: an aborted sync commits nothing past
|
|
365
|
+
* its last complete page. `finish` runs once every feed page, repair batch
|
|
366
|
+
* and the GC have committed, still under the sync lease — a sync that fails
|
|
367
|
+
* or aborts earlier never reaches it.
|
|
357
368
|
*/
|
|
358
369
|
async sync(options = {}) {
|
|
359
370
|
const transportFor = this.#transportFor;
|
|
@@ -377,7 +388,12 @@ var Replica = class {
|
|
|
377
388
|
signal: options.signal
|
|
378
389
|
};
|
|
379
390
|
try {
|
|
380
|
-
|
|
391
|
+
const report = await this.#syncUnder(attempt, options.limit ?? DEFAULT_PAGE_LIMIT);
|
|
392
|
+
if (options.finish !== void 0) {
|
|
393
|
+
options.signal?.throwIfAborted();
|
|
394
|
+
await options.finish(lease);
|
|
395
|
+
}
|
|
396
|
+
return report;
|
|
381
397
|
} catch (error) {
|
|
382
398
|
const canFallBack = pending && attempt.promote !== null && index < candidates.length - 1;
|
|
383
399
|
if (canFallBack && isReplicaError(error, ReplicaErrorCode.GRANT_REVOKED)) {
|
|
@@ -474,6 +490,7 @@ var Replica = class {
|
|
|
474
490
|
const verified = await this.#verify(page, attempt);
|
|
475
491
|
coverage = page.more ? coverage === "complete" ? "complete" : "bootstrapping" : "complete";
|
|
476
492
|
const at = new Date(this.#now()).toISOString();
|
|
493
|
+
attempt.signal?.throwIfAborted();
|
|
477
494
|
await this.#store.applyPage(attempt.lease, {
|
|
478
495
|
changes: verified.changes,
|
|
479
496
|
blobs: verified.blobs,
|
|
@@ -604,6 +621,7 @@ var Replica = class {
|
|
|
604
621
|
await this.#store.renewLease(attempt.lease, this.#leaseTtlMs);
|
|
605
622
|
if (changes.length === 0) continue;
|
|
606
623
|
const state = await this.#state();
|
|
624
|
+
attempt.signal?.throwIfAborted();
|
|
607
625
|
await this.#store.applyPage(attempt.lease, {
|
|
608
626
|
changes,
|
|
609
627
|
blobs,
|