@tinycloud/replica 0.1.0-beta.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.
@@ -0,0 +1,264 @@
1
+ /**
2
+ * Replica contracts shared by every host (CLI SQLite store, browser worker).
3
+ * The transport and store interfaces are the frozen TC-18/TC-19 contract.
4
+ */
5
+ /** One row of `tinycloud.kv/sync`: a key's latest state. */
6
+ type Change = {
7
+ key: string;
8
+ deleted: false;
9
+ etag: string;
10
+ metadata: Record<string, string>;
11
+ } | {
12
+ key: string;
13
+ deleted: true;
14
+ };
15
+ /** The node-attested window in which the caller's authority holds (ISO-8601 or null). */
16
+ type AuthorityWindow = {
17
+ notBefore: string | null;
18
+ expiresAt: string | null;
19
+ retainUntil: string | null;
20
+ };
21
+ type SyncSource = {
22
+ nodeDid: string;
23
+ space: string;
24
+ prefix: string;
25
+ };
26
+ type SyncPage = {
27
+ changes: Change[];
28
+ more: boolean;
29
+ cursor: string;
30
+ source: SyncSource;
31
+ /** Present on every `kv/sync` response; persisted with the page. */
32
+ authority: AuthorityWindow;
33
+ };
34
+ type FetchedContent = {
35
+ bytes: Uint8Array;
36
+ etag: string;
37
+ } | {
38
+ missing: true;
39
+ };
40
+ interface ReplicaTransport {
41
+ /** One `tinycloud.kv/sync` page (`KVService.changes`). */
42
+ syncPage(a: {
43
+ prefix: string;
44
+ cursor?: string;
45
+ limit: number;
46
+ /** CID of a `tinycloud.kv/retain` grant to present with this sync. */
47
+ retentionGrant?: string;
48
+ signal?: AbortSignal;
49
+ }): Promise<SyncPage>;
50
+ /** Current content for `keys` (`batchGet` ≤100; single binary get for oversize values). */
51
+ fetchContent(keys: string[], a?: {
52
+ signal?: AbortSignal;
53
+ }): Promise<Map<string, FetchedContent>>;
54
+ }
55
+ type LocalReadPolicy = "whileGrantValid" | "retainAfterExpiry";
56
+ type ReplicaConfig = {
57
+ name: string;
58
+ replicaId: string;
59
+ /** The source host pinned at creation; never a discovered local node. */
60
+ host: string;
61
+ space: string;
62
+ prefix: string;
63
+ deviceDid: string;
64
+ allowSecrets: boolean;
65
+ localReadPolicy: LocalReadPolicy;
66
+ retentionGrantCid: string | null;
67
+ };
68
+ /** A signed grant as installed in the store. */
69
+ type GrantRecord = {
70
+ cid: string;
71
+ /** The signed UCAN bytes, exactly as issued. */
72
+ bytes: Uint8Array;
73
+ audience: string;
74
+ issuer: string;
75
+ /** Leaf window (seconds since epoch), an install check and upper bound only. */
76
+ notBefore: number | null;
77
+ expiresAt: number | null;
78
+ };
79
+ type Coverage = "empty" | "bootstrapping" | "complete";
80
+ type AuthorityState = "valid" | "expired" | "revoked" | "not-yet-valid";
81
+ type ReplicaState = {
82
+ config: ReplicaConfig;
83
+ grant: GrantRecord | null;
84
+ pendingGrant: GrantRecord | null;
85
+ nodeDid: string | null;
86
+ /** The node-attested window from the last successful sync. */
87
+ authority: AuthorityWindow | null;
88
+ revoked: string | null;
89
+ /** Set when the node reported the retention grant revoked: post-expiry reads raise GRANT_REVOKED. */
90
+ retentionRevoked: string | null;
91
+ /** Why the node refused the pending grant at its last attempt, if it did. */
92
+ pendingGrantError: ReplicaLastError | null;
93
+ cursor: string | null;
94
+ coverage: Coverage;
95
+ generation: number;
96
+ createdAt: string;
97
+ lastSyncAt: string | null;
98
+ lastCompleteAt: string | null;
99
+ lastError: ReplicaLastError | null;
100
+ lastReset: {
101
+ at: string;
102
+ reason: string;
103
+ } | null;
104
+ };
105
+ type ReplicaLastError = {
106
+ at: string;
107
+ code: string;
108
+ message: string;
109
+ };
110
+ type LeaseToken = {
111
+ token: number;
112
+ holder: string;
113
+ };
114
+ /** A verified row ready to commit. `hash` is the 64-hex blake3 of the content. */
115
+ type VerifiedChange = {
116
+ key: string;
117
+ deleted: false;
118
+ etag: string;
119
+ hash: string;
120
+ metadata: Record<string, string>;
121
+ content: boolean;
122
+ } | {
123
+ key: string;
124
+ deleted: true;
125
+ };
126
+ type VerifiedPage = {
127
+ changes: VerifiedChange[];
128
+ /** Verified bytes by hash; written as blobs before the rows commit. */
129
+ blobs: Map<string, Uint8Array>;
130
+ /** The cursor to persist; unchanged for repair-only pages. */
131
+ cursor: string | null;
132
+ source: SyncSource;
133
+ authority: AuthorityWindow | null;
134
+ coverage: Coverage;
135
+ /** Commit time (ISO-8601), recorded as the sync time. */
136
+ at: string;
137
+ /**
138
+ * The effective authority window this page commits under. The store checks
139
+ * it against its own clock inside the commit transaction; outside it
140
+ * nothing commits.
141
+ */
142
+ window: {
143
+ notBefore: string | null;
144
+ expiresAt: string | null;
145
+ };
146
+ /** True when this page closes a sync run (`more: false`). */
147
+ complete: boolean;
148
+ /**
149
+ * The pending grant the node just validated for this page: it becomes the
150
+ * active grant. A different grant installed as pending meanwhile stays pending.
151
+ */
152
+ promoteGrant: GrantRecord | null;
153
+ };
154
+ type LocalEntry = {
155
+ key: string;
156
+ deleted: false;
157
+ etag: string;
158
+ hash: string;
159
+ metadata: Record<string, string>;
160
+ content: boolean;
161
+ } | {
162
+ key: string;
163
+ deleted: true;
164
+ };
165
+ type ListOpts = {
166
+ prefix?: string;
167
+ after?: string;
168
+ limit?: number;
169
+ };
170
+ type ReplicaStatus = {
171
+ name: string;
172
+ replicaId: string;
173
+ source: {
174
+ host: string;
175
+ nodeDid: string | null;
176
+ space: string;
177
+ prefix: string;
178
+ };
179
+ device: {
180
+ did: string;
181
+ delegationCid: string | null;
182
+ pendingDelegationCid: string | null;
183
+ pendingDelegationError: ReplicaLastError | null;
184
+ };
185
+ authority: {
186
+ state: AuthorityState;
187
+ notBefore: string | null;
188
+ expiresAt: string | null;
189
+ retainUntil: string | null;
190
+ localReadPolicy: LocalReadPolicy;
191
+ retentionGrantCid: string | null;
192
+ revokedDetail: string | null;
193
+ retentionRevokedDetail: string | null;
194
+ };
195
+ consistency: "observed";
196
+ coverage: Coverage;
197
+ lastSyncAt: string | null;
198
+ lastCompleteAt: string | null;
199
+ counts: {
200
+ keys: number;
201
+ contentMissing: number;
202
+ tombstones: number;
203
+ };
204
+ bytes: number;
205
+ durability: string;
206
+ syncing: boolean;
207
+ /** A revocation purge has not removed every content file yet; the next open retries it. */
208
+ purgePending: boolean;
209
+ lastError: ReplicaLastError | null;
210
+ lastReset: {
211
+ at: string;
212
+ reason: string;
213
+ } | null;
214
+ };
215
+ interface ReplicaStore {
216
+ open(): Promise<ReplicaState | null>;
217
+ init(c: ReplicaConfig): Promise<void>;
218
+ /** Stored as pending until a successful sync under it. */
219
+ installGrant(g: GrantRecord): Promise<void>;
220
+ acquireSyncLease(ttlMs: number): Promise<LeaseToken | null>;
221
+ renewLease(t: LeaseToken, ttlMs: number): Promise<void>;
222
+ releaseLease(t: LeaseToken): Promise<void>;
223
+ /** Hashes whose blobs are referenced by a committed entry. */
224
+ hasContent(hashes: string[]): Promise<Set<string>>;
225
+ /** ONE transaction: blob refs + entries + tombstones + cursor + coverage; fenced by a live lease token. */
226
+ applyPage(t: LeaseToken, p: VerifiedPage): Promise<void>;
227
+ /**
228
+ * Clear entries, blobs and cursor; keep config and grants. The nodeDid pin
229
+ * is cleared too unless `keepSource` (an automatic 410 reset keeps it).
230
+ */
231
+ reset(t: LeaseToken, reason: string, options?: {
232
+ keepSource?: boolean;
233
+ }): Promise<void>;
234
+ /**
235
+ * Purge entries and blobs, keep a minimal blocked status. Invalidates every
236
+ * outstanding lease; later commits, GC and resets refuse.
237
+ */
238
+ markRevoked(detail: string): Promise<void>;
239
+ /**
240
+ * The node revoked the retention grant `cid`: drop it and its retainUntil, so
241
+ * post-expiry reads raise GRANT_REVOKED. A different current grant is kept.
242
+ */
243
+ markRetentionRevoked(cid: string, detail: string): Promise<void>;
244
+ /** The node revoked the pending grant: discard it (the active grant keeps serving). */
245
+ discardPendingGrant(cid: string, detail: string): Promise<void>;
246
+ /** The node refused the pending grant (not a revocation): keep it pending, with the reason. */
247
+ recordPendingGrantError(cid: string, e: ReplicaLastError): Promise<void>;
248
+ recordError(e: ReplicaLastError): Promise<void>;
249
+ get(key: string): Promise<LocalEntry | undefined>;
250
+ list(o: ListOpts): Promise<LocalEntry[]>;
251
+ /** Live entries whose content is missing (pending repairs). */
252
+ pendingRepairs(limit: number): Promise<Array<Extract<LocalEntry, {
253
+ deleted: false;
254
+ }>>>;
255
+ /** Content bytes of a committed blob, or undefined if absent. */
256
+ readContent(hash: string): Promise<Uint8Array | undefined>;
257
+ /** Fenced blob GC: unlink blobs no entry references, and stale temp files. */
258
+ collectGarbage(t: LeaseToken): Promise<number>;
259
+ /** `now` (ms since epoch) evaluates the authority window; defaults to the wall clock. */
260
+ status(now?: number): Promise<ReplicaStatus>;
261
+ close(): Promise<void>;
262
+ }
263
+
264
+ export type { AuthorityState as A, Coverage as C, FetchedContent as F, GrantRecord as G, ListOpts as L, ReplicaStore as R, SyncPage as S, VerifiedChange as V, ReplicaTransport as a, ReplicaStatus as b, AuthorityWindow as c, LocalReadPolicy as d, Change as e, LeaseToken as f, LocalEntry as g, ReplicaConfig as h, ReplicaLastError as i, ReplicaState as j, SyncSource as k, VerifiedPage as l };
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@tinycloud/replica",
3
+ "version": "0.1.0-beta.0",
4
+ "description": "Durable, read-only local replicas of a TinyCloud KV prefix over the tinycloud.kv/sync change feed.",
5
+ "author": "TinyCloud, Inc.",
6
+ "license": "EGPL",
7
+ "type": "module",
8
+ "main": "./dist/index.cjs",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js",
15
+ "require": "./dist/index.cjs"
16
+ },
17
+ "./sqlite": {
18
+ "types": "./dist/sqlite.d.ts",
19
+ "import": "./dist/sqlite.js",
20
+ "require": "./dist/sqlite.cjs"
21
+ },
22
+ "./package.json": "./package.json"
23
+ },
24
+ "sideEffects": false,
25
+ "engines": {
26
+ "node": ">=20"
27
+ },
28
+ "files": [
29
+ "dist"
30
+ ],
31
+ "scripts": {
32
+ "build": "tsup",
33
+ "clean": "rm -rf dist",
34
+ "test": "bun test",
35
+ "typecheck": "tsc --noEmit"
36
+ },
37
+ "dependencies": {
38
+ "@noble/curves": "^1.9.2",
39
+ "@noble/hashes": "^1.8.0"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^20",
43
+ "tsup": "^8.0.0",
44
+ "typescript": "^5.9.3"
45
+ },
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "https://github.com/TinyCloudLabs/js-sdk.git",
49
+ "directory": "packages/replica"
50
+ },
51
+ "publishConfig": {
52
+ "access": "public"
53
+ }
54
+ }