@drakkar.software/starfish-client 3.0.0-alpha.37 → 3.0.0-alpha.39
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.js +105 -11
- package/dist/bindings/zustand.js.map +2 -2
- package/dist/client.d.ts +47 -2
- package/dist/index.js +105 -11
- package/dist/index.js.map +2 -2
- package/dist/sync.d.ts +40 -7
- package/package.json +2 -2
package/dist/client.d.ts
CHANGED
|
@@ -95,6 +95,25 @@ export interface BatchPullOptions {
|
|
|
95
95
|
* omit the collection from `params` entirely (an unlisted collection reads one
|
|
96
96
|
* auto-filled doc). Results come back under the same name in request order. */
|
|
97
97
|
params?: Record<string, Record<string, string>[]>;
|
|
98
|
+
/**
|
|
99
|
+
* Per-collection append options, index-aligned to `params`. Makes the batch
|
|
100
|
+
* request **append/checkpoint-aware**: each entry returns the bounded tail of
|
|
101
|
+
* that collection's append-only log rather than the full document.
|
|
102
|
+
*
|
|
103
|
+
* Serialized as URL-encoded JSON alongside `params`. Server ignores it for
|
|
104
|
+
* collections that are not append-only (returns `{ error: "append_params_not_supported" }`
|
|
105
|
+
* for those entries). `full` is disallowed in batch (`full_not_allowed` per entry).
|
|
106
|
+
*
|
|
107
|
+
* Example — read the last 5 events for two rooms and the newest item for a third:
|
|
108
|
+
* ```ts
|
|
109
|
+
* await client.batchPull(["events"], {
|
|
110
|
+
* params: { events: [{ room: "a" }, { room: "b" }, { room: "c" }] },
|
|
111
|
+
* appendParams: { events: [{ last: 5 }, { last: 5 }, { last: 1 }] },
|
|
112
|
+
* })
|
|
113
|
+
* ```
|
|
114
|
+
* Each `data[appendField]` in the result is the filtered array for that entry.
|
|
115
|
+
*/
|
|
116
|
+
appendParams?: Record<string, AppendPullOptions[]>;
|
|
98
117
|
}
|
|
99
118
|
/**
|
|
100
119
|
* Low-level HTTP client for the Starfish sync protocol.
|
|
@@ -239,8 +258,7 @@ export declare class StarfishClient {
|
|
|
239
258
|
*
|
|
240
259
|
* For the common "many docs of one collection" case prefer {@link batchPullMany}.
|
|
241
260
|
*
|
|
242
|
-
*
|
|
243
|
-
* `pull(path, { since })` (or `AppendLogCursor`) per collection.
|
|
261
|
+
* Pass `appendParams` per entry for append-only bounded-tail reads (see {@link batchPullManyAppend}).
|
|
244
262
|
*/
|
|
245
263
|
batchPull(collections: string[], opts?: BatchPullOptions): Promise<BatchPullResult>;
|
|
246
264
|
/**
|
|
@@ -251,6 +269,33 @@ export declare class StarfishClient {
|
|
|
251
269
|
* issues no request and returns `[]`.
|
|
252
270
|
*/
|
|
253
271
|
batchPullMany(collection: string, paramsList: Record<string, string>[]): Promise<BatchPullEntry[]>;
|
|
272
|
+
/**
|
|
273
|
+
* Convenience over {@link batchPull} for reading append-only bounded tails from
|
|
274
|
+
* MANY entries of ONE collection in a single round-trip.
|
|
275
|
+
*
|
|
276
|
+
* Each request in `requests` carries optional `params` (path params) and
|
|
277
|
+
* `options` (append bounds: `since`/`last`/`limit`/`appendField`). An empty
|
|
278
|
+
* `requests` issues no request and returns `[]`.
|
|
279
|
+
*
|
|
280
|
+
* Returns an array aligned to `requests` by index. Each element is either:
|
|
281
|
+
* - the filtered array `T[]` extracted from `entry.data[appendField]`, or
|
|
282
|
+
* - `{ error: string }` if the server returned a per-entry error.
|
|
283
|
+
*
|
|
284
|
+
* The `appendField` used for extraction defaults to `"items"` and can be
|
|
285
|
+
* overridden per request via `options.appendField`.
|
|
286
|
+
*
|
|
287
|
+
* The `appendField` option is client-side only (used for result extraction, not sent to the server).
|
|
288
|
+
* It must match the collection's server-configured append field and defaults to `"items"`.
|
|
289
|
+
*
|
|
290
|
+
* Note: `full: true` is not supported in batch and is rejected client-side
|
|
291
|
+
* before the request is sent.
|
|
292
|
+
*/
|
|
293
|
+
batchPullManyAppend<T = unknown>(collection: string, requests: {
|
|
294
|
+
params?: Record<string, string>;
|
|
295
|
+
options: AppendPullOptions;
|
|
296
|
+
}[]): Promise<(T[] | {
|
|
297
|
+
error: string;
|
|
298
|
+
})[]>;
|
|
254
299
|
/**
|
|
255
300
|
* Push synced data to the server.
|
|
256
301
|
* @param path - The push endpoint path (e.g. "/push/users/abc/settings")
|
package/dist/index.js
CHANGED
|
@@ -446,8 +446,7 @@ var StarfishClient = class {
|
|
|
446
446
|
*
|
|
447
447
|
* For the common "many docs of one collection" case prefer {@link batchPullMany}.
|
|
448
448
|
*
|
|
449
|
-
*
|
|
450
|
-
* `pull(path, { since })` (or `AppendLogCursor`) per collection.
|
|
449
|
+
* Pass `appendParams` per entry for append-only bounded-tail reads (see {@link batchPullManyAppend}).
|
|
451
450
|
*/
|
|
452
451
|
async batchPull(collections, opts = {}) {
|
|
453
452
|
const search = new URLSearchParams();
|
|
@@ -455,6 +454,27 @@ var StarfishClient = class {
|
|
|
455
454
|
if (opts.params && Object.keys(opts.params).length > 0) {
|
|
456
455
|
search.set("params", JSON.stringify(opts.params));
|
|
457
456
|
}
|
|
457
|
+
if (opts.appendParams && Object.keys(opts.appendParams).length > 0) {
|
|
458
|
+
for (const [col, optsArr] of Object.entries(opts.appendParams)) {
|
|
459
|
+
for (const ap of optsArr) {
|
|
460
|
+
if (ap.full) {
|
|
461
|
+
throw new Error(
|
|
462
|
+
`batchPull: appendParams["${col}"] contains full:true \u2014 full is not supported in batch pull`
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
if (ap.since != null && (!Number.isInteger(ap.since) || ap.since < 0)) {
|
|
466
|
+
throw new Error(`batchPull: appendParams["${col}"].since must be a non-negative integer`);
|
|
467
|
+
}
|
|
468
|
+
if (ap.last != null && (!Number.isInteger(ap.last) || ap.last < 0)) {
|
|
469
|
+
throw new Error(`batchPull: appendParams["${col}"].last must be a non-negative integer`);
|
|
470
|
+
}
|
|
471
|
+
if (ap.limit != null && (!Number.isInteger(ap.limit) || ap.limit < 0)) {
|
|
472
|
+
throw new Error(`batchPull: appendParams["${col}"].limit must be a non-negative integer`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
search.set("appendParams", JSON.stringify(opts.appendParams));
|
|
477
|
+
}
|
|
458
478
|
const pathAndQuery = `${this.applyNamespace("/batch/pull")}?${search.toString()}`;
|
|
459
479
|
const url = `${this.baseUrl}${pathAndQuery}`;
|
|
460
480
|
const authHeaders = await this.buildAuthHeaders("GET", pathAndQuery, void 0);
|
|
@@ -479,6 +499,44 @@ var StarfishClient = class {
|
|
|
479
499
|
const res = await this.batchPull([collection], { params: { [collection]: paramsList } });
|
|
480
500
|
return res.collections[collection] ?? [];
|
|
481
501
|
}
|
|
502
|
+
/**
|
|
503
|
+
* Convenience over {@link batchPull} for reading append-only bounded tails from
|
|
504
|
+
* MANY entries of ONE collection in a single round-trip.
|
|
505
|
+
*
|
|
506
|
+
* Each request in `requests` carries optional `params` (path params) and
|
|
507
|
+
* `options` (append bounds: `since`/`last`/`limit`/`appendField`). An empty
|
|
508
|
+
* `requests` issues no request and returns `[]`.
|
|
509
|
+
*
|
|
510
|
+
* Returns an array aligned to `requests` by index. Each element is either:
|
|
511
|
+
* - the filtered array `T[]` extracted from `entry.data[appendField]`, or
|
|
512
|
+
* - `{ error: string }` if the server returned a per-entry error.
|
|
513
|
+
*
|
|
514
|
+
* The `appendField` used for extraction defaults to `"items"` and can be
|
|
515
|
+
* overridden per request via `options.appendField`.
|
|
516
|
+
*
|
|
517
|
+
* The `appendField` option is client-side only (used for result extraction, not sent to the server).
|
|
518
|
+
* It must match the collection's server-configured append field and defaults to `"items"`.
|
|
519
|
+
*
|
|
520
|
+
* Note: `full: true` is not supported in batch and is rejected client-side
|
|
521
|
+
* before the request is sent.
|
|
522
|
+
*/
|
|
523
|
+
async batchPullManyAppend(collection, requests) {
|
|
524
|
+
if (requests.length === 0) return [];
|
|
525
|
+
const paramsList = requests.map((r) => r.params ?? {});
|
|
526
|
+
const appendParamsList = requests.map(({ options: { appendField: _af, ...wireOpts } }) => wireOpts);
|
|
527
|
+
const res = await this.batchPull([collection], {
|
|
528
|
+
params: { [collection]: paramsList },
|
|
529
|
+
appendParams: { [collection]: appendParamsList }
|
|
530
|
+
});
|
|
531
|
+
const entries = res.collections[collection] ?? [];
|
|
532
|
+
return entries.map((entry, i) => {
|
|
533
|
+
if (entry.error) return { error: entry.error };
|
|
534
|
+
const appendField = requests[i]?.options.appendField ?? APPEND_DEFAULT_FIELD;
|
|
535
|
+
const data = entry.data;
|
|
536
|
+
const items = data?.[appendField];
|
|
537
|
+
return Array.isArray(items) ? items : [];
|
|
538
|
+
});
|
|
539
|
+
}
|
|
482
540
|
/**
|
|
483
541
|
* Push synced data to the server.
|
|
484
542
|
* @param path - The push endpoint path (e.g. "/push/users/abc/settings")
|
|
@@ -723,6 +781,8 @@ var SyncManager = class {
|
|
|
723
781
|
localData = {};
|
|
724
782
|
aborted = false;
|
|
725
783
|
lastFromCache = false;
|
|
784
|
+
/** True once {@link seedFromCache} has successfully seeded localData from the cache. */
|
|
785
|
+
seeded = false;
|
|
726
786
|
constructor(options) {
|
|
727
787
|
this.client = options.client;
|
|
728
788
|
this.pullPath = options.pullPath;
|
|
@@ -744,6 +804,24 @@ var SyncManager = class {
|
|
|
744
804
|
getData() {
|
|
745
805
|
return { ...this.localData };
|
|
746
806
|
}
|
|
807
|
+
/**
|
|
808
|
+
* Returns true when `pull()` / `ingest()` should merge against the current
|
|
809
|
+
* `localData` rather than replace it wholesale.
|
|
810
|
+
*
|
|
811
|
+
* Two situations establish a merge baseline:
|
|
812
|
+
* - A successful prior pull/ingest advanced `lastCheckpoint` beyond 0 (the
|
|
813
|
+
* normal steady-state case, unchanged since alpha.36).
|
|
814
|
+
* - A cache seed painted `localData` via {@link seedFromCache} AND the store
|
|
815
|
+
* uses a custom conflict resolver (i.e. NOT the default `deepMerge`). For a
|
|
816
|
+
* union/custom resolver the seeded snapshot is a real baseline that must not
|
|
817
|
+
* be clobbered by a short first live response (a cache-fallback on 429/5xx
|
|
818
|
+
* or a momentarily-short concurrent server snapshot). For the default
|
|
819
|
+
* `deepMerge` resolver we keep the pre-fix wholesale-replace behaviour so
|
|
820
|
+
* non-union stores are byte-identical to alpha.36.
|
|
821
|
+
*/
|
|
822
|
+
hasMergeBaseline() {
|
|
823
|
+
return this.lastCheckpoint > 0 || this.seeded && this.onConflict !== deepMerge;
|
|
824
|
+
}
|
|
747
825
|
/**
|
|
748
826
|
* Merge a remote snapshot with local (optimistic) data using this manager's
|
|
749
827
|
* conflict resolver — the same resolver the push-conflict path uses. A plain
|
|
@@ -778,7 +856,19 @@ var SyncManager = class {
|
|
|
778
856
|
* WITHOUT touching the network, decrypting in memory for E2E collections.
|
|
779
857
|
* Returns whether anything was seeded (false on a miss, an expired entry, or
|
|
780
858
|
* a decrypt failure — e.g. keyring skew). Call once on store creation before
|
|
781
|
-
* the initial live {@link pull}
|
|
859
|
+
* the initial live {@link pull}.
|
|
860
|
+
*
|
|
861
|
+
* `lastCheckpoint` is intentionally left at 0 so the first live pull sends a
|
|
862
|
+
* full (re)sync request to the server, not a delta. However, for stores with
|
|
863
|
+
* a custom conflict resolver (e.g. `createUnionMerge`) the seeded snapshot is
|
|
864
|
+
* treated as a merge baseline: {@link hasMergeBaseline} returns true, so the
|
|
865
|
+
* first pull/ingest merges against the seed rather than replacing it wholesale.
|
|
866
|
+
* This closes the bootstrap window where a short first-pull response (a cache-
|
|
867
|
+
* fallback on 429/5xx or a momentarily-short concurrent snapshot) would
|
|
868
|
+
* silently drop items the resolver was configured to preserve. For the default
|
|
869
|
+
* `deepMerge` resolver the first pull still takes the snapshot wholesale —
|
|
870
|
+
* behaviour is byte-identical to alpha.36.
|
|
871
|
+
*
|
|
782
872
|
* Requires the client to have been built with a `cache`.
|
|
783
873
|
*/
|
|
784
874
|
async seedFromCache() {
|
|
@@ -794,6 +884,7 @@ var SyncManager = class {
|
|
|
794
884
|
if (this.aborted) return false;
|
|
795
885
|
this.localData = data;
|
|
796
886
|
this.lastHash = cached.hash;
|
|
887
|
+
this.seeded = true;
|
|
797
888
|
this.lastFromCache = true;
|
|
798
889
|
return true;
|
|
799
890
|
}
|
|
@@ -807,12 +898,15 @@ var SyncManager = class {
|
|
|
807
898
|
* {@link StarfishClientOptions.onRevalidated}) into the store.
|
|
808
899
|
*
|
|
809
900
|
* Like {@link pull}, `ingest` conflict-merges the snapshot against the
|
|
810
|
-
* established baseline via `this.onConflict` when a
|
|
811
|
-
* union-merge store does not lose array
|
|
812
|
-
* (e.g. a stale cache-fallback on 429/5xx)
|
|
813
|
-
*
|
|
814
|
-
*
|
|
815
|
-
*
|
|
901
|
+
* established baseline via `this.onConflict` when a merge baseline exists
|
|
902
|
+
* ({@link hasMergeBaseline}) — so a union-merge store does not lose array
|
|
903
|
+
* items when a revalidation result (e.g. a stale cache-fallback on 429/5xx)
|
|
904
|
+
* is a shorter snapshot. The baseline is established by either a prior
|
|
905
|
+
* pull/ingest that advanced `lastCheckpoint`, or by a successful
|
|
906
|
+
* {@link seedFromCache} for a store with a custom resolver. The first ingest
|
|
907
|
+
* without such a baseline takes the snapshot wholesale (default `deepMerge`
|
|
908
|
+
* stores are byte-identical to alpha.36). Sets `lastFromCache = false` (a
|
|
909
|
+
* revalidation is a live response) so the binding can clear its `stale` flag.
|
|
816
910
|
*
|
|
817
911
|
* **Staleness guard**: if a `push()` advanced `lastCheckpoint` between the
|
|
818
912
|
* time the revalidation request was sent and the time it resolves, the
|
|
@@ -831,7 +925,7 @@ var SyncManager = class {
|
|
|
831
925
|
} else {
|
|
832
926
|
incoming = result.data;
|
|
833
927
|
}
|
|
834
|
-
this.localData = this.
|
|
928
|
+
this.localData = this.hasMergeBaseline() ? this.onConflict(this.localData, incoming) : incoming;
|
|
835
929
|
this.lastHash = result.hash;
|
|
836
930
|
this.lastCheckpoint = result.timestamp;
|
|
837
931
|
this.lastFromCache = false;
|
|
@@ -851,7 +945,7 @@ var SyncManager = class {
|
|
|
851
945
|
} else {
|
|
852
946
|
incoming = result.data;
|
|
853
947
|
}
|
|
854
|
-
this.localData = this.
|
|
948
|
+
this.localData = this.hasMergeBaseline() ? this.onConflict(this.localData, incoming) : incoming;
|
|
855
949
|
result.data = this.localData;
|
|
856
950
|
this.lastHash = result.hash;
|
|
857
951
|
this.lastCheckpoint = result.timestamp;
|