@drakkar.software/starfish-client 3.0.0-alpha.29 → 3.0.0-alpha.31
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/dist/bindings/zustand.d.ts +67 -1
- package/dist/bindings/zustand.js +140 -1
- package/dist/bindings/zustand.js.map +2 -2
- package/dist/client.d.ts +30 -0
- package/dist/events.d.ts +150 -0
- package/dist/events.js +116 -0
- package/dist/events.js.map +7 -0
- package/dist/fetch.d.ts +27 -0
- package/dist/fetch.js +32 -0
- package/dist/fetch.js.map +2 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.js +105 -0
- package/dist/index.js.map +3 -3
- package/dist/kv-cache.d.ts +63 -0
- package/dist/types.d.ts +14 -0
- package/package.json +6 -2
|
@@ -4,7 +4,7 @@ import type { DevtoolsOptions } from "zustand/middleware";
|
|
|
4
4
|
import type { Encryptor } from "@drakkar.software/starfish-protocol";
|
|
5
5
|
import { SyncManager } from "../sync.js";
|
|
6
6
|
import { AppendLogCursor, type AppendElement } from "../append-log.js";
|
|
7
|
-
import type { StarfishCapProvider, ConflictResolver, PullCache } from "../types.js";
|
|
7
|
+
import type { StarfishClientOptions, StarfishCapProvider, ConflictResolver, PullCache } from "../types.js";
|
|
8
8
|
import type { SyncLogger } from "../logger.js";
|
|
9
9
|
import type { Validator } from "../validate.js";
|
|
10
10
|
export interface StarfishState {
|
|
@@ -163,6 +163,19 @@ export interface SyncInitConfig {
|
|
|
163
163
|
cache?: PullCache;
|
|
164
164
|
/** Max age (ms) for {@link cache} entries; see {@link StarfishClientOptions.cacheMaxAgeMs}. */
|
|
165
165
|
cacheMaxAgeMs?: number;
|
|
166
|
+
/**
|
|
167
|
+
* HTTP status codes for which pulls fall back to the last-cached snapshot rather than
|
|
168
|
+
* throwing — stale-while-revalidate for transient server failures.
|
|
169
|
+
* See {@link StarfishClientOptions.cacheFallbackStatuses}.
|
|
170
|
+
* Recommended set for offline-first apps: `[429, 500, 502, 503, 504]`.
|
|
171
|
+
*/
|
|
172
|
+
cacheFallbackStatuses?: number[];
|
|
173
|
+
/**
|
|
174
|
+
* Called after a background revalidation following a {@link cacheFallbackStatuses} hit:
|
|
175
|
+
* the server returned a live response and the fresh snapshot has been written through.
|
|
176
|
+
* See {@link StarfishClientOptions.onRevalidated}.
|
|
177
|
+
*/
|
|
178
|
+
onRevalidated?: StarfishClientOptions["onRevalidated"];
|
|
166
179
|
logger?: SyncLogger;
|
|
167
180
|
validate?: Validator;
|
|
168
181
|
}
|
|
@@ -176,6 +189,59 @@ export interface SyncInitConfig {
|
|
|
176
189
|
* Pass `null` to disable sync (returns `null`).
|
|
177
190
|
*/
|
|
178
191
|
export declare function useSyncInit(config: SyncInitConfig | null): StoreApi<StarfishStore> | null;
|
|
192
|
+
/**
|
|
193
|
+
* Config for a shared sync store — identical to {@link SyncInitConfig} EXCEPT:
|
|
194
|
+
* - `onData` is omitted: it is not safe to fan out a single `onRemoteUpdate`
|
|
195
|
+
* callback to multiple independent subscribers. Consumers should instead
|
|
196
|
+
* subscribe to the returned store via `store.subscribe(...)`.
|
|
197
|
+
* - `storeName` is REQUIRED: it is the registry key, not an optional label.
|
|
198
|
+
*/
|
|
199
|
+
export type SharedSyncConfig = Omit<SyncInitConfig, "onData" | "storeName"> & {
|
|
200
|
+
/** Registry key AND store persistence label. Required; there is no default. */
|
|
201
|
+
storeName: string;
|
|
202
|
+
};
|
|
203
|
+
/**
|
|
204
|
+
* Return (or create) the shared zustand store for `config.storeName`.
|
|
205
|
+
*
|
|
206
|
+
* On the **first** acquire, constructs `StarfishClient` → `SyncManager` → store
|
|
207
|
+
* (forwarding all config fields, including `cacheFallbackStatuses` and `onRevalidated`
|
|
208
|
+
* for native stale-while-revalidate), then fires `seed().finally(pull())`. On every
|
|
209
|
+
* subsequent acquire of the same `storeName`, the existing store is returned — **no**
|
|
210
|
+
* new pull fires.
|
|
211
|
+
*
|
|
212
|
+
* Always pair with {@link releaseSyncStore}. Call {@link clearSyncStoreRegistry}
|
|
213
|
+
* on account switch / sign-out.
|
|
214
|
+
*/
|
|
215
|
+
export declare function acquireSyncStore(config: SharedSyncConfig): StoreApi<StarfishStore>;
|
|
216
|
+
/**
|
|
217
|
+
* Release a previously acquired store. Decrements the refCount; on 0 the entry is
|
|
218
|
+
* evicted — the store, client, and sync manager are dropped and GC'd (mirrors
|
|
219
|
+
* `useSyncInit`'s own teardown, which simply drops the local store reference).
|
|
220
|
+
*/
|
|
221
|
+
export declare function releaseSyncStore(storeName: string): void;
|
|
222
|
+
/**
|
|
223
|
+
* Clear all registry entries.
|
|
224
|
+
*
|
|
225
|
+
* Call on account switch or sign-out alongside any other per-session cache clears.
|
|
226
|
+
* An identity guard inside {@link acquireSyncStore} prevents any in-flight pull from
|
|
227
|
+
* firing against the old session's cap after this is called.
|
|
228
|
+
*/
|
|
229
|
+
export declare function clearSyncStoreRegistry(): void;
|
|
230
|
+
/**
|
|
231
|
+
* React hook that returns (or creates) the shared zustand store for
|
|
232
|
+
* `config.storeName` — a drop-in replacement for {@link useSyncInit} when the
|
|
233
|
+
* same logical document is consumed from multiple components.
|
|
234
|
+
*
|
|
235
|
+
* **Key design decision — effect deps include only `storeName`:** config identity
|
|
236
|
+
* churn (fresh `capProvider`/`encryptor` refs per render) is intentionally ignored.
|
|
237
|
+
* For a given `(user, space)` the cap and keyring are functionally equivalent across
|
|
238
|
+
* refs, and no `onData` fan-out is needed, so the shared store never needs to rebuild
|
|
239
|
+
* on churn. The `configRef` pattern ensures the latest config values are captured at
|
|
240
|
+
* acquire-time without re-running the effect.
|
|
241
|
+
*
|
|
242
|
+
* Pass `null` to disable sync (returns `null`).
|
|
243
|
+
*/
|
|
244
|
+
export declare function useSharedSyncStore(config: SharedSyncConfig | null): StoreApi<StarfishStore> | null;
|
|
179
245
|
export interface StarfishLogState {
|
|
180
246
|
/** The full accumulated log, newest appended last. */
|
|
181
247
|
items: AppendElement[];
|
package/dist/bindings/zustand.js
CHANGED
|
@@ -263,6 +263,13 @@ var StarfishHttpError = class extends Error {
|
|
|
263
263
|
this.name = "StarfishHttpError";
|
|
264
264
|
}
|
|
265
265
|
};
|
|
266
|
+
var AppendHttpError = class extends Error {
|
|
267
|
+
constructor(status, message) {
|
|
268
|
+
super(message);
|
|
269
|
+
this.status = status;
|
|
270
|
+
this.name = "AppendHttpError";
|
|
271
|
+
}
|
|
272
|
+
};
|
|
266
273
|
|
|
267
274
|
// src/fetch.ts
|
|
268
275
|
function parseRetryAfterMs(header, opts) {
|
|
@@ -762,6 +769,62 @@ var StarfishClient = class {
|
|
|
762
769
|
}
|
|
763
770
|
return res.json();
|
|
764
771
|
}
|
|
772
|
+
/**
|
|
773
|
+
* Append one element to a **public-write** append-only collection with an
|
|
774
|
+
* Ed25519 author proof but **no cap `Authorization` header**.
|
|
775
|
+
*
|
|
776
|
+
* Unlike {@link append}, which always attaches a cap-signed `Authorization`
|
|
777
|
+
* header from the configured `capProvider`, this method signs only the
|
|
778
|
+
* append-author proof (binding the element to the writer's Ed25519 key) and
|
|
779
|
+
* sends the request without authentication headers. This is required for
|
|
780
|
+
* collections with `writeRoles: ["public"]` — the server's cap-scope check
|
|
781
|
+
* would reject a request carrying a cap whose scope does not cover the path.
|
|
782
|
+
*
|
|
783
|
+
* Typical use-case: writing a sealed invitation to another user's
|
|
784
|
+
* public-write inbox collection without needing a cap scoped to the
|
|
785
|
+
* recipient's namespace. The author proof is optional on the server side
|
|
786
|
+
* (`requireAuthorSignature: false` for a public inbox), but signing anyway
|
|
787
|
+
* binds the stored element to the sender's Ed25519 key for verification in
|
|
788
|
+
* the receive path.
|
|
789
|
+
*
|
|
790
|
+
* The element is sent as `{ data, authorPubkey, authorSignature }`.
|
|
791
|
+
*
|
|
792
|
+
* @param path The push path, e.g. `/push/inbox/{userId}/{shard}`.
|
|
793
|
+
* @param element The JSON element to append.
|
|
794
|
+
* @param signer The sender's Ed25519 keypair (signs the author proof).
|
|
795
|
+
*
|
|
796
|
+
* @throws {AppendHttpError} on a non-2xx response.
|
|
797
|
+
*/
|
|
798
|
+
async appendAnonymous(path, element, signer) {
|
|
799
|
+
const sendPath = this.applyNamespace(path);
|
|
800
|
+
const documentKey = stripPushPrefix(path);
|
|
801
|
+
const { authorPubkey, authorSignature } = signAppendAuthor(
|
|
802
|
+
documentKey,
|
|
803
|
+
element,
|
|
804
|
+
signer.edPubHex,
|
|
805
|
+
signer.edPrivHex
|
|
806
|
+
);
|
|
807
|
+
const body = JSON.stringify({
|
|
808
|
+
[DATA_FIELD]: element,
|
|
809
|
+
[AUTHOR_PUBKEY_FIELD]: authorPubkey,
|
|
810
|
+
[AUTHOR_SIGNATURE_FIELD]: authorSignature
|
|
811
|
+
});
|
|
812
|
+
const res = await this.fetch(`${this.baseUrl}${sendPath}`, {
|
|
813
|
+
method: "POST",
|
|
814
|
+
headers: {
|
|
815
|
+
[HEADER_CONTENT_TYPE]: "application/json",
|
|
816
|
+
[HEADER_ACCEPT]: "application/json"
|
|
817
|
+
},
|
|
818
|
+
body
|
|
819
|
+
});
|
|
820
|
+
if (!res.ok) {
|
|
821
|
+
const detail = await res.text().catch(() => "");
|
|
822
|
+
throw new AppendHttpError(
|
|
823
|
+
res.status,
|
|
824
|
+
`anonymous append failed: HTTP ${res.status} ${detail}`.trim()
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
765
828
|
/**
|
|
766
829
|
* Pull binary data from a blob collection.
|
|
767
830
|
* Returns raw bytes with the content hash from the ETag header.
|
|
@@ -1309,7 +1372,9 @@ function useSyncInit(config) {
|
|
|
1309
1372
|
capProvider: config.capProvider,
|
|
1310
1373
|
fetch: config.fetch,
|
|
1311
1374
|
cache: config.cache,
|
|
1312
|
-
cacheMaxAgeMs: config.cacheMaxAgeMs
|
|
1375
|
+
cacheMaxAgeMs: config.cacheMaxAgeMs,
|
|
1376
|
+
cacheFallbackStatuses: config.cacheFallbackStatuses,
|
|
1377
|
+
onRevalidated: config.onRevalidated
|
|
1313
1378
|
});
|
|
1314
1379
|
const syncManager = new SyncManager({
|
|
1315
1380
|
client,
|
|
@@ -1353,6 +1418,76 @@ function useSyncInit(config) {
|
|
|
1353
1418
|
]);
|
|
1354
1419
|
return store;
|
|
1355
1420
|
}
|
|
1421
|
+
var _syncStoreRegistry = /* @__PURE__ */ new Map();
|
|
1422
|
+
function acquireSyncStore(config) {
|
|
1423
|
+
const existing = _syncStoreRegistry.get(config.storeName);
|
|
1424
|
+
if (existing) {
|
|
1425
|
+
existing.refCount += 1;
|
|
1426
|
+
return existing.store;
|
|
1427
|
+
}
|
|
1428
|
+
const client = new StarfishClient({
|
|
1429
|
+
baseUrl: config.serverUrl,
|
|
1430
|
+
namespace: config.namespace,
|
|
1431
|
+
capProvider: config.capProvider,
|
|
1432
|
+
fetch: config.fetch,
|
|
1433
|
+
cache: config.cache,
|
|
1434
|
+
cacheMaxAgeMs: config.cacheMaxAgeMs,
|
|
1435
|
+
cacheFallbackStatuses: config.cacheFallbackStatuses,
|
|
1436
|
+
onRevalidated: config.onRevalidated
|
|
1437
|
+
});
|
|
1438
|
+
const syncManager = new SyncManager({
|
|
1439
|
+
client,
|
|
1440
|
+
pullPath: config.pullPath,
|
|
1441
|
+
pushPath: config.pushPath,
|
|
1442
|
+
encryptor: config.encryptor,
|
|
1443
|
+
onConflict: config.onConflict,
|
|
1444
|
+
logger: config.logger,
|
|
1445
|
+
validate: config.validate
|
|
1446
|
+
});
|
|
1447
|
+
const store = createStarfishStore({
|
|
1448
|
+
name: config.storeName,
|
|
1449
|
+
syncManager,
|
|
1450
|
+
storage: config.storage
|
|
1451
|
+
// No onRemoteUpdate: consumers subscribe via store.subscribe() — see module comment.
|
|
1452
|
+
});
|
|
1453
|
+
const entry = { store, refCount: 1 };
|
|
1454
|
+
_syncStoreRegistry.set(config.storeName, entry);
|
|
1455
|
+
store.getState().seed().finally(() => {
|
|
1456
|
+
if (_syncStoreRegistry.get(config.storeName) === entry) {
|
|
1457
|
+
store.getState().pull().catch(() => {
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
});
|
|
1461
|
+
return store;
|
|
1462
|
+
}
|
|
1463
|
+
function releaseSyncStore(storeName) {
|
|
1464
|
+
const entry = _syncStoreRegistry.get(storeName);
|
|
1465
|
+
if (!entry) return;
|
|
1466
|
+
entry.refCount -= 1;
|
|
1467
|
+
if (entry.refCount <= 0) _syncStoreRegistry.delete(storeName);
|
|
1468
|
+
}
|
|
1469
|
+
function clearSyncStoreRegistry() {
|
|
1470
|
+
_syncStoreRegistry.clear();
|
|
1471
|
+
}
|
|
1472
|
+
function useSharedSyncStore(config) {
|
|
1473
|
+
const [store, setStore] = useState(null);
|
|
1474
|
+
const storeName = config?.storeName ?? null;
|
|
1475
|
+
const configRef = useRef(config);
|
|
1476
|
+
configRef.current = config;
|
|
1477
|
+
useEffect(() => {
|
|
1478
|
+
if (!storeName) {
|
|
1479
|
+
setStore(null);
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
const acquired = acquireSyncStore(configRef.current);
|
|
1483
|
+
setStore(acquired);
|
|
1484
|
+
return () => {
|
|
1485
|
+
releaseSyncStore(storeName);
|
|
1486
|
+
setStore(null);
|
|
1487
|
+
};
|
|
1488
|
+
}, [storeName]);
|
|
1489
|
+
return store;
|
|
1490
|
+
}
|
|
1356
1491
|
function createStarfishLog(options) {
|
|
1357
1492
|
const { cursor } = options;
|
|
1358
1493
|
const storeCreator = (rawSet, get) => {
|
|
@@ -1432,11 +1567,14 @@ function useLogConnectivity(store) {
|
|
|
1432
1567
|
}, [store]);
|
|
1433
1568
|
}
|
|
1434
1569
|
export {
|
|
1570
|
+
acquireSyncStore,
|
|
1435
1571
|
aggregateSyncStatus,
|
|
1572
|
+
clearSyncStoreRegistry,
|
|
1436
1573
|
createStarfishLog,
|
|
1437
1574
|
createStarfishStore,
|
|
1438
1575
|
deriveLogStatus,
|
|
1439
1576
|
deriveSyncStatus,
|
|
1577
|
+
releaseSyncStore,
|
|
1440
1578
|
subscribeLogStatus,
|
|
1441
1579
|
subscribeSyncStatus,
|
|
1442
1580
|
useConnectivity,
|
|
@@ -1444,6 +1582,7 @@ export {
|
|
|
1444
1582
|
useLastSynced,
|
|
1445
1583
|
useLogConnectivity,
|
|
1446
1584
|
useLogStatus,
|
|
1585
|
+
useSharedSyncStore,
|
|
1447
1586
|
useStarfish,
|
|
1448
1587
|
useStarfishData,
|
|
1449
1588
|
useStarfishLog,
|