@drakkar.software/starfish-client 3.0.0-alpha.10 → 3.0.0-alpha.12
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/append-log.d.ts +1 -3
- package/dist/append-log.js +267 -0
- package/dist/background-sync.js +29 -0
- package/dist/bindings/suspense.js +49 -0
- package/dist/bindings/zustand.js +28 -20
- package/dist/bindings/zustand.js.map +2 -2
- package/dist/client.d.ts +30 -13
- package/dist/client.js +391 -0
- package/dist/config.js +18 -0
- package/dist/debounced-sync.js +120 -0
- package/dist/dedup.js +35 -0
- package/dist/export.js +115 -0
- package/dist/history.js +61 -0
- package/dist/index.js +30 -23
- package/dist/index.js.map +3 -3
- package/dist/logger.js +80 -0
- package/dist/migrate.js +38 -0
- package/dist/mobile-lifecycle.js +94 -0
- package/dist/multi-store.js +92 -0
- package/dist/polling.js +52 -0
- package/dist/resolvers.js +223 -0
- package/dist/service-worker.js +55 -0
- package/dist/storage/indexeddb.js +59 -0
- package/dist/sync.js +181 -0
- package/dist/types.d.ts +1 -9
- package/dist/types.js +18 -0
- package/dist/validate.js +28 -0
- package/package.json +2 -2
package/dist/append-log.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type Encryptor } from "@drakkar.software/starfish-protocol";
|
|
2
2
|
import type { StarfishClient } from "./client.js";
|
|
3
3
|
import type { SyncLogger } from "./logger.js";
|
|
4
4
|
/**
|
|
@@ -28,8 +28,6 @@ export interface AuthorVerifier {
|
|
|
28
28
|
* (verify only that the signature is valid for the element's self-declared
|
|
29
29
|
* `authorPubkey` — see the `verifyAuthor` note on restricting authors). */
|
|
30
30
|
expectedAuthorPubkey?: string;
|
|
31
|
-
/** Signing suite the signatures were produced under. Defaults to `DEFAULT_ALG`. */
|
|
32
|
-
alg?: Alg;
|
|
33
31
|
}
|
|
34
32
|
/**
|
|
35
33
|
* What to do when a single element fails verification or decryption during a
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { verifyAppendAuthor, } from "@drakkar.software/starfish-protocol";
|
|
2
|
+
/** The `/pull/` action prefix; mirrors `PUSH_PATH_PREFIX` for the read side. */
|
|
3
|
+
const PULL_PATH_PREFIX = "/pull/";
|
|
4
|
+
/** The storage `documentKey` for a pull `path`: the path with the `/pull/`
|
|
5
|
+
* action prefix stripped (the namespace lives only in the URL). The author
|
|
6
|
+
* signature binds to this key, so a reader re-derives it the same way the
|
|
7
|
+
* writer did from `/push/…`. */
|
|
8
|
+
function stripPullPrefix(path) {
|
|
9
|
+
return path.startsWith(PULL_PATH_PREFIX) ? path.slice(PULL_PATH_PREFIX.length) : path;
|
|
10
|
+
}
|
|
11
|
+
/** Thrown when an append element's author signature fails verification. */
|
|
12
|
+
export class AppendAuthorError extends Error {
|
|
13
|
+
ts;
|
|
14
|
+
constructor(ts) {
|
|
15
|
+
super(`append element author verification failed (ts=${ts})`);
|
|
16
|
+
this.ts = ts;
|
|
17
|
+
this.name = "AppendAuthorError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Largest `ts` among `items`, or `0` when empty. The checkpoint for an
|
|
21
|
+
* append-only log is exactly this — the server returns elements with
|
|
22
|
+
* `ts > checkpoint`, and element timestamps are strictly increasing. */
|
|
23
|
+
export function checkpointOf(items) {
|
|
24
|
+
let max = 0;
|
|
25
|
+
for (const it of items)
|
|
26
|
+
if (it.ts > max)
|
|
27
|
+
max = it.ts;
|
|
28
|
+
return max;
|
|
29
|
+
}
|
|
30
|
+
/** Copy the optional author fields from `src` onto a fresh element with `data`. */
|
|
31
|
+
function withAuthor(ts, data, src) {
|
|
32
|
+
const out = { ts, data };
|
|
33
|
+
if (src.authorPubkey !== undefined)
|
|
34
|
+
out.authorPubkey = src.authorPubkey;
|
|
35
|
+
if (src.authorSignature !== undefined)
|
|
36
|
+
out.authorSignature = src.authorSignature;
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A stateful cursor over an append-only collection. It owns the accumulated
|
|
41
|
+
* array of elements and pulls only what is new: each {@link pull} derives the
|
|
42
|
+
* checkpoint from the last element it holds and asks the server for elements
|
|
43
|
+
* with a greater `ts`.
|
|
44
|
+
*
|
|
45
|
+
* This is the incremental, stateful counterpart to the deliberately stateless
|
|
46
|
+
* `client.pull(path, { appendField, since })`, and the sibling of
|
|
47
|
+
* {@link SyncManager} for append-only logs (no merge / push-conflict
|
|
48
|
+
* machinery — a log only grows).
|
|
49
|
+
*
|
|
50
|
+
* The cursor accumulates every pulled element in memory; for an unboundedly
|
|
51
|
+
* large log, pull a bounded window with raw `client.pull(path, { last })` instead.
|
|
52
|
+
*
|
|
53
|
+
* Cold start (nothing persisted) — first `pull()` fetches the whole collection:
|
|
54
|
+
* ```ts
|
|
55
|
+
* const log = new AppendLogCursor({ client, pullPath: "/pull/events" })
|
|
56
|
+
* const all = await log.pull()
|
|
57
|
+
* ```
|
|
58
|
+
* Warm start (resume from persisted data) — first `pull()` fetches only newer
|
|
59
|
+
* elements; persistence is a round-trip of `getItems()`:
|
|
60
|
+
* ```ts
|
|
61
|
+
* const log = new AppendLogCursor({ client, pullPath: "/pull/events",
|
|
62
|
+
* initialItems: await store.load() })
|
|
63
|
+
* const fresh = await log.pull()
|
|
64
|
+
* await store.save(log.getItems())
|
|
65
|
+
* ```
|
|
66
|
+
* Warm start for an **E2EE** log — persist ciphertext, render decrypted:
|
|
67
|
+
* ```ts
|
|
68
|
+
* const log = new AppendLogCursor({ client, pullPath: "/pull/streamchat",
|
|
69
|
+
* encryptor, persistEncrypted: true, onElementError: "skip",
|
|
70
|
+
* initialItems: await store.load() }) // ciphertext from disk
|
|
71
|
+
* const history = await log.getDecryptedItems() // render persisted history
|
|
72
|
+
* const fresh = await log.pull() // decrypted delta
|
|
73
|
+
* await store.save(log.getItems()) // ciphertext back to disk
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export class AppendLogCursor {
|
|
77
|
+
client;
|
|
78
|
+
pullPath;
|
|
79
|
+
appendField;
|
|
80
|
+
encryptor;
|
|
81
|
+
verifyAuthor;
|
|
82
|
+
onElementError;
|
|
83
|
+
persistEncrypted;
|
|
84
|
+
documentKey;
|
|
85
|
+
logger;
|
|
86
|
+
loggerName;
|
|
87
|
+
items;
|
|
88
|
+
lastCheckpoint;
|
|
89
|
+
/** Tail of the serialized pull chain. Concurrent `pull()` calls queue behind
|
|
90
|
+
* it so each runs against the checkpoint the previous one advanced — no two
|
|
91
|
+
* overlapping fetches read the same checkpoint and double-append a window. */
|
|
92
|
+
pullChain = Promise.resolve();
|
|
93
|
+
constructor(options) {
|
|
94
|
+
this.client = options.client;
|
|
95
|
+
this.pullPath = options.pullPath;
|
|
96
|
+
this.appendField = options.appendField ?? "items";
|
|
97
|
+
this.encryptor = options.encryptor;
|
|
98
|
+
this.verifyAuthor = options.verifyAuthor;
|
|
99
|
+
this.onElementError = options.onElementError ?? "throw";
|
|
100
|
+
this.persistEncrypted = options.persistEncrypted ?? false;
|
|
101
|
+
this.documentKey = stripPullPrefix(options.pullPath);
|
|
102
|
+
this.logger = options.logger;
|
|
103
|
+
this.loggerName =
|
|
104
|
+
options.loggerName ?? options.pullPath.split("/").filter(Boolean).pop() ?? options.pullPath;
|
|
105
|
+
const seed = options.initialItems ?? [];
|
|
106
|
+
const seedCheckpoint = checkpointOf(seed);
|
|
107
|
+
if (options.since != null) {
|
|
108
|
+
if (options.since < 0)
|
|
109
|
+
throw new Error("since must be non-negative");
|
|
110
|
+
if (options.since < seedCheckpoint) {
|
|
111
|
+
throw new Error("since must be >= the max ts of initialItems");
|
|
112
|
+
}
|
|
113
|
+
this.lastCheckpoint = options.since;
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
this.lastCheckpoint = seedCheckpoint;
|
|
117
|
+
}
|
|
118
|
+
this.items = [...seed];
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Fetch elements newer than the current checkpoint, verify + decrypt them,
|
|
122
|
+
* append them to the local log, and return ONLY the newly-fetched batch
|
|
123
|
+
* (decrypted when an `encryptor` is set).
|
|
124
|
+
*
|
|
125
|
+
* Atomic under `onElementError: "throw"` (the default): the batch is fully
|
|
126
|
+
* verified and decrypted into a local before any state mutation, so a
|
|
127
|
+
* verify/decrypt failure throws without advancing the checkpoint past elements
|
|
128
|
+
* that could never be re-fetched. Under `"skip"`, a failing element is dropped
|
|
129
|
+
* from the returned batch but the checkpoint still advances past it.
|
|
130
|
+
*
|
|
131
|
+
* Safe to call concurrently: overlapping calls are serialized internally, so
|
|
132
|
+
* each runs against the checkpoint the previous one advanced (no double-fetch
|
|
133
|
+
* of the same window). The next pull after one completes will pick up anything
|
|
134
|
+
* that arrived in between.
|
|
135
|
+
*/
|
|
136
|
+
async pull() {
|
|
137
|
+
// Chain onto the previous pull (whether it resolved or rejected) so calls
|
|
138
|
+
// run one-at-a-time against the latest checkpoint. `pullChain` swallows
|
|
139
|
+
// outcomes to stay alive; the caller still sees this call's real result.
|
|
140
|
+
const run = this.pullChain.then(() => this.doPull(), () => this.doPull());
|
|
141
|
+
this.pullChain = run.then(() => undefined, () => undefined);
|
|
142
|
+
return run;
|
|
143
|
+
}
|
|
144
|
+
async doPull() {
|
|
145
|
+
this.logger?.pullStart(this.loggerName);
|
|
146
|
+
const start = performance.now();
|
|
147
|
+
try {
|
|
148
|
+
const since = this.lastCheckpoint;
|
|
149
|
+
// Omit `since` on cold start so the request carries no `?checkpoint=`.
|
|
150
|
+
const opts = since > 0 ? { appendField: this.appendField, since } : { appendField: this.appendField };
|
|
151
|
+
const raw = await this.client.pull(this.pullPath, opts);
|
|
152
|
+
const batch = []; // decrypted, returned to the caller
|
|
153
|
+
const stored = []; // what we keep in `items` (cipher- or plaintext)
|
|
154
|
+
let maxTs = since;
|
|
155
|
+
let skipped = 0;
|
|
156
|
+
for (const el of raw) {
|
|
157
|
+
// Defensive: guard a misbehaving/mocked server from making us
|
|
158
|
+
// double-append a held element. Gated on `since > 0` to mirror the
|
|
159
|
+
// server (which only filters when checkpoint > 0): on a cold start
|
|
160
|
+
// `since` is 0 and we must NOT drop a legitimate `ts: 0` first element.
|
|
161
|
+
if (since > 0 && el.ts <= since)
|
|
162
|
+
continue;
|
|
163
|
+
// Advance past every windowed element BEFORE verify/decrypt so a skipped
|
|
164
|
+
// element still moves the checkpoint and is never re-fetched.
|
|
165
|
+
if (el.ts > maxTs)
|
|
166
|
+
maxTs = el.ts;
|
|
167
|
+
let decrypted = null;
|
|
168
|
+
try {
|
|
169
|
+
this.verifyOne(el);
|
|
170
|
+
const data = this.encryptor ? await this.encryptor.decrypt(el.data) : el.data;
|
|
171
|
+
decrypted = withAuthor(el.ts, data, el);
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
// "throw" rethrows here, before any state mutation below — atomic.
|
|
175
|
+
if (this.onElementError !== "skip")
|
|
176
|
+
throw err;
|
|
177
|
+
skipped++;
|
|
178
|
+
}
|
|
179
|
+
if (this.persistEncrypted) {
|
|
180
|
+
// Keep the original ciphertext envelope (even for a skipped element:
|
|
181
|
+
// it is valid data we simply cannot read now — a later key might).
|
|
182
|
+
stored.push(withAuthor(el.ts, el.data, el));
|
|
183
|
+
}
|
|
184
|
+
else if (decrypted) {
|
|
185
|
+
stored.push(decrypted);
|
|
186
|
+
}
|
|
187
|
+
if (decrypted)
|
|
188
|
+
batch.push(decrypted);
|
|
189
|
+
}
|
|
190
|
+
this.items.push(...stored);
|
|
191
|
+
this.lastCheckpoint = maxTs;
|
|
192
|
+
this.logger?.pullSuccess(this.loggerName, Math.round(performance.now() - start), skipped > 0 ? { skippedCount: skipped } : undefined);
|
|
193
|
+
return batch;
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
this.logger?.pullError(this.loggerName, err instanceof Error ? err.message : String(err));
|
|
197
|
+
throw err;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/** Verify a single element's author signature over its RAW (pre-decryption)
|
|
201
|
+
* `data`. Throws {@link AppendAuthorError} on any failure. No-op when
|
|
202
|
+
* verification is disabled. */
|
|
203
|
+
verifyOne(el) {
|
|
204
|
+
if (!this.verifyAuthor)
|
|
205
|
+
return;
|
|
206
|
+
const policy = typeof this.verifyAuthor === "object" ? this.verifyAuthor : {};
|
|
207
|
+
const { authorPubkey, authorSignature } = el;
|
|
208
|
+
if (!authorPubkey || !authorSignature)
|
|
209
|
+
throw new AppendAuthorError(el.ts);
|
|
210
|
+
// Public keys are hex, which is case-insensitive — compare normalized so a
|
|
211
|
+
// caller passing a differently-cased `expectedAuthorPubkey` isn't falsely rejected.
|
|
212
|
+
if (policy.expectedAuthorPubkey &&
|
|
213
|
+
authorPubkey.toLowerCase() !== policy.expectedAuthorPubkey.toLowerCase()) {
|
|
214
|
+
throw new AppendAuthorError(el.ts);
|
|
215
|
+
}
|
|
216
|
+
void policy;
|
|
217
|
+
const ok = verifyAppendAuthor(this.documentKey, el.data, authorPubkey, authorSignature);
|
|
218
|
+
if (!ok)
|
|
219
|
+
throw new AppendAuthorError(el.ts);
|
|
220
|
+
}
|
|
221
|
+
/** The full accumulated log (a shallow copy), in `ts` order. Under
|
|
222
|
+
* `persistEncrypted` these carry CIPHERTEXT `data` (persist them as-is, then
|
|
223
|
+
* re-seed via `initialItems`); otherwise they carry decrypted/plaintext data. */
|
|
224
|
+
getItems() {
|
|
225
|
+
return [...this.items];
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* The full accumulated log, DECRYPTED — for rendering warm-started history in
|
|
229
|
+
* `persistEncrypted` mode (where {@link getItems} holds ciphertext). Honors
|
|
230
|
+
* `onElementError` (a `"skip"` cursor drops elements it cannot read). When the
|
|
231
|
+
* cursor has no `encryptor`, or is not in `persistEncrypted` mode, the held
|
|
232
|
+
* elements are already plaintext/decrypted and are returned as-is.
|
|
233
|
+
*/
|
|
234
|
+
async getDecryptedItems() {
|
|
235
|
+
const snapshot = [...this.items];
|
|
236
|
+
if (!this.encryptor || !this.persistEncrypted)
|
|
237
|
+
return snapshot;
|
|
238
|
+
const out = [];
|
|
239
|
+
for (const el of snapshot) {
|
|
240
|
+
try {
|
|
241
|
+
this.verifyOne(el);
|
|
242
|
+
const data = await this.encryptor.decrypt(el.data);
|
|
243
|
+
out.push(withAuthor(el.ts, data, el));
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
if (this.onElementError !== "skip")
|
|
247
|
+
throw err;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
/** The current checkpoint: the max `ts` held (the next pull's `since`). `0`
|
|
253
|
+
* when nothing has been pulled or seeded. */
|
|
254
|
+
getCheckpoint() {
|
|
255
|
+
return this.lastCheckpoint;
|
|
256
|
+
}
|
|
257
|
+
/** Restore the checkpoint without seeding items — for persistence layers that
|
|
258
|
+
* store only the checkpoint. Used to resume incrementally across restarts.
|
|
259
|
+
* Rejects a value below the max `ts` already held: rewinding would make the
|
|
260
|
+
* next pull re-deliver, and duplicate, elements the cursor already has. */
|
|
261
|
+
setCheckpoint(ts) {
|
|
262
|
+
if (ts < checkpointOf(this.items)) {
|
|
263
|
+
throw new Error("checkpoint must be >= the max ts already held");
|
|
264
|
+
}
|
|
265
|
+
this.lastCheckpoint = ts;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background Sync API integration for pending changes.
|
|
3
|
+
* Uses the Web Background Sync API to retry failed sync operations
|
|
4
|
+
* when connectivity is restored, even if the app is closed.
|
|
5
|
+
*/
|
|
6
|
+
/** Check if the Background Sync API is supported in the current environment. */
|
|
7
|
+
export function isBackgroundSyncSupported() {
|
|
8
|
+
return (typeof navigator !== "undefined" &&
|
|
9
|
+
"serviceWorker" in navigator &&
|
|
10
|
+
"SyncManager" in globalThis);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Register a background sync event with the active service worker.
|
|
14
|
+
* Returns true if registration succeeded, false if not supported or no active SW.
|
|
15
|
+
*/
|
|
16
|
+
export async function registerBackgroundSync(opts) {
|
|
17
|
+
if (!isBackgroundSyncSupported())
|
|
18
|
+
return false;
|
|
19
|
+
const tag = opts?.tag ?? "starfish-sync";
|
|
20
|
+
try {
|
|
21
|
+
const registration = await navigator.serviceWorker.ready;
|
|
22
|
+
// @ts-expect-error - SyncManager types may not be available
|
|
23
|
+
await registration.sync.register(tag);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Suspense integration for Starfish sync data.
|
|
3
|
+
* Creates resources that throw Promises while loading (Suspense protocol).
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Create a Suspense-compatible resource from an async fetcher.
|
|
7
|
+
* The first call to `read()` triggers the fetch. While loading, `read()` throws
|
|
8
|
+
* a Promise (which React Suspense catches to show a fallback). Once resolved,
|
|
9
|
+
* `read()` returns the value synchronously.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* const resource = createSuspenseResource(() => syncManager.pull())
|
|
14
|
+
* function MyComponent() {
|
|
15
|
+
* const data = resource.read() // throws while loading, returns data when ready
|
|
16
|
+
* return <div>{JSON.stringify(data)}</div>
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export function createSuspenseResource(fetcher) {
|
|
21
|
+
let status = "pending";
|
|
22
|
+
let result;
|
|
23
|
+
let error;
|
|
24
|
+
let promise = null;
|
|
25
|
+
function init() {
|
|
26
|
+
if (promise)
|
|
27
|
+
return promise;
|
|
28
|
+
promise = fetcher().then((value) => {
|
|
29
|
+
status = "resolved";
|
|
30
|
+
result = value;
|
|
31
|
+
}, (err) => {
|
|
32
|
+
status = "rejected";
|
|
33
|
+
error = err;
|
|
34
|
+
});
|
|
35
|
+
return promise;
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
read() {
|
|
39
|
+
switch (status) {
|
|
40
|
+
case "pending":
|
|
41
|
+
throw init();
|
|
42
|
+
case "resolved":
|
|
43
|
+
return result;
|
|
44
|
+
case "rejected":
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
package/dist/bindings/zustand.js
CHANGED
|
@@ -240,11 +240,9 @@ import {
|
|
|
240
240
|
HEADER_SIG,
|
|
241
241
|
HEADER_TS,
|
|
242
242
|
HEADER_NONCE,
|
|
243
|
-
HEADER_ALG,
|
|
244
243
|
HEADER_PUB,
|
|
245
244
|
HEADER_CONTENT_TYPE,
|
|
246
245
|
HEADER_ACCEPT,
|
|
247
|
-
DEFAULT_ALG,
|
|
248
246
|
signAppendAuthor,
|
|
249
247
|
signRequest,
|
|
250
248
|
stableStringify
|
|
@@ -354,23 +352,19 @@ var StarfishClient = class {
|
|
|
354
352
|
* presenter` bind the server checks.
|
|
355
353
|
*/
|
|
356
354
|
async capRequestHeaders(capCtx, method, pathAndQuery, body) {
|
|
357
|
-
const { cap, devEdPrivHex, pubHex
|
|
355
|
+
const { cap, devEdPrivHex, pubHex } = capCtx;
|
|
358
356
|
const req = {
|
|
359
357
|
method,
|
|
360
358
|
pathAndQuery,
|
|
361
359
|
body,
|
|
362
360
|
host: this.signingHost()
|
|
363
361
|
};
|
|
364
|
-
const
|
|
365
|
-
const { alg, sig, ts, nonce } = await signRequest(req, devEdPrivHex, {
|
|
366
|
-
alg: signAlg
|
|
367
|
-
});
|
|
362
|
+
const { sig, ts, nonce } = await signRequest(req, devEdPrivHex);
|
|
368
363
|
const headers = {
|
|
369
364
|
[HEADER_AUTHORIZATION]: `Cap ${encodeCapAuth(cap)}`,
|
|
370
365
|
[HEADER_SIG]: sig,
|
|
371
366
|
[HEADER_TS]: String(ts),
|
|
372
|
-
[HEADER_NONCE]: nonce
|
|
373
|
-
[HEADER_ALG]: alg
|
|
367
|
+
[HEADER_NONCE]: nonce
|
|
374
368
|
};
|
|
375
369
|
if (pubHex !== void 0) headers[HEADER_PUB] = pubHex;
|
|
376
370
|
return headers;
|
|
@@ -384,11 +378,10 @@ var StarfishClient = class {
|
|
|
384
378
|
* unsigned and a server requiring signatures rejects it.
|
|
385
379
|
*/
|
|
386
380
|
appendAuthorKey(capCtx) {
|
|
387
|
-
const { cap, pubHex
|
|
381
|
+
const { cap, pubHex } = capCtx;
|
|
388
382
|
const authorPubHex = pubHex ?? cap.sub;
|
|
389
383
|
if (authorPubHex === void 0) return null;
|
|
390
|
-
|
|
391
|
-
return { authorPubHex, signAlg };
|
|
384
|
+
return { authorPubHex };
|
|
392
385
|
}
|
|
393
386
|
async pull(path, checkpointOrOptions) {
|
|
394
387
|
let pathAndQuery = this.applyNamespace(path);
|
|
@@ -436,12 +429,16 @@ var StarfishClient = class {
|
|
|
436
429
|
return result;
|
|
437
430
|
}
|
|
438
431
|
/**
|
|
439
|
-
* Pull several
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
432
|
+
* Pull several documents in one round-trip via `/batch/pull`. `collections` is
|
|
433
|
+
* the list of distinct collection names; `opts.params` supplies, per collection,
|
|
434
|
+
* an ARRAY of path-param sets — one per document to read — so the SAME collection
|
|
435
|
+
* can fan in many documents (e.g. many users' `profile`) in a single request.
|
|
436
|
+
* The server auto-fills the `{identity}` param from the authenticated caller for
|
|
437
|
+
* any set that omits it, so a self-doc collection needs no params. Returns a map
|
|
438
|
+
* of collection name → an ARRAY of pulled documents (or per-document `{ error }`),
|
|
439
|
+
* in request order. Honors the configured namespace.
|
|
440
|
+
*
|
|
441
|
+
* For the common "many docs of one collection" case prefer {@link batchPullMany}.
|
|
445
442
|
*
|
|
446
443
|
* Note: not append/checkpoint-aware — for incremental append-only reads use
|
|
447
444
|
* `pull(path, { since })` (or `AppendLogCursor`) per collection.
|
|
@@ -464,6 +461,18 @@ var StarfishClient = class {
|
|
|
464
461
|
}
|
|
465
462
|
return await res.json();
|
|
466
463
|
}
|
|
464
|
+
/**
|
|
465
|
+
* Convenience over {@link batchPull} for reading MANY documents of ONE
|
|
466
|
+
* collection in a single round-trip: pass the per-document param-sets and get
|
|
467
|
+
* back the {@link BatchPullEntry} array aligned to `paramsList` by index (each
|
|
468
|
+
* entry is `{ data, hash, timestamp }` or `{ error }`). An empty `paramsList`
|
|
469
|
+
* issues no request and returns `[]`.
|
|
470
|
+
*/
|
|
471
|
+
async batchPullMany(collection, paramsList) {
|
|
472
|
+
if (paramsList.length === 0) return [];
|
|
473
|
+
const res = await this.batchPull([collection], { params: { [collection]: paramsList } });
|
|
474
|
+
return res.collections[collection] ?? [];
|
|
475
|
+
}
|
|
467
476
|
/**
|
|
468
477
|
* Push synced data to the server.
|
|
469
478
|
* @param path - The push endpoint path (e.g. "/push/users/abc/settings")
|
|
@@ -535,8 +544,7 @@ var StarfishClient = class {
|
|
|
535
544
|
documentKey,
|
|
536
545
|
data,
|
|
537
546
|
authorKey.authorPubHex,
|
|
538
|
-
capCtx.devEdPrivHex
|
|
539
|
-
authorKey.signAlg
|
|
547
|
+
capCtx.devEdPrivHex
|
|
540
548
|
);
|
|
541
549
|
bodyObj[AUTHOR_PUBKEY_FIELD] = authorPubkey;
|
|
542
550
|
bodyObj[AUTHOR_SIGNATURE_FIELD] = authorSignature;
|