@drakkar.software/starfish-client 3.0.0-alpha.1 → 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 -47
- 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 -49
- 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 +28 -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,28 +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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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 };
|
|
328
392
|
}
|
|
329
393
|
async pull(path, checkpointOrOptions) {
|
|
330
|
-
let pathAndQuery = path;
|
|
394
|
+
let pathAndQuery = this.applyNamespace(path);
|
|
331
395
|
let appendField;
|
|
332
396
|
if (typeof checkpointOrOptions === "number") {
|
|
333
397
|
if (checkpointOrOptions) pathAndQuery += `?checkpoint=${checkpointOrOptions}`;
|
|
@@ -359,7 +423,7 @@ var StarfishClient = class {
|
|
|
359
423
|
const authHeaders = await this.buildAuthHeaders("GET", pathAndQuery, void 0);
|
|
360
424
|
const res = await this.fetch(url, {
|
|
361
425
|
method: "GET",
|
|
362
|
-
headers: {
|
|
426
|
+
headers: { [HEADER_ACCEPT]: "application/json", ...authHeaders }
|
|
363
427
|
});
|
|
364
428
|
if (!res.ok) {
|
|
365
429
|
throw new StarfishHttpError(res.status, await res.text());
|
|
@@ -371,27 +435,62 @@ var StarfishClient = class {
|
|
|
371
435
|
}
|
|
372
436
|
return result;
|
|
373
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
|
+
}
|
|
374
467
|
/**
|
|
375
468
|
* Push synced data to the server.
|
|
376
469
|
* @param path - The push endpoint path (e.g. "/push/users/abc/settings")
|
|
377
470
|
* @param data - The full document data to push
|
|
378
471
|
* @param baseHash - Hash of the document this push is based on (null for first push)
|
|
379
472
|
*
|
|
380
|
-
* v3 author
|
|
381
|
-
*
|
|
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.
|
|
382
476
|
* @throws {ConflictError} if the server detects a hash mismatch (409)
|
|
383
477
|
*/
|
|
384
|
-
async push(path, data, baseHash) {
|
|
478
|
+
async push(path, data, baseHash, author) {
|
|
385
479
|
const body = JSON.stringify({
|
|
386
|
-
data,
|
|
387
|
-
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
|
+
}
|
|
388
486
|
});
|
|
389
|
-
const
|
|
390
|
-
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}`, {
|
|
391
490
|
method: "POST",
|
|
392
491
|
headers: {
|
|
393
|
-
|
|
394
|
-
|
|
492
|
+
[HEADER_CONTENT_TYPE]: "application/json",
|
|
493
|
+
[HEADER_ACCEPT]: "application/json",
|
|
395
494
|
...authHeaders
|
|
396
495
|
},
|
|
397
496
|
body
|
|
@@ -404,21 +503,77 @@ var StarfishClient = class {
|
|
|
404
503
|
}
|
|
405
504
|
return res.json();
|
|
406
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
|
+
}
|
|
407
561
|
/**
|
|
408
562
|
* Pull binary data from a blob collection.
|
|
409
563
|
* Returns raw bytes with the content hash from the ETag header.
|
|
410
564
|
*/
|
|
411
565
|
async pullBlob(path) {
|
|
412
|
-
const
|
|
413
|
-
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}`, {
|
|
414
569
|
method: "GET",
|
|
415
|
-
headers: {
|
|
570
|
+
headers: { [HEADER_ACCEPT]: "*/*", ...authHeaders }
|
|
416
571
|
});
|
|
417
572
|
if (!res.ok) {
|
|
418
573
|
throw new StarfishHttpError(res.status, await res.text());
|
|
419
574
|
}
|
|
420
575
|
const etag = res.headers.get("ETag")?.replace(/"/g, "") ?? null;
|
|
421
|
-
const contentType = res.headers.get(
|
|
576
|
+
const contentType = res.headers.get(HEADER_CONTENT_TYPE) ?? "application/octet-stream";
|
|
422
577
|
const data = await res.arrayBuffer();
|
|
423
578
|
return { data, hash: etag, contentType };
|
|
424
579
|
}
|
|
@@ -427,12 +582,13 @@ var StarfishClient = class {
|
|
|
427
582
|
* Binary collections use last-write-wins (no conflict detection).
|
|
428
583
|
*/
|
|
429
584
|
async pushBlob(path, data, contentType) {
|
|
430
|
-
const
|
|
431
|
-
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}`, {
|
|
432
588
|
method: "POST",
|
|
433
589
|
headers: {
|
|
434
|
-
|
|
435
|
-
|
|
590
|
+
[HEADER_CONTENT_TYPE]: contentType,
|
|
591
|
+
[HEADER_ACCEPT]: "application/json",
|
|
436
592
|
...authHeaders
|
|
437
593
|
},
|
|
438
594
|
body: data
|
|
@@ -445,7 +601,13 @@ var StarfishClient = class {
|
|
|
445
601
|
};
|
|
446
602
|
|
|
447
603
|
// src/sync.ts
|
|
448
|
-
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";
|
|
449
611
|
|
|
450
612
|
// src/validate.ts
|
|
451
613
|
var ValidationError = class extends Error {
|
|
@@ -550,23 +712,24 @@ var SyncManager = class {
|
|
|
550
712
|
try {
|
|
551
713
|
const sealed = this.encryptor ? await this.encryptor.encrypt(pendingData) : pendingData;
|
|
552
714
|
if (this.aborted) throw new AbortError();
|
|
553
|
-
let
|
|
715
|
+
let author;
|
|
554
716
|
if (this.signer) {
|
|
555
717
|
const { devEdPubHex, sign } = await this.signer.getSigner();
|
|
556
718
|
if (this.aborted) throw new AbortError();
|
|
557
|
-
const
|
|
719
|
+
const documentKey = stripPushPrefix(this.pushPath);
|
|
720
|
+
const canonical = docAuthorCanonicalInput(documentKey, sealed);
|
|
558
721
|
const sigBytes = await sign(new TextEncoder().encode(canonical));
|
|
559
722
|
if (this.aborted) throw new AbortError();
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
authorSignature: getBase64().encode(sigBytes)
|
|
723
|
+
author = {
|
|
724
|
+
[AUTHOR_PUBKEY_FIELD2]: devEdPubHex,
|
|
725
|
+
[AUTHOR_SIGNATURE_FIELD2]: getBase64().encode(sigBytes)
|
|
564
726
|
};
|
|
565
727
|
}
|
|
566
728
|
const result = await this.client.push(
|
|
567
729
|
this.pushPath,
|
|
568
|
-
|
|
569
|
-
this.lastHash
|
|
730
|
+
sealed,
|
|
731
|
+
this.lastHash,
|
|
732
|
+
author
|
|
570
733
|
);
|
|
571
734
|
if (this.aborted) throw new AbortError();
|
|
572
735
|
this.lastHash = result.hash;
|
|
@@ -840,6 +1003,7 @@ function useSyncInit(config) {
|
|
|
840
1003
|
}
|
|
841
1004
|
const client = new StarfishClient({
|
|
842
1005
|
baseUrl: config.serverUrl,
|
|
1006
|
+
namespace: config.namespace,
|
|
843
1007
|
capProvider: config.capProvider,
|
|
844
1008
|
fetch: config.fetch
|
|
845
1009
|
});
|
|
@@ -883,16 +1047,101 @@ function useSyncInit(config) {
|
|
|
883
1047
|
]);
|
|
884
1048
|
return store;
|
|
885
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
|
+
}
|
|
886
1128
|
export {
|
|
887
1129
|
aggregateSyncStatus,
|
|
1130
|
+
createStarfishLog,
|
|
888
1131
|
createStarfishStore,
|
|
1132
|
+
deriveLogStatus,
|
|
889
1133
|
deriveSyncStatus,
|
|
1134
|
+
subscribeLogStatus,
|
|
890
1135
|
subscribeSyncStatus,
|
|
891
1136
|
useConnectivity,
|
|
892
1137
|
useCrossTabSync,
|
|
893
1138
|
useLastSynced,
|
|
1139
|
+
useLogConnectivity,
|
|
1140
|
+
useLogStatus,
|
|
894
1141
|
useStarfish,
|
|
895
1142
|
useStarfishData,
|
|
1143
|
+
useStarfishLog,
|
|
1144
|
+
useStarfishLogItems,
|
|
896
1145
|
useSyncInit,
|
|
897
1146
|
useSyncStatus
|
|
898
1147
|
};
|