@drakkar.software/starfish-client 3.0.0-alpha.31 → 3.0.0-alpha.36
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 +5 -1
- package/dist/bindings/zustand.d.ts +13 -1
- package/dist/bindings/zustand.js +163 -80
- package/dist/bindings/zustand.js.map +2 -2
- package/dist/client.d.ts +44 -6
- package/dist/index.js +144 -80
- package/dist/index.js.map +2 -2
- package/dist/sync.d.ts +22 -0
- package/dist/types.d.ts +13 -7
- package/package.json +3 -3
package/dist/client.d.ts
CHANGED
|
@@ -54,6 +54,21 @@ export interface PullOptions {
|
|
|
54
54
|
checkpoint?: number;
|
|
55
55
|
/** Include the sibling `_keyring` document in the response. Defaults to false. */
|
|
56
56
|
withKeyring?: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Serve the last-synced cached snapshot immediately (tagged via
|
|
59
|
+
* {@link pullWasFromCache}) and revalidate in the background. Requires a
|
|
60
|
+
* {@link StarfishClientOptions.cache} to be configured; without one the option
|
|
61
|
+
* is a no-op and the pull goes to the network as usual.
|
|
62
|
+
*
|
|
63
|
+
* On a cache hit: returns the stale snapshot at once, kicks a background fetch,
|
|
64
|
+
* and on success writes the fresh snapshot to cache and fires
|
|
65
|
+
* {@link StarfishClientOptions.onRevalidated}. Uses the same dedup set as the
|
|
66
|
+
* {@link StarfishClientOptions.cacheFallbackStatuses} revalidation path — a
|
|
67
|
+
* concurrent error-triggered loop for the same document is not duplicated.
|
|
68
|
+
*
|
|
69
|
+
* On a cache miss: falls through to the normal network-first pull unchanged.
|
|
70
|
+
*/
|
|
71
|
+
staleWhileRevalidate?: boolean;
|
|
57
72
|
}
|
|
58
73
|
/** Per-collection result in a {@link BatchPullResult}: either the pulled
|
|
59
74
|
* document (`data`/`hash`/`timestamp`) or a per-collection `error` string. */
|
|
@@ -95,6 +110,12 @@ export declare class StarfishClient {
|
|
|
95
110
|
private readonly cacheFallbackStatuses?;
|
|
96
111
|
private readonly onRevalidated?;
|
|
97
112
|
private readonly revalidating;
|
|
113
|
+
/**
|
|
114
|
+
* In-memory mirror of the latest document timestamp written to each cache
|
|
115
|
+
* key via {@link writeCache}. Updated synchronously so {@link revalidateLoop}
|
|
116
|
+
* can guard against stale overwrites without an extra async cache read.
|
|
117
|
+
*/
|
|
118
|
+
private readonly latestCacheTimestamp;
|
|
98
119
|
/**
|
|
99
120
|
* Installed client-side plugins. Currently stored as inert data; no
|
|
100
121
|
* hooks fire yet. Extensions can inspect this list if needed.
|
|
@@ -167,14 +188,31 @@ export declare class StarfishClient {
|
|
|
167
188
|
pull(path: string, options: PullOptions): Promise<PullResult>;
|
|
168
189
|
/** Pull an append-only collection. Extracts and returns `data[appendField]` as `T[]`. */
|
|
169
190
|
pull<T = unknown>(path: string, options: AppendPullOptions): Promise<T[]>;
|
|
170
|
-
/**
|
|
191
|
+
/**
|
|
192
|
+
* Write a pull snapshot to the cache. Fire-and-forget; errors are swallowed
|
|
193
|
+
* so a failing cache never blocks the caller. No-op when no cache is configured.
|
|
194
|
+
*/
|
|
195
|
+
private writeCache;
|
|
196
|
+
/** Build the URL + auth headers for one revalidation GET. Shared between
|
|
197
|
+
* {@link pull} and {@link revalidateLoop} to avoid duplicated fetch setup. */
|
|
198
|
+
private revalidateFetch;
|
|
199
|
+
/**
|
|
200
|
+
* Deduplicated fire-and-forget: starts one revalidation loop per cacheKey.
|
|
201
|
+
* Used by both the {@link cacheFallbackStatuses} error path (delayed first
|
|
202
|
+
* attempt, honoring `Retry-After`) and the {@link PullOptions.staleWhileRevalidate}
|
|
203
|
+
* read path (`immediate: true` — no initial delay on the first attempt). The
|
|
204
|
+
* `revalidating` set deduplicates across both triggers so a concurrent
|
|
205
|
+
* error-triggered loop and an SWR-on-read loop for the same key collapse to one.
|
|
206
|
+
*/
|
|
171
207
|
private scheduleRevalidate;
|
|
172
208
|
/**
|
|
173
|
-
* Background revalidation loop
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
209
|
+
* Background revalidation loop shared by both {@link cacheFallbackStatuses}
|
|
210
|
+
* hits and {@link PullOptions.staleWhileRevalidate} reads.
|
|
211
|
+
*
|
|
212
|
+
* Retries (honoring `Retry-After`) up to {@link MAX_REVALIDATE_ATTEMPTS} times.
|
|
213
|
+
* When `immediate` is true the first attempt fires without any initial delay
|
|
214
|
+
* (SWR-on-read path). On a live 2xx the fresh snapshot is written to cache and
|
|
215
|
+
* {@link onRevalidated} fires. Stops early on a non-fallback status (403/404).
|
|
178
216
|
*/
|
|
179
217
|
private revalidateLoop;
|
|
180
218
|
/**
|
package/dist/index.js
CHANGED
|
@@ -110,6 +110,12 @@ var StarfishClient = class {
|
|
|
110
110
|
cacheFallbackStatuses;
|
|
111
111
|
onRevalidated;
|
|
112
112
|
revalidating = /* @__PURE__ */ new Set();
|
|
113
|
+
/**
|
|
114
|
+
* In-memory mirror of the latest document timestamp written to each cache
|
|
115
|
+
* key via {@link writeCache}. Updated synchronously so {@link revalidateLoop}
|
|
116
|
+
* can guard against stale overwrites without an extra async cache read.
|
|
117
|
+
*/
|
|
118
|
+
latestCacheTimestamp = /* @__PURE__ */ new Map();
|
|
113
119
|
/**
|
|
114
120
|
* Installed client-side plugins. Currently stored as inert data; no
|
|
115
121
|
* hooks fire yet. Extensions can inspect this list if needed.
|
|
@@ -226,11 +232,12 @@ var StarfishClient = class {
|
|
|
226
232
|
async pull(path, checkpointOrOptions) {
|
|
227
233
|
let pathAndQuery = this.applyNamespace(path);
|
|
228
234
|
let appendField;
|
|
235
|
+
let swr = false;
|
|
229
236
|
if (typeof checkpointOrOptions === "number") {
|
|
230
237
|
if (checkpointOrOptions) pathAndQuery += `?checkpoint=${checkpointOrOptions}`;
|
|
231
238
|
} else if (checkpointOrOptions != null) {
|
|
232
239
|
const opts = checkpointOrOptions;
|
|
233
|
-
const isPullOptions = opts.withKeyring !== void 0 || opts.checkpoint !== void 0;
|
|
240
|
+
const isPullOptions = opts.withKeyring !== void 0 || opts.checkpoint !== void 0 || opts.staleWhileRevalidate !== void 0;
|
|
234
241
|
const params = new URLSearchParams();
|
|
235
242
|
if (isPullOptions) {
|
|
236
243
|
if (opts.checkpoint != null && opts.checkpoint > 0) {
|
|
@@ -239,6 +246,7 @@ var StarfishClient = class {
|
|
|
239
246
|
if (opts.withKeyring) {
|
|
240
247
|
params.set("withKeyring", "1");
|
|
241
248
|
}
|
|
249
|
+
swr = opts.staleWhileRevalidate === true;
|
|
242
250
|
} else {
|
|
243
251
|
appendField = opts.appendField ?? APPEND_DEFAULT_FIELD;
|
|
244
252
|
if (opts.full && (opts.since != null || opts.limit != null || opts.last != null)) {
|
|
@@ -265,6 +273,19 @@ var StarfishClient = class {
|
|
|
265
273
|
const url = `${this.baseUrl}${pathAndQuery}`;
|
|
266
274
|
const authHeaders = await this.buildAuthHeaders("GET", pathAndQuery, void 0);
|
|
267
275
|
const cacheKey = this.cache && appendField === void 0 ? pullCacheKey(pathAndQuery) : void 0;
|
|
276
|
+
if (swr && cacheKey) {
|
|
277
|
+
const cached = await this.readCache(cacheKey);
|
|
278
|
+
if (cached) {
|
|
279
|
+
this.scheduleRevalidate(
|
|
280
|
+
cacheKey,
|
|
281
|
+
pathAndQuery,
|
|
282
|
+
null,
|
|
283
|
+
/* immediate */
|
|
284
|
+
true
|
|
285
|
+
);
|
|
286
|
+
return cached;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
268
289
|
let res;
|
|
269
290
|
try {
|
|
270
291
|
res = await this.fetch(url, {
|
|
@@ -296,67 +317,87 @@ var StarfishClient = class {
|
|
|
296
317
|
const list = result.data?.[appendField];
|
|
297
318
|
return Array.isArray(list) ? list : [];
|
|
298
319
|
}
|
|
299
|
-
if (cacheKey)
|
|
300
|
-
const snapshot = {
|
|
301
|
-
data: result.data,
|
|
302
|
-
hash: result.hash,
|
|
303
|
-
timestamp: result.timestamp,
|
|
304
|
-
cachedAt: Date.now()
|
|
305
|
-
};
|
|
306
|
-
void this.cache.set(cacheKey, JSON.stringify(snapshot)).catch(() => {
|
|
307
|
-
});
|
|
308
|
-
}
|
|
320
|
+
if (cacheKey) this.writeCache(cacheKey, result);
|
|
309
321
|
return result;
|
|
310
322
|
}
|
|
311
|
-
/**
|
|
312
|
-
|
|
323
|
+
/**
|
|
324
|
+
* Write a pull snapshot to the cache. Fire-and-forget; errors are swallowed
|
|
325
|
+
* so a failing cache never blocks the caller. No-op when no cache is configured.
|
|
326
|
+
*/
|
|
327
|
+
writeCache(cacheKey, result) {
|
|
328
|
+
if (!this.cache) return;
|
|
329
|
+
if (result.timestamp > (this.latestCacheTimestamp.get(cacheKey) ?? -1)) {
|
|
330
|
+
this.latestCacheTimestamp.set(cacheKey, result.timestamp);
|
|
331
|
+
}
|
|
332
|
+
const snapshot = {
|
|
333
|
+
data: result.data,
|
|
334
|
+
hash: result.hash,
|
|
335
|
+
timestamp: result.timestamp,
|
|
336
|
+
cachedAt: Date.now()
|
|
337
|
+
};
|
|
338
|
+
void this.cache.set(cacheKey, JSON.stringify(snapshot)).catch(() => {
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
/** Build the URL + auth headers for one revalidation GET. Shared between
|
|
342
|
+
* {@link pull} and {@link revalidateLoop} to avoid duplicated fetch setup. */
|
|
343
|
+
async revalidateFetch(pathAndQuery) {
|
|
344
|
+
const url = `${this.baseUrl}${pathAndQuery}`;
|
|
345
|
+
const authHeaders = await this.buildAuthHeaders("GET", pathAndQuery, void 0);
|
|
346
|
+
return this.fetch(url, {
|
|
347
|
+
method: "GET",
|
|
348
|
+
headers: { [HEADER_ACCEPT]: "application/json", ...authHeaders }
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Deduplicated fire-and-forget: starts one revalidation loop per cacheKey.
|
|
353
|
+
* Used by both the {@link cacheFallbackStatuses} error path (delayed first
|
|
354
|
+
* attempt, honoring `Retry-After`) and the {@link PullOptions.staleWhileRevalidate}
|
|
355
|
+
* read path (`immediate: true` — no initial delay on the first attempt). The
|
|
356
|
+
* `revalidating` set deduplicates across both triggers so a concurrent
|
|
357
|
+
* error-triggered loop and an SWR-on-read loop for the same key collapse to one.
|
|
358
|
+
*/
|
|
359
|
+
scheduleRevalidate(cacheKey, pathAndQuery, retryAfterHeader, immediate = false) {
|
|
313
360
|
if (this.revalidating.has(cacheKey)) return;
|
|
314
361
|
this.revalidating.add(cacheKey);
|
|
315
|
-
void this.revalidateLoop(cacheKey, pathAndQuery, retryAfterHeader).finally(() => {
|
|
362
|
+
void this.revalidateLoop(cacheKey, pathAndQuery, retryAfterHeader, immediate).finally(() => {
|
|
316
363
|
this.revalidating.delete(cacheKey);
|
|
317
364
|
});
|
|
318
365
|
}
|
|
319
366
|
/**
|
|
320
|
-
* Background revalidation loop
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
*
|
|
367
|
+
* Background revalidation loop shared by both {@link cacheFallbackStatuses}
|
|
368
|
+
* hits and {@link PullOptions.staleWhileRevalidate} reads.
|
|
369
|
+
*
|
|
370
|
+
* Retries (honoring `Retry-After`) up to {@link MAX_REVALIDATE_ATTEMPTS} times.
|
|
371
|
+
* When `immediate` is true the first attempt fires without any initial delay
|
|
372
|
+
* (SWR-on-read path). On a live 2xx the fresh snapshot is written to cache and
|
|
373
|
+
* {@link onRevalidated} fires. Stops early on a non-fallback status (403/404).
|
|
325
374
|
*/
|
|
326
|
-
async revalidateLoop(cacheKey, pathAndQuery, firstRetryAfter) {
|
|
375
|
+
async revalidateLoop(cacheKey, pathAndQuery, firstRetryAfter, immediate = false) {
|
|
327
376
|
let retryAfterHeader = firstRetryAfter;
|
|
377
|
+
const fallbackSet = this.cacheFallbackStatuses ? new Set(this.cacheFallbackStatuses) : null;
|
|
328
378
|
for (let attempt = 0; attempt < MAX_REVALIDATE_ATTEMPTS; attempt++) {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
await sleep(delay);
|
|
337
|
-
try {
|
|
338
|
-
const url = `${this.baseUrl}${pathAndQuery}`;
|
|
339
|
-
const authHeaders = await this.buildAuthHeaders("GET", pathAndQuery, void 0);
|
|
340
|
-
const res = await this.fetch(url, {
|
|
341
|
-
method: "GET",
|
|
342
|
-
headers: { [HEADER_ACCEPT]: "application/json", ...authHeaders }
|
|
379
|
+
if (!immediate || attempt > 0) {
|
|
380
|
+
const delay = parseRetryAfterMs(retryAfterHeader, {
|
|
381
|
+
fallbackMs: Math.min(
|
|
382
|
+
REVALIDATE_INITIAL_DELAY_MS * Math.pow(2, attempt),
|
|
383
|
+
REVALIDATE_MAX_DELAY_MS
|
|
384
|
+
),
|
|
385
|
+
maxMs: REVALIDATE_MAX_DELAY_MS
|
|
343
386
|
});
|
|
387
|
+
await sleep(delay);
|
|
388
|
+
}
|
|
389
|
+
try {
|
|
390
|
+
const res = await this.revalidateFetch(pathAndQuery);
|
|
344
391
|
if (res.ok) {
|
|
345
392
|
const result = await res.json();
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
timestamp: result.timestamp,
|
|
351
|
-
cachedAt: Date.now()
|
|
352
|
-
};
|
|
353
|
-
void this.cache.set(cacheKey, JSON.stringify(snapshot)).catch(() => {
|
|
354
|
-
});
|
|
393
|
+
const latestTs = this.latestCacheTimestamp.get(cacheKey) ?? -1;
|
|
394
|
+
if (result.timestamp >= latestTs) {
|
|
395
|
+
this.writeCache(cacheKey, result);
|
|
396
|
+
this.onRevalidated?.(pathAndQuery, result);
|
|
355
397
|
}
|
|
356
|
-
this.onRevalidated?.(pathAndQuery, result);
|
|
357
398
|
return;
|
|
358
399
|
}
|
|
359
|
-
if (!
|
|
400
|
+
if (!fallbackSet?.has(res.status)) {
|
|
360
401
|
return;
|
|
361
402
|
}
|
|
362
403
|
retryAfterHeader = res.headers.get("Retry-After");
|
|
@@ -478,15 +519,7 @@ var StarfishClient = class {
|
|
|
478
519
|
const result = await res.json();
|
|
479
520
|
if (this.cache) {
|
|
480
521
|
const pullPath = sendPath.replace("/push/", "/pull/");
|
|
481
|
-
|
|
482
|
-
const snapshot = {
|
|
483
|
-
data,
|
|
484
|
-
hash: result.hash,
|
|
485
|
-
timestamp: result.timestamp,
|
|
486
|
-
cachedAt: Date.now()
|
|
487
|
-
};
|
|
488
|
-
void this.cache.set(cacheKey, JSON.stringify(snapshot)).catch(() => {
|
|
489
|
-
});
|
|
522
|
+
this.writeCache(pullCacheKey(pullPath), { data, hash: result.hash, timestamp: result.timestamp });
|
|
490
523
|
}
|
|
491
524
|
return result;
|
|
492
525
|
}
|
|
@@ -767,6 +800,42 @@ var SyncManager = class {
|
|
|
767
800
|
getCheckpoint() {
|
|
768
801
|
return this.lastCheckpoint;
|
|
769
802
|
}
|
|
803
|
+
/**
|
|
804
|
+
* Apply a freshly-fetched `PullResult` to this manager's state WITHOUT
|
|
805
|
+
* firing a network request. Used by the zustand binding's `mergeResult`
|
|
806
|
+
* action to absorb a background revalidation result (delivered via
|
|
807
|
+
* {@link StarfishClientOptions.onRevalidated}) into the store.
|
|
808
|
+
*
|
|
809
|
+
* Like {@link pull}, `ingest` conflict-merges the snapshot against the
|
|
810
|
+
* established baseline via `this.onConflict` when a checkpoint exists — so a
|
|
811
|
+
* union-merge store does not lose array items when a revalidation result
|
|
812
|
+
* (e.g. a stale cache-fallback on 429/5xx) is a shorter snapshot. The first
|
|
813
|
+
* ingest (no checkpoint yet) takes the snapshot wholesale. Sets
|
|
814
|
+
* `lastFromCache = false` (a revalidation is a live response) so the binding
|
|
815
|
+
* can clear its `stale` flag.
|
|
816
|
+
*
|
|
817
|
+
* **Staleness guard**: if a `push()` advanced `lastCheckpoint` between the
|
|
818
|
+
* time the revalidation request was sent and the time it resolves, the
|
|
819
|
+
* result is from an older document version. Ingesting it would clobber the
|
|
820
|
+
* user's just-saved edit and reset `lastHash` to a stale server hash
|
|
821
|
+
* (causing a spurious 409 on the next push). We silently drop the result in
|
|
822
|
+
* that case — the store's post-push state is already correct.
|
|
823
|
+
*/
|
|
824
|
+
async ingest(result) {
|
|
825
|
+
if (this.aborted) return;
|
|
826
|
+
if (result.timestamp < this.lastCheckpoint) return;
|
|
827
|
+
let incoming;
|
|
828
|
+
if (this.encryptor) {
|
|
829
|
+
incoming = await this.encryptor.decrypt(result.data);
|
|
830
|
+
if (this.aborted) return;
|
|
831
|
+
} else {
|
|
832
|
+
incoming = result.data;
|
|
833
|
+
}
|
|
834
|
+
this.localData = this.lastCheckpoint > 0 ? this.onConflict(this.localData, incoming) : incoming;
|
|
835
|
+
this.lastHash = result.hash;
|
|
836
|
+
this.lastCheckpoint = result.timestamp;
|
|
837
|
+
this.lastFromCache = false;
|
|
838
|
+
}
|
|
770
839
|
async pull() {
|
|
771
840
|
if (this.aborted) throw new AbortError();
|
|
772
841
|
this.logger?.pullStart(this.loggerName);
|
|
@@ -775,17 +844,15 @@ var SyncManager = class {
|
|
|
775
844
|
const result = await this.client.pull(this.pullPath, this.lastCheckpoint);
|
|
776
845
|
if (this.aborted) throw new AbortError();
|
|
777
846
|
this.lastFromCache = pullWasFromCache(result);
|
|
847
|
+
let incoming;
|
|
778
848
|
if (this.encryptor) {
|
|
779
|
-
|
|
849
|
+
incoming = await this.encryptor.decrypt(result.data);
|
|
780
850
|
if (this.aborted) throw new AbortError();
|
|
781
|
-
this.localData = decrypted;
|
|
782
|
-
result.data = decrypted;
|
|
783
|
-
} else if (this.lastCheckpoint > 0) {
|
|
784
|
-
this.localData = deepMerge(this.localData, result.data);
|
|
785
|
-
result.data = this.localData;
|
|
786
851
|
} else {
|
|
787
|
-
|
|
852
|
+
incoming = result.data;
|
|
788
853
|
}
|
|
854
|
+
this.localData = this.lastCheckpoint > 0 ? this.onConflict(this.localData, incoming) : incoming;
|
|
855
|
+
result.data = this.localData;
|
|
789
856
|
this.lastHash = result.hash;
|
|
790
857
|
this.lastCheckpoint = result.timestamp;
|
|
791
858
|
this.logger?.pullSuccess(this.loggerName, Math.round(performance.now() - start));
|
|
@@ -971,20 +1038,22 @@ var AppendLogCursor = class {
|
|
|
971
1038
|
const raw = await this.client.pull(this.pullPath, opts);
|
|
972
1039
|
const batch = [];
|
|
973
1040
|
const stored = [];
|
|
974
|
-
let maxTs = since;
|
|
975
1041
|
let skipped = 0;
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1042
|
+
const eligible = raw.filter((el) => !(since > 0 && el.ts <= since));
|
|
1043
|
+
let maxTs = since;
|
|
1044
|
+
for (const el of eligible) if (el.ts > maxTs) maxTs = el.ts;
|
|
1045
|
+
const decryptResults = await Promise.all(eligible.map(async (el) => {
|
|
980
1046
|
try {
|
|
981
1047
|
this.verifyOne(el);
|
|
982
1048
|
const data = this.encryptor ? await this.encryptor.decrypt(el.data) : el.data;
|
|
983
|
-
decrypted
|
|
1049
|
+
return { el, decrypted: withAuthor(el.ts, data, el) };
|
|
984
1050
|
} catch (err) {
|
|
985
1051
|
if (this.onElementError !== "skip") throw err;
|
|
986
1052
|
skipped++;
|
|
1053
|
+
return { el, decrypted: null };
|
|
987
1054
|
}
|
|
1055
|
+
}));
|
|
1056
|
+
for (const { el, decrypted } of decryptResults) {
|
|
988
1057
|
if (this.persistEncrypted) {
|
|
989
1058
|
stored.push(withAuthor(el.ts, el.data, el));
|
|
990
1059
|
} else if (decrypted) {
|
|
@@ -1041,17 +1110,17 @@ var AppendLogCursor = class {
|
|
|
1041
1110
|
async getDecryptedItems() {
|
|
1042
1111
|
const snapshot = [...this.items];
|
|
1043
1112
|
if (!this.encryptor || !this.persistEncrypted) return snapshot;
|
|
1044
|
-
const
|
|
1045
|
-
for (const el of snapshot) {
|
|
1113
|
+
const results = await Promise.all(snapshot.map(async (el) => {
|
|
1046
1114
|
try {
|
|
1047
1115
|
this.verifyOne(el);
|
|
1048
1116
|
const data = await this.encryptor.decrypt(el.data);
|
|
1049
|
-
|
|
1117
|
+
return withAuthor(el.ts, data, el);
|
|
1050
1118
|
} catch (err) {
|
|
1051
1119
|
if (this.onElementError !== "skip") throw err;
|
|
1120
|
+
return null;
|
|
1052
1121
|
}
|
|
1053
|
-
}
|
|
1054
|
-
return
|
|
1122
|
+
}));
|
|
1123
|
+
return results.filter((el) => el !== null);
|
|
1055
1124
|
}
|
|
1056
1125
|
/** The current checkpoint: the max `ts` held (the next pull's `since`). `0`
|
|
1057
1126
|
* when nothing has been pulled or seeded. */
|
|
@@ -1197,8 +1266,7 @@ function shallowEqual(a, b) {
|
|
|
1197
1266
|
if (typeof a !== "object") return false;
|
|
1198
1267
|
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
1199
1268
|
if (Array.isArray(a) && Array.isArray(b)) {
|
|
1200
|
-
|
|
1201
|
-
return a.every((v, i) => shallowEqual(v, b[i]));
|
|
1269
|
+
return a.length === b.length && a.every((v, i) => shallowEqual(v, b[i]));
|
|
1202
1270
|
}
|
|
1203
1271
|
const aObj = a;
|
|
1204
1272
|
const bObj = b;
|
|
@@ -1740,12 +1808,8 @@ async function unregisterServiceWorkers() {
|
|
|
1740
1808
|
if (!isServiceWorkerSupported()) return false;
|
|
1741
1809
|
try {
|
|
1742
1810
|
const registrations = await navigator.serviceWorker.getRegistrations();
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
const result = await registration.unregister();
|
|
1746
|
-
if (result) unregistered = true;
|
|
1747
|
-
}
|
|
1748
|
-
return unregistered;
|
|
1811
|
+
const results = await Promise.all(registrations.map((r) => r.unregister()));
|
|
1812
|
+
return results.some(Boolean);
|
|
1749
1813
|
} catch {
|
|
1750
1814
|
return false;
|
|
1751
1815
|
}
|