@drakkar.software/starfish-client 3.0.0-alpha.14 → 3.0.0-alpha.16
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 +16 -0
- package/dist/bindings/zustand.d.ts +27 -1
- package/dist/bindings/zustand.js +126 -8
- package/dist/bindings/zustand.js.map +2 -2
- package/dist/client.d.ts +27 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +109 -4
- package/dist/index.js.map +2 -2
- package/dist/sync.d.ts +18 -0
- package/dist/types.d.ts +43 -0
- package/package.json +2 -2
package/dist/client.d.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import type { PullResult, PushSuccess } from "@drakkar.software/starfish-protocol";
|
|
2
2
|
import { type AppendAuthor } from "@drakkar.software/starfish-protocol";
|
|
3
3
|
import type { StarfishClientOptions } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Whether a {@link PullResult} was served from the offline read-through cache
|
|
6
|
+
* (the transport was unreachable) rather than a live server response. Used by
|
|
7
|
+
* {@link SyncManager} to surface a `stale` flag to the UI without treating a
|
|
8
|
+
* cache hit as proof the server is reachable.
|
|
9
|
+
*/
|
|
10
|
+
export declare function pullWasFromCache(result: PullResult): boolean;
|
|
4
11
|
/** The storage `documentKey` for a push `path`: the path with the `/push/`
|
|
5
12
|
* action prefix stripped (the namespace lives only in the URL). The author
|
|
6
13
|
* signature binds to this key. */
|
|
@@ -76,12 +83,20 @@ export declare class StarfishClient {
|
|
|
76
83
|
private readonly namespace?;
|
|
77
84
|
private readonly capProvider?;
|
|
78
85
|
private readonly fetch;
|
|
86
|
+
private readonly cache?;
|
|
87
|
+
private readonly cacheMaxAgeMs?;
|
|
79
88
|
/**
|
|
80
89
|
* Installed client-side plugins. Currently stored as inert data; no
|
|
81
90
|
* hooks fire yet. Extensions can inspect this list if needed.
|
|
82
91
|
*/
|
|
83
92
|
readonly plugins: ReadonlyArray<import("./types.js").ClientPlugin>;
|
|
84
93
|
constructor(options: StarfishClientOptions);
|
|
94
|
+
/**
|
|
95
|
+
* Mark a `PullResult` as having been served from the offline read-through
|
|
96
|
+
* cache (transport was unreachable). Non-enumerable so it doesn't leak into
|
|
97
|
+
* JSON / equality / re-caching; read via {@link pullWasFromCache}.
|
|
98
|
+
*/
|
|
99
|
+
private tagFromCache;
|
|
85
100
|
/**
|
|
86
101
|
* Resolve the host portion of the URL the client will send to. The host
|
|
87
102
|
* is folded into the signed canonical input as the `h` field so the
|
|
@@ -142,6 +157,18 @@ export declare class StarfishClient {
|
|
|
142
157
|
pull(path: string, options: PullOptions): Promise<PullResult>;
|
|
143
158
|
/** Pull an append-only collection. Extracts and returns `data[appendField]` as `T[]`. */
|
|
144
159
|
pull<T = unknown>(path: string, options: AppendPullOptions): Promise<T[]>;
|
|
160
|
+
/**
|
|
161
|
+
* Read the cached snapshot for a document `path` WITHOUT hitting the network —
|
|
162
|
+
* the basis for cache-first paint (seed the UI from the last-synced snapshot,
|
|
163
|
+
* then revalidate with a live {@link pull}). Returns the tagged `PullResult`,
|
|
164
|
+
* or null when no cache is configured / there's no entry. Namespacing matches
|
|
165
|
+
* {@link pull}, so the key lines up with whatever `pull` wrote.
|
|
166
|
+
*/
|
|
167
|
+
peekCache(path: string): Promise<PullResult | null>;
|
|
168
|
+
/** Read + parse a cached pull snapshot, tagged {@link tagFromCache}. Returns
|
|
169
|
+
* null on a miss or an unparseable blob (never throws — a corrupt cache entry
|
|
170
|
+
* must not break a pull, just miss). */
|
|
171
|
+
private readCache;
|
|
145
172
|
/**
|
|
146
173
|
* Pull several documents in one round-trip via `/batch/pull`. `collections` is
|
|
147
174
|
* the list of distinct collection names; `opts.params` supplies, per collection,
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export { stableStringify, computeHash } from "@drakkar.software/starfish-protoco
|
|
|
4
4
|
export { buildRevocationList, revocationListCanonicalSigningInput } from "@drakkar.software/starfish-protocol";
|
|
5
5
|
export type { RevocationList, RevocationEntry, RevokedSubject, BuildRevocationListOpts, } from "@drakkar.software/starfish-protocol";
|
|
6
6
|
export type { PullResult, PushSuccess, PullKeyringProjection } from "@drakkar.software/starfish-protocol";
|
|
7
|
-
export { StarfishClient } from "./client.js";
|
|
7
|
+
export { StarfishClient, pullWasFromCache } from "./client.js";
|
|
8
8
|
export type { BlobPullResult, BlobPushResult, AppendPullOptions, PullOptions, BatchPullOptions, BatchPullResult, BatchPullEntry, } from "./client.js";
|
|
9
9
|
export { SyncManager, AbortError } from "./sync.js";
|
|
10
10
|
export type { SyncManagerOptions, SyncSigner } from "./sync.js";
|
|
@@ -13,7 +13,7 @@ export type { AppendLogCursorOptions, AppendElement, AuthorVerifier, ElementErro
|
|
|
13
13
|
export { ENCRYPTED_KEY } from "@drakkar.software/starfish-protocol";
|
|
14
14
|
export type { Encryptor } from "@drakkar.software/starfish-protocol";
|
|
15
15
|
export { ConflictError, StarfishHttpError, } from "./types.js";
|
|
16
|
-
export type { StarfishClientOptions, StarfishCapProvider, ConflictResolver, ClientPlugin, } from "./types.js";
|
|
16
|
+
export type { StarfishClientOptions, StarfishCapProvider, PullCache, ConflictResolver, ClientPlugin, } from "./types.js";
|
|
17
17
|
export { consoleSyncLogger, noopSyncLogger, createMetricsCollector } from "./logger.js";
|
|
18
18
|
export type { SyncLogger, SyncMetrics, MetricsCollector } from "./logger.js";
|
|
19
19
|
export { createMigrator } from "./migrate.js";
|
package/dist/index.js
CHANGED
|
@@ -41,6 +41,13 @@ var StarfishHttpError = class extends Error {
|
|
|
41
41
|
|
|
42
42
|
// src/client.ts
|
|
43
43
|
var APPEND_DEFAULT_FIELD = "items";
|
|
44
|
+
function pullCacheKey(pathAndQuery) {
|
|
45
|
+
const q = pathAndQuery.indexOf("?");
|
|
46
|
+
return q === -1 ? pathAndQuery : pathAndQuery.slice(0, q);
|
|
47
|
+
}
|
|
48
|
+
function pullWasFromCache(result) {
|
|
49
|
+
return result.fromCache === true;
|
|
50
|
+
}
|
|
44
51
|
function stripPushPrefix(path) {
|
|
45
52
|
return path.startsWith(PUSH_PATH_PREFIX) ? path.slice(PUSH_PATH_PREFIX.length) : path;
|
|
46
53
|
}
|
|
@@ -58,6 +65,8 @@ var StarfishClient = class {
|
|
|
58
65
|
namespace;
|
|
59
66
|
capProvider;
|
|
60
67
|
fetch;
|
|
68
|
+
cache;
|
|
69
|
+
cacheMaxAgeMs;
|
|
61
70
|
/**
|
|
62
71
|
* Installed client-side plugins. Currently stored as inert data; no
|
|
63
72
|
* hooks fire yet. Extensions can inspect this list if needed.
|
|
@@ -68,8 +77,19 @@ var StarfishClient = class {
|
|
|
68
77
|
this.namespace = options.namespace || void 0;
|
|
69
78
|
this.capProvider = options.capProvider;
|
|
70
79
|
this.fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
80
|
+
this.cache = options.cache;
|
|
81
|
+
this.cacheMaxAgeMs = options.cacheMaxAgeMs;
|
|
71
82
|
this.plugins = options.plugins ? [...options.plugins] : [];
|
|
72
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* Mark a `PullResult` as having been served from the offline read-through
|
|
86
|
+
* cache (transport was unreachable). Non-enumerable so it doesn't leak into
|
|
87
|
+
* JSON / equality / re-caching; read via {@link pullWasFromCache}.
|
|
88
|
+
*/
|
|
89
|
+
tagFromCache(result) {
|
|
90
|
+
Object.defineProperty(result, "fromCache", { value: true, enumerable: false });
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
73
93
|
/**
|
|
74
94
|
* Resolve the host portion of the URL the client will send to. The host
|
|
75
95
|
* is folded into the signed canonical input as the `h` field so the
|
|
@@ -189,10 +209,20 @@ var StarfishClient = class {
|
|
|
189
209
|
}
|
|
190
210
|
const url = `${this.baseUrl}${pathAndQuery}`;
|
|
191
211
|
const authHeaders = await this.buildAuthHeaders("GET", pathAndQuery, void 0);
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
212
|
+
const cacheKey = this.cache && appendField === void 0 ? pullCacheKey(pathAndQuery) : void 0;
|
|
213
|
+
let res;
|
|
214
|
+
try {
|
|
215
|
+
res = await this.fetch(url, {
|
|
216
|
+
method: "GET",
|
|
217
|
+
headers: { [HEADER_ACCEPT]: "application/json", ...authHeaders }
|
|
218
|
+
});
|
|
219
|
+
} catch (err) {
|
|
220
|
+
if (cacheKey) {
|
|
221
|
+
const cached = await this.readCache(cacheKey);
|
|
222
|
+
if (cached) return cached;
|
|
223
|
+
}
|
|
224
|
+
throw err;
|
|
225
|
+
}
|
|
196
226
|
if (!res.ok) {
|
|
197
227
|
throw new StarfishHttpError(res.status, await res.text());
|
|
198
228
|
}
|
|
@@ -201,8 +231,46 @@ var StarfishClient = class {
|
|
|
201
231
|
const list = result.data?.[appendField];
|
|
202
232
|
return Array.isArray(list) ? list : [];
|
|
203
233
|
}
|
|
234
|
+
if (cacheKey) {
|
|
235
|
+
const snapshot = {
|
|
236
|
+
data: result.data,
|
|
237
|
+
hash: result.hash,
|
|
238
|
+
timestamp: result.timestamp,
|
|
239
|
+
cachedAt: Date.now()
|
|
240
|
+
};
|
|
241
|
+
void this.cache.set(cacheKey, JSON.stringify(snapshot)).catch(() => {
|
|
242
|
+
});
|
|
243
|
+
}
|
|
204
244
|
return result;
|
|
205
245
|
}
|
|
246
|
+
/**
|
|
247
|
+
* Read the cached snapshot for a document `path` WITHOUT hitting the network —
|
|
248
|
+
* the basis for cache-first paint (seed the UI from the last-synced snapshot,
|
|
249
|
+
* then revalidate with a live {@link pull}). Returns the tagged `PullResult`,
|
|
250
|
+
* or null when no cache is configured / there's no entry. Namespacing matches
|
|
251
|
+
* {@link pull}, so the key lines up with whatever `pull` wrote.
|
|
252
|
+
*/
|
|
253
|
+
async peekCache(path) {
|
|
254
|
+
if (!this.cache) return null;
|
|
255
|
+
return this.readCache(pullCacheKey(this.applyNamespace(path)));
|
|
256
|
+
}
|
|
257
|
+
/** Read + parse a cached pull snapshot, tagged {@link tagFromCache}. Returns
|
|
258
|
+
* null on a miss or an unparseable blob (never throws — a corrupt cache entry
|
|
259
|
+
* must not break a pull, just miss). */
|
|
260
|
+
async readCache(cacheKey) {
|
|
261
|
+
try {
|
|
262
|
+
const raw = await this.cache.get(cacheKey);
|
|
263
|
+
if (!raw) return null;
|
|
264
|
+
const parsed = JSON.parse(raw);
|
|
265
|
+
if (!parsed || typeof parsed.hash !== "string") return null;
|
|
266
|
+
if (this.cacheMaxAgeMs != null && Date.now() - (parsed.cachedAt ?? 0) > this.cacheMaxAgeMs) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
return this.tagFromCache({ data: parsed.data ?? {}, hash: parsed.hash, timestamp: parsed.timestamp ?? 0 });
|
|
270
|
+
} catch {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
206
274
|
/**
|
|
207
275
|
* Pull several documents in one round-trip via `/batch/pull`. `collections` is
|
|
208
276
|
* the list of distinct collection names; `opts.params` supplies, per collection,
|
|
@@ -430,6 +498,7 @@ var SyncManager = class {
|
|
|
430
498
|
lastCheckpoint = 0;
|
|
431
499
|
localData = {};
|
|
432
500
|
aborted = false;
|
|
501
|
+
lastFromCache = false;
|
|
433
502
|
constructor(options) {
|
|
434
503
|
this.client = options.client;
|
|
435
504
|
this.pullPath = options.pullPath;
|
|
@@ -470,6 +539,40 @@ var SyncManager = class {
|
|
|
470
539
|
setHash(hash) {
|
|
471
540
|
this.lastHash = hash;
|
|
472
541
|
}
|
|
542
|
+
/**
|
|
543
|
+
* Whether the most recent {@link pull} (or {@link seedFromCache}) was served
|
|
544
|
+
* from the client's offline read-through cache rather than a live server
|
|
545
|
+
* response. The binding surfaces this as a `stale` flag so the UI can show an
|
|
546
|
+
* offline indicator without treating a cache hit as "reachable". Reset to
|
|
547
|
+
* false by the next successful network pull.
|
|
548
|
+
*/
|
|
549
|
+
getLastPullFromCache() {
|
|
550
|
+
return this.lastFromCache;
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Cache-first paint: seed `localData` from the client's read-through cache
|
|
554
|
+
* WITHOUT touching the network, decrypting in memory for E2E collections.
|
|
555
|
+
* Returns whether anything was seeded (false on a miss, an expired entry, or
|
|
556
|
+
* a decrypt failure — e.g. keyring skew). Call once on store creation before
|
|
557
|
+
* the initial live {@link pull}, which then supersedes the seeded snapshot.
|
|
558
|
+
* Requires the client to have been built with a `cache`.
|
|
559
|
+
*/
|
|
560
|
+
async seedFromCache() {
|
|
561
|
+
if (this.aborted) return false;
|
|
562
|
+
const cached = await this.client.peekCache(this.pullPath);
|
|
563
|
+
if (!cached) return false;
|
|
564
|
+
let data;
|
|
565
|
+
try {
|
|
566
|
+
data = this.encryptor ? await this.encryptor.decrypt(cached.data) : cached.data;
|
|
567
|
+
} catch {
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
if (this.aborted) return false;
|
|
571
|
+
this.localData = data;
|
|
572
|
+
this.lastHash = cached.hash;
|
|
573
|
+
this.lastFromCache = true;
|
|
574
|
+
return true;
|
|
575
|
+
}
|
|
473
576
|
getCheckpoint() {
|
|
474
577
|
return this.lastCheckpoint;
|
|
475
578
|
}
|
|
@@ -480,6 +583,7 @@ var SyncManager = class {
|
|
|
480
583
|
try {
|
|
481
584
|
const result = await this.client.pull(this.pullPath, this.lastCheckpoint);
|
|
482
585
|
if (this.aborted) throw new AbortError();
|
|
586
|
+
this.lastFromCache = pullWasFromCache(result);
|
|
483
587
|
if (this.encryptor) {
|
|
484
588
|
const decrypted = await this.encryptor.decrypt(result.data);
|
|
485
589
|
if (this.aborted) throw new AbortError();
|
|
@@ -1671,6 +1775,7 @@ export {
|
|
|
1671
1775
|
isServiceWorkerSupported,
|
|
1672
1776
|
noopSyncLogger,
|
|
1673
1777
|
pruneTombstones,
|
|
1778
|
+
pullWasFromCache,
|
|
1674
1779
|
registerBackgroundSync,
|
|
1675
1780
|
registerServiceWorker,
|
|
1676
1781
|
revocationListCanonicalSigningInput,
|