@drakkar.software/starfish-client 3.0.0-alpha.0 → 3.0.0-alpha.10
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 +27 -0
- package/dist/append-log.d.ts +230 -0
- package/dist/bindings/legend.d.ts +23 -0
- package/dist/bindings/legend.js +32 -0
- package/dist/bindings/legend.js.map +2 -2
- package/dist/bindings/zustand.d.ts +45 -0
- package/dist/bindings/zustand.js +296 -45
- package/dist/bindings/zustand.js.map +3 -3
- package/dist/client.d.ts +95 -3
- package/dist/config.d.ts +2 -2
- package/dist/index.d.ts +5 -3
- package/dist/index.js +442 -47
- package/dist/index.js.map +4 -4
- package/dist/logger.d.ts +3 -0
- package/dist/mobile-lifecycle.d.ts +28 -1
- package/dist/types.d.ts +35 -1
- package/package.json +2 -2
package/dist/bindings/zustand.js
CHANGED
|
@@ -230,6 +230,22 @@ import { useEffect, useRef, useState, useCallback } from "react";
|
|
|
230
230
|
|
|
231
231
|
// src/client.ts
|
|
232
232
|
import {
|
|
233
|
+
AUTHOR_PUBKEY_FIELD,
|
|
234
|
+
AUTHOR_SIGNATURE_FIELD,
|
|
235
|
+
DATA_FIELD,
|
|
236
|
+
TS_FIELD,
|
|
237
|
+
BASE_HASH_FIELD,
|
|
238
|
+
PUSH_PATH_PREFIX,
|
|
239
|
+
HEADER_AUTHORIZATION,
|
|
240
|
+
HEADER_SIG,
|
|
241
|
+
HEADER_TS,
|
|
242
|
+
HEADER_NONCE,
|
|
243
|
+
HEADER_ALG,
|
|
244
|
+
HEADER_PUB,
|
|
245
|
+
HEADER_CONTENT_TYPE,
|
|
246
|
+
HEADER_ACCEPT,
|
|
247
|
+
DEFAULT_ALG,
|
|
248
|
+
signAppendAuthor,
|
|
233
249
|
signRequest,
|
|
234
250
|
stableStringify
|
|
235
251
|
} from "@drakkar.software/starfish-protocol";
|
|
@@ -252,6 +268,9 @@ var StarfishHttpError = class extends Error {
|
|
|
252
268
|
|
|
253
269
|
// src/client.ts
|
|
254
270
|
var APPEND_DEFAULT_FIELD = "items";
|
|
271
|
+
function stripPushPrefix(path) {
|
|
272
|
+
return path.startsWith(PUSH_PATH_PREFIX) ? path.slice(PUSH_PATH_PREFIX.length) : path;
|
|
273
|
+
}
|
|
255
274
|
function encodeCapAuth(cap) {
|
|
256
275
|
const json = stableStringify(cap);
|
|
257
276
|
if (typeof btoa === "function") {
|
|
@@ -263,6 +282,7 @@ function encodeCapAuth(cap) {
|
|
|
263
282
|
}
|
|
264
283
|
var StarfishClient = class {
|
|
265
284
|
baseUrl;
|
|
285
|
+
namespace;
|
|
266
286
|
capProvider;
|
|
267
287
|
fetch;
|
|
268
288
|
/**
|
|
@@ -272,6 +292,7 @@ var StarfishClient = class {
|
|
|
272
292
|
plugins;
|
|
273
293
|
constructor(options) {
|
|
274
294
|
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
295
|
+
this.namespace = options.namespace || void 0;
|
|
275
296
|
this.capProvider = options.capProvider;
|
|
276
297
|
this.fetch = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
277
298
|
this.plugins = options.plugins ? [...options.plugins] : [];
|
|
@@ -295,6 +316,20 @@ var StarfishClient = class {
|
|
|
295
316
|
return "";
|
|
296
317
|
}
|
|
297
318
|
}
|
|
319
|
+
/**
|
|
320
|
+
* Rewrite a request path for the configured namespace. A no-op when no
|
|
321
|
+
* namespace is set; otherwise `/{action}/…` becomes `/v1/{namespace}/{action}/…`
|
|
322
|
+
* (the `/v1` protocol-version segment is part of the namespaced route, matching
|
|
323
|
+
* the Python client and the server's namespace mount).
|
|
324
|
+
*
|
|
325
|
+
* Applied to the path used for BOTH the signature and the URL so the canonical
|
|
326
|
+
* path the client signs equals the path the server reconstructs from the URL.
|
|
327
|
+
* Covers SDK-helper-built paths too — that's the point: a namespace-unaware
|
|
328
|
+
* helper passing `/push/spaces/x/_keyring` reaches `/v1/{ns}/push/spaces/x/_keyring`.
|
|
329
|
+
*/
|
|
330
|
+
applyNamespace(path) {
|
|
331
|
+
return this.namespace ? `/v1/${this.namespace}${path}` : path;
|
|
332
|
+
}
|
|
298
333
|
/**
|
|
299
334
|
* Build auth headers for a request. When a `capProvider` is set, signs the
|
|
300
335
|
* request with the device's Ed25519 private key and returns the v3 header
|
|
@@ -306,26 +341,57 @@ var StarfishClient = class {
|
|
|
306
341
|
* The host bound into the signature is derived from `baseUrl` once per call.
|
|
307
342
|
*/
|
|
308
343
|
async buildAuthHeaders(method, pathAndQuery, body) {
|
|
309
|
-
if (this.capProvider) {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
344
|
+
if (!this.capProvider) return {};
|
|
345
|
+
const capCtx = await this.capProvider.getCap();
|
|
346
|
+
return this.capRequestHeaders(capCtx, method, pathAndQuery, body);
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Build the request-signing headers from an ALREADY-fetched cap context. Split
|
|
350
|
+
* out of {@link buildAuthHeaders} so {@link append} can fetch the cap once and
|
|
351
|
+
* reuse it for BOTH the author signature (over the element data) and the
|
|
352
|
+
* request signature (over the body), without redeeming the cap twice — a
|
|
353
|
+
* second `getCap()` could rotate keys and break the `authorPubkey ===
|
|
354
|
+
* presenter` bind the server checks.
|
|
355
|
+
*/
|
|
356
|
+
async capRequestHeaders(capCtx, method, pathAndQuery, body) {
|
|
357
|
+
const { cap, devEdPrivHex, pubHex, presenterAlg } = capCtx;
|
|
358
|
+
const req = {
|
|
359
|
+
method,
|
|
360
|
+
pathAndQuery,
|
|
361
|
+
body,
|
|
362
|
+
host: this.signingHost()
|
|
363
|
+
};
|
|
364
|
+
const signAlg = cap.kind === "audience" ? presenterAlg ?? DEFAULT_ALG : cap.subAlg ?? cap.issAlg;
|
|
365
|
+
const { alg, sig, ts, nonce } = await signRequest(req, devEdPrivHex, {
|
|
366
|
+
alg: signAlg
|
|
367
|
+
});
|
|
368
|
+
const headers = {
|
|
369
|
+
[HEADER_AUTHORIZATION]: `Cap ${encodeCapAuth(cap)}`,
|
|
370
|
+
[HEADER_SIG]: sig,
|
|
371
|
+
[HEADER_TS]: String(ts),
|
|
372
|
+
[HEADER_NONCE]: nonce,
|
|
373
|
+
[HEADER_ALG]: alg
|
|
374
|
+
};
|
|
375
|
+
if (pubHex !== void 0) headers[HEADER_PUB] = pubHex;
|
|
376
|
+
return headers;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Resolve the author public key to attach to a signed append: the redeemer's
|
|
380
|
+
* `pubHex` for an audience cap, else the cert subject `cap.sub` for a
|
|
381
|
+
* device/member cap. This is the SAME key that signs the request, so a server
|
|
382
|
+
* enforcing author proof can bind the stored element to its writer. Returns
|
|
383
|
+
* undefined only for a (malformed) cap with neither — the append then goes
|
|
384
|
+
* unsigned and a server requiring signatures rejects it.
|
|
385
|
+
*/
|
|
386
|
+
appendAuthorKey(capCtx) {
|
|
387
|
+
const { cap, pubHex, presenterAlg } = capCtx;
|
|
388
|
+
const authorPubHex = pubHex ?? cap.sub;
|
|
389
|
+
if (authorPubHex === void 0) return null;
|
|
390
|
+
const signAlg = cap.kind === "audience" ? presenterAlg ?? DEFAULT_ALG : cap.subAlg ?? cap.issAlg;
|
|
391
|
+
return { authorPubHex, signAlg };
|
|
326
392
|
}
|
|
327
393
|
async pull(path, checkpointOrOptions) {
|
|
328
|
-
let pathAndQuery = path;
|
|
394
|
+
let pathAndQuery = this.applyNamespace(path);
|
|
329
395
|
let appendField;
|
|
330
396
|
if (typeof checkpointOrOptions === "number") {
|
|
331
397
|
if (checkpointOrOptions) pathAndQuery += `?checkpoint=${checkpointOrOptions}`;
|
|
@@ -357,7 +423,7 @@ var StarfishClient = class {
|
|
|
357
423
|
const authHeaders = await this.buildAuthHeaders("GET", pathAndQuery, void 0);
|
|
358
424
|
const res = await this.fetch(url, {
|
|
359
425
|
method: "GET",
|
|
360
|
-
headers: {
|
|
426
|
+
headers: { [HEADER_ACCEPT]: "application/json", ...authHeaders }
|
|
361
427
|
});
|
|
362
428
|
if (!res.ok) {
|
|
363
429
|
throw new StarfishHttpError(res.status, await res.text());
|
|
@@ -369,27 +435,62 @@ var StarfishClient = class {
|
|
|
369
435
|
}
|
|
370
436
|
return result;
|
|
371
437
|
}
|
|
438
|
+
/**
|
|
439
|
+
* Pull several collections in one round-trip via `/batch/pull`. `collections`
|
|
440
|
+
* is the list of collection names; `opts.params` supplies path params per
|
|
441
|
+
* collection (serialized to a URL-encoded JSON `params` query). The server
|
|
442
|
+
* auto-fills the `{identity}` param from the authenticated caller, so per-user
|
|
443
|
+
* collections need no params. Returns a map of collection name → its pulled
|
|
444
|
+
* document or a per-collection `{ error }`. Honors the configured namespace.
|
|
445
|
+
*
|
|
446
|
+
* Note: not append/checkpoint-aware — for incremental append-only reads use
|
|
447
|
+
* `pull(path, { since })` (or `AppendLogCursor`) per collection.
|
|
448
|
+
*/
|
|
449
|
+
async batchPull(collections, opts = {}) {
|
|
450
|
+
const search = new URLSearchParams();
|
|
451
|
+
search.set("collections", collections.join(","));
|
|
452
|
+
if (opts.params && Object.keys(opts.params).length > 0) {
|
|
453
|
+
search.set("params", JSON.stringify(opts.params));
|
|
454
|
+
}
|
|
455
|
+
const pathAndQuery = `${this.applyNamespace("/batch/pull")}?${search.toString()}`;
|
|
456
|
+
const url = `${this.baseUrl}${pathAndQuery}`;
|
|
457
|
+
const authHeaders = await this.buildAuthHeaders("GET", pathAndQuery, void 0);
|
|
458
|
+
const res = await this.fetch(url, {
|
|
459
|
+
method: "GET",
|
|
460
|
+
headers: { [HEADER_ACCEPT]: "application/json", ...authHeaders }
|
|
461
|
+
});
|
|
462
|
+
if (!res.ok) {
|
|
463
|
+
throw new StarfishHttpError(res.status, await res.text());
|
|
464
|
+
}
|
|
465
|
+
return await res.json();
|
|
466
|
+
}
|
|
372
467
|
/**
|
|
373
468
|
* Push synced data to the server.
|
|
374
469
|
* @param path - The push endpoint path (e.g. "/push/users/abc/settings")
|
|
375
470
|
* @param data - The full document data to push
|
|
376
471
|
* @param baseHash - Hash of the document this push is based on (null for first push)
|
|
377
472
|
*
|
|
378
|
-
* v3 author
|
|
379
|
-
*
|
|
473
|
+
* v3 author proof (`authorPubkey` + `authorSignature`) is passed via `author`
|
|
474
|
+
* (produced by `SyncManager` when a `signer` is configured) and sent as
|
|
475
|
+
* top-level body siblings of `data`, where the server verifies it.
|
|
380
476
|
* @throws {ConflictError} if the server detects a hash mismatch (409)
|
|
381
477
|
*/
|
|
382
|
-
async push(path, data, baseHash) {
|
|
478
|
+
async push(path, data, baseHash, author) {
|
|
383
479
|
const body = JSON.stringify({
|
|
384
|
-
data,
|
|
385
|
-
baseHash
|
|
480
|
+
[DATA_FIELD]: data,
|
|
481
|
+
[BASE_HASH_FIELD]: baseHash,
|
|
482
|
+
...author && {
|
|
483
|
+
[AUTHOR_PUBKEY_FIELD]: author.authorPubkey,
|
|
484
|
+
[AUTHOR_SIGNATURE_FIELD]: author.authorSignature
|
|
485
|
+
}
|
|
386
486
|
});
|
|
387
|
-
const
|
|
388
|
-
const
|
|
487
|
+
const sendPath = this.applyNamespace(path);
|
|
488
|
+
const authHeaders = await this.buildAuthHeaders("POST", sendPath, body);
|
|
489
|
+
const res = await this.fetch(`${this.baseUrl}${sendPath}`, {
|
|
389
490
|
method: "POST",
|
|
390
491
|
headers: {
|
|
391
|
-
|
|
392
|
-
|
|
492
|
+
[HEADER_CONTENT_TYPE]: "application/json",
|
|
493
|
+
[HEADER_ACCEPT]: "application/json",
|
|
393
494
|
...authHeaders
|
|
394
495
|
},
|
|
395
496
|
body
|
|
@@ -402,21 +503,77 @@ var StarfishClient = class {
|
|
|
402
503
|
}
|
|
403
504
|
return res.json();
|
|
404
505
|
}
|
|
506
|
+
/**
|
|
507
|
+
* Append an element to an appendOnly (`by_timestamp`) collection.
|
|
508
|
+
*
|
|
509
|
+
* Unlike {@link push}, appendOnly writes carry no hash/conflict check — an
|
|
510
|
+
* authorized append is always accepted. Each element is stored server-side as
|
|
511
|
+
* `{ts, data}` and pulls can filter by `ts` via `since`/`checkpoint`.
|
|
512
|
+
*
|
|
513
|
+
* @param path - the push endpoint (e.g. "/push/events")
|
|
514
|
+
* @param data - the element payload. For a `delegated` collection, encrypt it
|
|
515
|
+
* first (e.g. `createKeyringEncryptor(keyring, kem).encrypt(data)`); the
|
|
516
|
+
* server stores it opaquely and never reads it.
|
|
517
|
+
* @param opts.ts - optional client-supplied element timestamp (ms). Must be a
|
|
518
|
+
* non-negative integer strictly greater than the latest stored element's ts
|
|
519
|
+
* (else the server responds 409). Omit to let the server assign one.
|
|
520
|
+
* @throws {StarfishHttpError} on a non-2xx response — e.g. 409
|
|
521
|
+
* `{ error: "non_monotonic_timestamp" }` for a non-monotonic timestamp, or
|
|
522
|
+
* `{ error: "append_limit_exceeded", limit }` if the collection's `maxItems`
|
|
523
|
+
* cap is reached (partition by a path parameter for higher volume).
|
|
524
|
+
*/
|
|
525
|
+
async append(path, data, opts = {}) {
|
|
526
|
+
const sendPath = this.applyNamespace(path);
|
|
527
|
+
const bodyObj = { [DATA_FIELD]: data };
|
|
528
|
+
if (opts.ts !== void 0) bodyObj[TS_FIELD] = opts.ts;
|
|
529
|
+
const capCtx = this.capProvider ? await this.capProvider.getCap() : null;
|
|
530
|
+
if (capCtx) {
|
|
531
|
+
const authorKey = this.appendAuthorKey(capCtx);
|
|
532
|
+
if (authorKey) {
|
|
533
|
+
const documentKey = stripPushPrefix(path);
|
|
534
|
+
const { authorPubkey, authorSignature } = signAppendAuthor(
|
|
535
|
+
documentKey,
|
|
536
|
+
data,
|
|
537
|
+
authorKey.authorPubHex,
|
|
538
|
+
capCtx.devEdPrivHex,
|
|
539
|
+
authorKey.signAlg
|
|
540
|
+
);
|
|
541
|
+
bodyObj[AUTHOR_PUBKEY_FIELD] = authorPubkey;
|
|
542
|
+
bodyObj[AUTHOR_SIGNATURE_FIELD] = authorSignature;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
const body = JSON.stringify(bodyObj);
|
|
546
|
+
const authHeaders = capCtx ? await this.capRequestHeaders(capCtx, "POST", sendPath, body) : {};
|
|
547
|
+
const res = await this.fetch(`${this.baseUrl}${sendPath}`, {
|
|
548
|
+
method: "POST",
|
|
549
|
+
headers: {
|
|
550
|
+
[HEADER_CONTENT_TYPE]: "application/json",
|
|
551
|
+
[HEADER_ACCEPT]: "application/json",
|
|
552
|
+
...authHeaders
|
|
553
|
+
},
|
|
554
|
+
body
|
|
555
|
+
});
|
|
556
|
+
if (!res.ok) {
|
|
557
|
+
throw new StarfishHttpError(res.status, await res.text());
|
|
558
|
+
}
|
|
559
|
+
return res.json();
|
|
560
|
+
}
|
|
405
561
|
/**
|
|
406
562
|
* Pull binary data from a blob collection.
|
|
407
563
|
* Returns raw bytes with the content hash from the ETag header.
|
|
408
564
|
*/
|
|
409
565
|
async pullBlob(path) {
|
|
410
|
-
const
|
|
411
|
-
const
|
|
566
|
+
const sendPath = this.applyNamespace(path);
|
|
567
|
+
const authHeaders = await this.buildAuthHeaders("GET", sendPath, void 0);
|
|
568
|
+
const res = await this.fetch(`${this.baseUrl}${sendPath}`, {
|
|
412
569
|
method: "GET",
|
|
413
|
-
headers: {
|
|
570
|
+
headers: { [HEADER_ACCEPT]: "*/*", ...authHeaders }
|
|
414
571
|
});
|
|
415
572
|
if (!res.ok) {
|
|
416
573
|
throw new StarfishHttpError(res.status, await res.text());
|
|
417
574
|
}
|
|
418
575
|
const etag = res.headers.get("ETag")?.replace(/"/g, "") ?? null;
|
|
419
|
-
const contentType = res.headers.get(
|
|
576
|
+
const contentType = res.headers.get(HEADER_CONTENT_TYPE) ?? "application/octet-stream";
|
|
420
577
|
const data = await res.arrayBuffer();
|
|
421
578
|
return { data, hash: etag, contentType };
|
|
422
579
|
}
|
|
@@ -425,12 +582,13 @@ var StarfishClient = class {
|
|
|
425
582
|
* Binary collections use last-write-wins (no conflict detection).
|
|
426
583
|
*/
|
|
427
584
|
async pushBlob(path, data, contentType) {
|
|
428
|
-
const
|
|
429
|
-
const
|
|
585
|
+
const sendPath = this.applyNamespace(path);
|
|
586
|
+
const authHeaders = await this.buildAuthHeaders("POST", sendPath, void 0);
|
|
587
|
+
const res = await this.fetch(`${this.baseUrl}${sendPath}`, {
|
|
430
588
|
method: "POST",
|
|
431
589
|
headers: {
|
|
432
|
-
|
|
433
|
-
|
|
590
|
+
[HEADER_CONTENT_TYPE]: contentType,
|
|
591
|
+
[HEADER_ACCEPT]: "application/json",
|
|
434
592
|
...authHeaders
|
|
435
593
|
},
|
|
436
594
|
body: data
|
|
@@ -443,7 +601,13 @@ var StarfishClient = class {
|
|
|
443
601
|
};
|
|
444
602
|
|
|
445
603
|
// src/sync.ts
|
|
446
|
-
import {
|
|
604
|
+
import {
|
|
605
|
+
AUTHOR_PUBKEY_FIELD as AUTHOR_PUBKEY_FIELD2,
|
|
606
|
+
AUTHOR_SIGNATURE_FIELD as AUTHOR_SIGNATURE_FIELD2,
|
|
607
|
+
deepMerge,
|
|
608
|
+
docAuthorCanonicalInput,
|
|
609
|
+
getBase64
|
|
610
|
+
} from "@drakkar.software/starfish-protocol";
|
|
447
611
|
|
|
448
612
|
// src/validate.ts
|
|
449
613
|
var ValidationError = class extends Error {
|
|
@@ -548,23 +712,24 @@ var SyncManager = class {
|
|
|
548
712
|
try {
|
|
549
713
|
const sealed = this.encryptor ? await this.encryptor.encrypt(pendingData) : pendingData;
|
|
550
714
|
if (this.aborted) throw new AbortError();
|
|
551
|
-
let
|
|
715
|
+
let author;
|
|
552
716
|
if (this.signer) {
|
|
553
717
|
const { devEdPubHex, sign } = await this.signer.getSigner();
|
|
554
718
|
if (this.aborted) throw new AbortError();
|
|
555
|
-
const
|
|
719
|
+
const documentKey = stripPushPrefix(this.pushPath);
|
|
720
|
+
const canonical = docAuthorCanonicalInput(documentKey, sealed);
|
|
556
721
|
const sigBytes = await sign(new TextEncoder().encode(canonical));
|
|
557
722
|
if (this.aborted) throw new AbortError();
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
authorSignature: getBase64().encode(sigBytes)
|
|
723
|
+
author = {
|
|
724
|
+
[AUTHOR_PUBKEY_FIELD2]: devEdPubHex,
|
|
725
|
+
[AUTHOR_SIGNATURE_FIELD2]: getBase64().encode(sigBytes)
|
|
562
726
|
};
|
|
563
727
|
}
|
|
564
728
|
const result = await this.client.push(
|
|
565
729
|
this.pushPath,
|
|
566
|
-
|
|
567
|
-
this.lastHash
|
|
730
|
+
sealed,
|
|
731
|
+
this.lastHash,
|
|
732
|
+
author
|
|
568
733
|
);
|
|
569
734
|
if (this.aborted) throw new AbortError();
|
|
570
735
|
this.lastHash = result.hash;
|
|
@@ -838,6 +1003,7 @@ function useSyncInit(config) {
|
|
|
838
1003
|
}
|
|
839
1004
|
const client = new StarfishClient({
|
|
840
1005
|
baseUrl: config.serverUrl,
|
|
1006
|
+
namespace: config.namespace,
|
|
841
1007
|
capProvider: config.capProvider,
|
|
842
1008
|
fetch: config.fetch
|
|
843
1009
|
});
|
|
@@ -881,16 +1047,101 @@ function useSyncInit(config) {
|
|
|
881
1047
|
]);
|
|
882
1048
|
return store;
|
|
883
1049
|
}
|
|
1050
|
+
function createStarfishLog(options) {
|
|
1051
|
+
const { cursor } = options;
|
|
1052
|
+
const storeCreator = (rawSet, get) => {
|
|
1053
|
+
const set = rawSet;
|
|
1054
|
+
return {
|
|
1055
|
+
// Seed from the cursor so a warm-started cursor's items show immediately.
|
|
1056
|
+
items: cursor.getItems(),
|
|
1057
|
+
loading: false,
|
|
1058
|
+
online: true,
|
|
1059
|
+
error: null,
|
|
1060
|
+
checkpoint: cursor.getCheckpoint(),
|
|
1061
|
+
pull: async () => {
|
|
1062
|
+
if (get().loading) return [];
|
|
1063
|
+
set({ loading: true, error: null }, false, "log/pull/start");
|
|
1064
|
+
try {
|
|
1065
|
+
const batch = await cursor.pull();
|
|
1066
|
+
set(
|
|
1067
|
+
{ items: cursor.getItems(), checkpoint: cursor.getCheckpoint(), loading: false },
|
|
1068
|
+
false,
|
|
1069
|
+
"log/pull/success"
|
|
1070
|
+
);
|
|
1071
|
+
return batch;
|
|
1072
|
+
} catch (err) {
|
|
1073
|
+
set({ loading: false, error: err instanceof Error ? err.message : String(err) }, false, "log/pull/error");
|
|
1074
|
+
return [];
|
|
1075
|
+
}
|
|
1076
|
+
},
|
|
1077
|
+
setOnline: (online) => {
|
|
1078
|
+
set({ online }, false, "log/setOnline");
|
|
1079
|
+
}
|
|
1080
|
+
};
|
|
1081
|
+
};
|
|
1082
|
+
const withSelector = subscribeWithSelector(storeCreator);
|
|
1083
|
+
return createStore()(
|
|
1084
|
+
options.devtools ? options.devtools(withSelector) : withSelector
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
function deriveLogStatus(state) {
|
|
1088
|
+
if (!state.online) return "offline";
|
|
1089
|
+
if (state.error) return "error";
|
|
1090
|
+
if (state.loading) return "loading";
|
|
1091
|
+
return "idle";
|
|
1092
|
+
}
|
|
1093
|
+
function useStarfishLog(store) {
|
|
1094
|
+
return useStore(store);
|
|
1095
|
+
}
|
|
1096
|
+
function useStarfishLogItems(store, selector) {
|
|
1097
|
+
return useStore(
|
|
1098
|
+
store,
|
|
1099
|
+
(state) => selector ? selector(state.items) : state.items
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
function useLogStatus(store) {
|
|
1103
|
+
return useStore(store, deriveLogStatus);
|
|
1104
|
+
}
|
|
1105
|
+
function subscribeLogStatus(store, callback) {
|
|
1106
|
+
let prev = deriveLogStatus(store.getState());
|
|
1107
|
+
callback(prev);
|
|
1108
|
+
return store.subscribe((state) => {
|
|
1109
|
+
const next = deriveLogStatus(state);
|
|
1110
|
+
if (next !== prev) {
|
|
1111
|
+
prev = next;
|
|
1112
|
+
callback(next);
|
|
1113
|
+
}
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
function useLogConnectivity(store) {
|
|
1117
|
+
useEffect(() => {
|
|
1118
|
+
const handleOnline = () => store.getState().setOnline(true);
|
|
1119
|
+
const handleOffline = () => store.getState().setOnline(false);
|
|
1120
|
+
window.addEventListener("online", handleOnline);
|
|
1121
|
+
window.addEventListener("offline", handleOffline);
|
|
1122
|
+
return () => {
|
|
1123
|
+
window.removeEventListener("online", handleOnline);
|
|
1124
|
+
window.removeEventListener("offline", handleOffline);
|
|
1125
|
+
};
|
|
1126
|
+
}, [store]);
|
|
1127
|
+
}
|
|
884
1128
|
export {
|
|
885
1129
|
aggregateSyncStatus,
|
|
1130
|
+
createStarfishLog,
|
|
886
1131
|
createStarfishStore,
|
|
1132
|
+
deriveLogStatus,
|
|
887
1133
|
deriveSyncStatus,
|
|
1134
|
+
subscribeLogStatus,
|
|
888
1135
|
subscribeSyncStatus,
|
|
889
1136
|
useConnectivity,
|
|
890
1137
|
useCrossTabSync,
|
|
891
1138
|
useLastSynced,
|
|
1139
|
+
useLogConnectivity,
|
|
1140
|
+
useLogStatus,
|
|
892
1141
|
useStarfish,
|
|
893
1142
|
useStarfishData,
|
|
1143
|
+
useStarfishLog,
|
|
1144
|
+
useStarfishLogItems,
|
|
894
1145
|
useSyncInit,
|
|
895
1146
|
useSyncStatus
|
|
896
1147
|
};
|