@estiva-app/protocol 0.3.0 → 0.5.0

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/CHANGELOG.md CHANGED
@@ -4,6 +4,117 @@ Every entry answers the wire question explicitly, including when the answer is
4
4
  nothing (ADR 0002 §4b). A change to the bytes an app publishes is a MAJOR — in
5
5
  `0.x`, a MINOR — even when no TypeScript signature moved.
6
6
 
7
+ ## 0.5.0 — 2026-09-01
8
+
9
+ **Wire behaviour: unchanged.** No builder, tag layout, id computation or
10
+ signature input moved. `queryAll` returns the same events; it asks for them in
11
+ half as many requests.
12
+
13
+ - **A short page no longer always costs a confirming round trip.** `queryAll`
14
+ spent two requests on every filter, including one holding three events, and
15
+ on Ship's `loadAll` that doubling was **33 filters → 66 requests** against a
16
+ relay that meters `POST /query` at 300 a minute.
17
+
18
+ The confirmation existed for a real reason: a page shorter than the requested
19
+ `limit` is ambiguous, because the relay clamps to `min(requested, ceiling)`
20
+ and may have clamped at exactly that many. The ambiguity dissolves
21
+ arithmetically rather than by trusting anyone — a clamp at `n` requires the
22
+ ceiling to *equal* `n`, the ceiling is one constant for the relay, and the
23
+ largest page it has already handed back is a lower bound on it. So
24
+ `n < observedPageCeiling` rules the clamp out on evidence the relay itself
25
+ produced.
26
+
27
+ **Deliberately not NIP-11's `limitation.max_limit`.** That number is a claim,
28
+ and a relay advertising more than it clamps to would make every page look
29
+ short and the first one get mistaken for the whole set — the SHA-8 bug, back.
30
+ Buzz currently advertises 1000 and clamps at 1000, so trusting it would work
31
+ today; the source default in `nip11.rs` is 10000 against a `buzz-db` clamp of
32
+ 1000, so it has not always been true, and being right by luck is not a
33
+ design. Substituting a trusted 1000 for the observed bound fails the SHA-8
34
+ regression test outright, which is the control that settles it.
35
+
36
+ Conservative before it has evidence: the first filter through a fresh `Relay`
37
+ still confirms, and so does the **widest** filter on every read, since it
38
+ ties its own bound. The floor is therefore one request per filter *plus one*.
39
+
40
+ Measured on Ship's `loadAll` against production, published client versus this
41
+ one: **66 requests → 35 cold, 34 warm — 48% fewer — 8.8 s → 0.9 s, and the
42
+ fold compares equal event for event** (every project and issue address with
43
+ its status, every change id, every conversation id).
44
+
45
+ - **This still does not fit a five-second poll.** 34 requests × 12 reads a
46
+ minute is 408 against a 300 budget; at a ten-second poll it is 204 and fits.
47
+ The remaining half is the caller's cadence, not this package's.
48
+
49
+ Additive with no signature change, but the request pattern a caller produces
50
+ changes, so a MINOR: ADR 0002 §4b puts the break on MINOR within `0.x`.
51
+
52
+ ## 0.4.0 — 2026-09-01
53
+
54
+ **Wire behaviour: unchanged.** No builder, tag layout, id computation or
55
+ signature input moved. `queryAll` returns the same array it always did — see
56
+ below, that is the property the tests are about.
57
+
58
+ - **`queryAll` runs its filters concurrently**, bounded by the new
59
+ `DEFAULT_QUERY_CONCURRENCY` (8) and overridable per call via
60
+ `QueryAllOptions.concurrency`. `concurrency: 1` is the old serial read
61
+ exactly.
62
+
63
+ Filters handed to one call are independent; only the pages *inside* a filter
64
+ are a cursor walk, and those stay sequential because page N+1's `until` is
65
+ not known until page N has answered. Running the filters in sequence made a
66
+ read cost the sum of every filter's latency. Measured on Ship's `loadAll`
67
+ against production: **33 filters, 66 round trips, median 206 ms, 100% of wall
68
+ clock spent inside `query` one request at a time.** The same read, same relay,
69
+ same credentials, through this build: **9.0 s → 1.68 s, and the fold compares
70
+ equal event for event** — every project and issue address with its status,
71
+ every change id, every conversation id.
72
+
73
+ The concurrency is deliberately not observable in the answer. Results are
74
+ collected per filter and concatenated in filter order, and a failure reports
75
+ the **lowest-indexed** filter's error rather than whichever request lost the
76
+ race — so a broken read names the same filter twice running.
77
+
78
+ - **What this does not buy you: relay budget.** Buzz meters `POST /query`
79
+ against `human_api_calls_per_min` — a fixed 60-second window, default 300,
80
+ applied to every bridge call whatever tier the caller is. One Ship workspace
81
+ read is 66 of those. Concurrency changes when a read spends its requests,
82
+ never how many, so a poller that simply reads more often now spends the same
83
+ allowance sooner. Documented at `DEFAULT_QUERY_CONCURRENCY` because it is the
84
+ first thing a caller will get wrong.
85
+
86
+ Additive, and behaviour under a caller's existing call changes (requests now
87
+ overlap), so a MINOR: ADR 0002 §4b puts the break on MINOR within `0.x`.
88
+
89
+ ## 0.3.0 — 2026-09-01
90
+
91
+ *Backfilled 2026-09-01. This release shipped without an entry, and so did 0.2.0
92
+ below — the file's own rule is that every release answers the wire question, and
93
+ twice it went unanswered. Recorded now from the release commits rather than left
94
+ as a gap.*
95
+
96
+ **Wire behaviour: unchanged.** NIP-19 is an encoding of a pointer, not of an
97
+ event; nothing an app publishes moved.
98
+
99
+ - **`nevent`** — `EventPointer`, `encodeNevent`, `decodeNevent`. An event that
100
+ is not addressable had no reference form at all, so a plain `kind:9` message
101
+ could be resolved by nothing. TLV type 0 carries the event id as 32 raw
102
+ bytes, where `naddr` carries a UTF-8 `d`; that difference is the whole of the
103
+ codec.
104
+
105
+ ## 0.2.0 — 2026-08-28
106
+
107
+ *Backfilled 2026-09-01, from the release commit — see the note under 0.3.0.*
108
+
109
+ **Wire behaviour: unchanged.** A read-path fix; no published bytes moved.
110
+
111
+ - **`RELAY_PAGE_CEILING`, and `queryAll` pages instead of truncating.** Buzz
112
+ clamps a REQ to its advertised NIP-11 `max_limit`, which halved from 10000 to
113
+ 1000, and NIP-01 has no truncation signal — so a caller asking for 2000 got
114
+ 1000 and no indication why. `limit: 2000` was a literal at four call sites
115
+ across two apps and the agent, none of which could know when the relay changed
116
+ it.
117
+
7
118
  ## 0.1.2 — 2026-08-27
8
119
 
9
120
  **Wire behaviour: unchanged.** One constant added. No builder, tag layout, id
package/dist/bridge.d.ts CHANGED
@@ -37,6 +37,48 @@ export interface PublishResult {
37
37
  * is the relay's real ceiling — see its note on why.
38
38
  */
39
39
  export declare const RELAY_PAGE_CEILING = 1000;
40
+ /**
41
+ * How many of a {@link Relay.queryAll} call's filters may be in flight at once.
42
+ *
43
+ * Filters are independent — only paging *within* one is a cursor walk — and
44
+ * running them one after another made a read as slow as the sum of its parts.
45
+ * Measured on Ship's `loadAll` against production: **33 filters, 66 sequential
46
+ * round trips, median 206 ms, 100% of wall clock spent inside `query` one
47
+ * request at a time.** At a concurrency of 8 the same read returned a
48
+ * byte-identical answer in 1.9 s instead of 9.0 s.
49
+ *
50
+ * Bounded rather than unbounded on purpose. A workspace's filter count grows
51
+ * with its Folders, so `Promise.all` over all of them would open a fan-out
52
+ * whose width is set by the *data* — fine at 33, an accidental flood at 500,
53
+ * and the relay is shared. Eight is enough to hide the latency without any
54
+ * caller having to think about it.
55
+ *
56
+ * Pass `concurrency: 1` to restore the strictly serial read.
57
+ *
58
+ * ## This does not buy a caller more relay budget
59
+ *
60
+ * Concurrency changes how fast a read spends its requests, never how many it
61
+ * makes. Buzz meters `POST /query` against `human_api_calls_per_min` — a fixed
62
+ * 60-second window, **default 300, applied to every bridge call whatever tier
63
+ * the caller is** (`enforce_http_admission` in `api/bridge.rs` reads the human
64
+ * limit unconditionally). At 66 requests, one Ship workspace read is 22% of a
65
+ * minute's entire allowance, so no client gets more than ~4.5 of them a minute
66
+ * however it schedules them.
67
+ *
68
+ * A poller therefore has to slow down as its reads get faster, or it simply
69
+ * spends the same budget sooner and starts collecting
70
+ * `rate-limited: quota exceeded`. Making a read cheap is the fix for that;
71
+ * making it parallel is not.
72
+ */
73
+ export declare const DEFAULT_QUERY_CONCURRENCY = 8;
74
+ export interface QueryAllOptions {
75
+ /** Events per request. Defaults to {@link RELAY_PAGE_CEILING}. */
76
+ pageSize?: number;
77
+ /** Give up after this many pages *per filter*, rather than looping forever. */
78
+ maxPages?: number;
79
+ /** Filters in flight at once. Defaults to {@link DEFAULT_QUERY_CONCURRENCY}; 1 is serial. */
80
+ concurrency?: number;
81
+ }
40
82
  export declare function parsePublishResponse(status: number, text: string): PublishResult;
41
83
  export interface QueryResult {
42
84
  ok: boolean;
@@ -95,6 +137,18 @@ export declare class Relay {
95
137
  private readonly signer;
96
138
  private readonly headers;
97
139
  private readonly transport;
140
+ /**
141
+ * The largest page this relay has ever handed back, across every call.
142
+ *
143
+ * A *lower bound* on the relay's real clamp, and the only one obtainable
144
+ * without trusting anybody: the relay returned this many events in one
145
+ * response, so its ceiling is at least this large. {@link Relay.pageFilter}
146
+ * uses it to tell a short page apart from a clamped one — see there.
147
+ *
148
+ * Per instance and never reset, so the second read through a long-lived
149
+ * `Relay` is cheaper than the first.
150
+ */
151
+ private observedPageCeiling;
98
152
  constructor(url: string, signer: Signer, options?: RelayOptions | RelayHeaders);
99
153
  private post;
100
154
  publish(event: SignedEvent): Promise<PublishResult>;
@@ -131,10 +185,26 @@ export declare class Relay {
131
185
  * value of `until` reaches past them without skipping some, and no correct
132
186
  * answer exists from this API — so it says so instead of returning a
133
187
  * plausible subset.
188
+ *
189
+ * **Filters run concurrently, up to {@link DEFAULT_QUERY_CONCURRENCY}.** They
190
+ * are independent of one another; only the pages inside one are a cursor
191
+ * walk. Doing them in sequence made a read cost the sum of every filter's
192
+ * latency, which is what put Ship's workspace read at nine seconds against a
193
+ * five-second poll — long enough that a write's re-read had usually not
194
+ * finished before the next one began.
195
+ *
196
+ * The concurrency is deliberately **not observable in the answer**: results
197
+ * are collected per filter and concatenated in filter order, so the returned
198
+ * array is identical to the serial one, and a failure reports the
199
+ * lowest-indexed filter's error rather than whichever lost the race.
200
+ */
201
+ queryAll(filters: Record<string, unknown>[], options?: QueryAllOptions): Promise<SignedEvent[]>;
202
+ /**
203
+ * One filter, paged to exhaustion. The sequential half, and it has to be:
204
+ * `until` is a cursor, so page N+1's request is not known until page N has
205
+ * answered. Only the filters are independent, which is why they are the
206
+ * axis {@link Relay.queryAll} parallelises.
134
207
  */
135
- queryAll(filters: Record<string, unknown>[], options?: {
136
- pageSize?: number;
137
- maxPages?: number;
138
- }): Promise<SignedEvent[]>;
208
+ private pageFilter;
139
209
  }
140
210
  //# sourceMappingURL=bridge.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAA;AAEvC,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,OAAO,CAAA;IACX,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,wDAAwD;IACxD,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,4EAA4E;IAC5E,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED;;;;;;;;GAQG;AACH;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,kBAAkB,OAAO,CAAA;AAEtC,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,aAAa,CAehF;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,OAAO,CAAA;IACX,MAAM,EAAE,WAAW,EAAE,CAAA;IACrB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,wDAAwD;AACxD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,WAAW,CAgB5E;AAED;;;;;;GAMG;AACH,MAAM,MAAM,SAAS,GAAG,CACtB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,KACpE,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAAA;CAAE,CAAC,CAAA;AAKzD,4EAA4E;AAC5E,MAAM,MAAM,YAAY,GAAG,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;AAEzF,MAAM,WAAW,YAAY;IAC3B;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,EAAE,YAAY,CAAA;IACtB,iEAAiE;IACjE,KAAK,CAAC,EAAE,SAAS,CAAA;CAClB;AAED;;;;;;GAMG;AACH,qBAAa,KAAK;IAChB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAQ;IAC5B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAc;IACtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAuB;gBAErC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,YAAY,GAAG,YAAiB;YAWpE,IAAI;IAwBZ,OAAO,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,CAAC,aAAa,CAAC;IAKzD;;;;;;OAMG;IACG,KAAK,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAOvE;;;;;;;;;;;;;;;;;;;;;;;;;OAyBG;IACG,QAAQ,CACZ,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAClC,OAAO,GAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAO,GACrD,OAAO,CAAC,WAAW,EAAE,CAAC;CAqC1B"}
1
+ {"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"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"}
package/dist/bridge.js CHANGED
@@ -53,6 +53,40 @@ import { authorizationHeader } from './nip98.js';
53
53
  * is the relay's real ceiling — see its note on why.
54
54
  */
55
55
  export const RELAY_PAGE_CEILING = 1000;
56
+ /**
57
+ * How many of a {@link Relay.queryAll} call's filters may be in flight at once.
58
+ *
59
+ * Filters are independent — only paging *within* one is a cursor walk — and
60
+ * running them one after another made a read as slow as the sum of its parts.
61
+ * Measured on Ship's `loadAll` against production: **33 filters, 66 sequential
62
+ * round trips, median 206 ms, 100% of wall clock spent inside `query` one
63
+ * request at a time.** At a concurrency of 8 the same read returned a
64
+ * byte-identical answer in 1.9 s instead of 9.0 s.
65
+ *
66
+ * Bounded rather than unbounded on purpose. A workspace's filter count grows
67
+ * with its Folders, so `Promise.all` over all of them would open a fan-out
68
+ * whose width is set by the *data* — fine at 33, an accidental flood at 500,
69
+ * and the relay is shared. Eight is enough to hide the latency without any
70
+ * caller having to think about it.
71
+ *
72
+ * Pass `concurrency: 1` to restore the strictly serial read.
73
+ *
74
+ * ## This does not buy a caller more relay budget
75
+ *
76
+ * Concurrency changes how fast a read spends its requests, never how many it
77
+ * makes. Buzz meters `POST /query` against `human_api_calls_per_min` — a fixed
78
+ * 60-second window, **default 300, applied to every bridge call whatever tier
79
+ * the caller is** (`enforce_http_admission` in `api/bridge.rs` reads the human
80
+ * limit unconditionally). At 66 requests, one Ship workspace read is 22% of a
81
+ * minute's entire allowance, so no client gets more than ~4.5 of them a minute
82
+ * however it schedules them.
83
+ *
84
+ * A poller therefore has to slow down as its reads get faster, or it simply
85
+ * spends the same budget sooner and starts collecting
86
+ * `rate-limited: quota exceeded`. Making a read cheap is the fix for that;
87
+ * making it parallel is not.
88
+ */
89
+ export const DEFAULT_QUERY_CONCURRENCY = 8;
56
90
  export function parsePublishResponse(status, text) {
57
91
  let parsed;
58
92
  try {
@@ -102,6 +136,18 @@ export class Relay {
102
136
  signer;
103
137
  headers;
104
138
  transport;
139
+ /**
140
+ * The largest page this relay has ever handed back, across every call.
141
+ *
142
+ * A *lower bound* on the relay's real clamp, and the only one obtainable
143
+ * without trusting anybody: the relay returned this many events in one
144
+ * response, so its ceiling is at least this large. {@link Relay.pageFilter}
145
+ * uses it to tell a short page apart from a clamped one — see there.
146
+ *
147
+ * Per instance and never reset, so the second read through a long-lived
148
+ * `Relay` is cheaper than the first.
149
+ */
150
+ observedPageCeiling = 0;
105
151
  constructor(url, signer, options = {}) {
106
152
  this.url = url.replace(/\/+$/, '');
107
153
  this.signer = signer;
@@ -178,38 +224,140 @@ export class Relay {
178
224
  * value of `until` reaches past them without skipping some, and no correct
179
225
  * answer exists from this API — so it says so instead of returning a
180
226
  * plausible subset.
227
+ *
228
+ * **Filters run concurrently, up to {@link DEFAULT_QUERY_CONCURRENCY}.** They
229
+ * are independent of one another; only the pages inside one are a cursor
230
+ * walk. Doing them in sequence made a read cost the sum of every filter's
231
+ * latency, which is what put Ship's workspace read at nine seconds against a
232
+ * five-second poll — long enough that a write's re-read had usually not
233
+ * finished before the next one began.
234
+ *
235
+ * The concurrency is deliberately **not observable in the answer**: results
236
+ * are collected per filter and concatenated in filter order, so the returned
237
+ * array is identical to the serial one, and a failure reports the
238
+ * lowest-indexed filter's error rather than whichever lost the race.
181
239
  */
182
240
  async queryAll(filters, options = {}) {
183
241
  const pageSize = options.pageSize ?? RELAY_PAGE_CEILING;
184
242
  const maxPages = options.maxPages ?? 100;
185
- const seen = new Set();
186
- const out = [];
187
- for (const filter of filters) {
188
- let until;
189
- for (let page = 0;; page++) {
190
- if (page >= maxPages) {
191
- throw new Error(`queryAll: gave up after ${maxPages} pages refusing to return a partial set`);
243
+ const concurrency = Math.max(1, options.concurrency ?? DEFAULT_QUERY_CONCURRENCY);
244
+ /*
245
+ Per filter, in filter order not one shared accumulator.
246
+
247
+ Concurrency must not be observable in the answer. Collecting each
248
+ filter's pages into its own slot and concatenating in order at the end
249
+ makes the output byte-identical to the serial version, whatever order the
250
+ responses actually arrive in. Dedup then runs over that ordered
251
+ concatenation, so an id first seen under filter 0 still belongs to filter
252
+ 0's run of events, exactly as before.
253
+ */
254
+ const perFilter = new Array(filters.length);
255
+ const failures = new Array(filters.length);
256
+ let next = 0;
257
+ let failed = false;
258
+ const worker = async () => {
259
+ for (;;) {
260
+ // Stop *starting* filters once one has failed. The serial version never
261
+ // reached them at all; this is as close as a fan-out gets, and it keeps
262
+ // a broken read from firing the whole remaining queue at the relay.
263
+ if (failed)
264
+ return;
265
+ const index = next++;
266
+ if (index >= filters.length)
267
+ return;
268
+ try {
269
+ perFilter[index] = await this.pageFilter(filters[index], pageSize, maxPages);
192
270
  }
193
- const events = await this.query([
194
- { ...filter, limit: pageSize, ...(until === undefined ? {} : { until }) },
195
- ]);
196
- if (events.length === 0)
197
- break;
198
- for (const e of events) {
199
- if (seen.has(e.id))
200
- continue;
201
- seen.add(e.id);
202
- out.push(e);
271
+ catch (error) {
272
+ failures[index] = error;
273
+ failed = true;
274
+ return;
203
275
  }
204
- const oldest = Math.min(...events.map((e) => e.created_at));
205
- if (until !== undefined && oldest >= until) {
206
- if (events.length >= pageSize) {
207
- throw new Error(`queryAll: more than ${pageSize} events share created_at=${until} — cannot page without skipping`);
208
- }
209
- break;
276
+ }
277
+ };
278
+ await Promise.all(Array.from({ length: Math.min(concurrency, filters.length) }, () => worker()));
279
+ /*
280
+ The lowest-indexed failure, not the first to reject in wall-clock time.
281
+
282
+ Under `Promise.all` the error a caller sees would otherwise depend on
283
+ which request happened to lose the race, so two runs of the same broken
284
+ read could report different filters. Serial order is the one order that
285
+ is reproducible, and it is what the serial version reported.
286
+ */
287
+ const firstFailure = failures.findIndex((error) => error !== undefined);
288
+ if (firstFailure !== -1)
289
+ throw failures[firstFailure];
290
+ const seen = new Set();
291
+ const out = [];
292
+ for (const events of perFilter) {
293
+ for (const e of events) {
294
+ if (seen.has(e.id))
295
+ continue;
296
+ seen.add(e.id);
297
+ out.push(e);
298
+ }
299
+ }
300
+ return out;
301
+ }
302
+ /**
303
+ * One filter, paged to exhaustion. The sequential half, and it has to be:
304
+ * `until` is a cursor, so page N+1's request is not known until page N has
305
+ * answered. Only the filters are independent, which is why they are the
306
+ * axis {@link Relay.queryAll} parallelises.
307
+ */
308
+ async pageFilter(filter, pageSize, maxPages) {
309
+ const out = [];
310
+ let until;
311
+ for (let page = 0;; page++) {
312
+ if (page >= maxPages) {
313
+ throw new Error(`queryAll: gave up after ${maxPages} pages — refusing to return a partial set`);
314
+ }
315
+ const events = await this.query([
316
+ { ...filter, limit: pageSize, ...(until === undefined ? {} : { until }) },
317
+ ]);
318
+ if (events.length === 0)
319
+ break;
320
+ out.push(...events);
321
+ /*
322
+ A page smaller than one this relay has already delivered is the end of
323
+ the filter, and no confirming round trip is needed to know it.
324
+
325
+ The reason the confirmation existed is real: a page shorter than the
326
+ `limit` we asked for is ambiguous, because the relay clamps to
327
+ `min(requested, ceiling)` and might have clamped at exactly this many.
328
+ But that requires the ceiling to *equal* this page's size — and the
329
+ ceiling is one constant for the relay, already known to be at least
330
+ `observedPageCeiling`. So `n < observedPageCeiling` rules the clamp out
331
+ arithmetically rather than by trusting anything.
332
+
333
+ Deliberately not NIP-11's `limitation.max_limit`. That number is a
334
+ claim: a relay advertising more than it clamps to would make every page
335
+ look short and the first one get mistaken for the whole set, which is
336
+ exactly the SHA-8 bug this loop exists to prevent. An observed page is
337
+ evidence — the relay demonstrably produced it.
338
+
339
+ Conservative before it has evidence: the first filter through a fresh
340
+ `Relay` still pays the confirmation, and so does any page that ties the
341
+ largest seen so far — which means **the widest filter always confirms**,
342
+ since it ties its own bound on every read. So the floor is one request
343
+ per filter plus one, not one per filter.
344
+
345
+ Measured on Ship's `loadAll` against production: **66 requests to 35
346
+ cold and 34 warm**, the same fold event for event, and 8.8 s to 0.9 s
347
+ alongside the concurrency of 0.4.0.
348
+ */
349
+ if (events.length > this.observedPageCeiling)
350
+ this.observedPageCeiling = events.length;
351
+ if (events.length < pageSize && events.length < this.observedPageCeiling)
352
+ break;
353
+ const oldest = Math.min(...events.map((e) => e.created_at));
354
+ if (until !== undefined && oldest >= until) {
355
+ if (events.length >= pageSize) {
356
+ throw new Error(`queryAll: more than ${pageSize} events share created_at=${until} — cannot page without skipping`);
210
357
  }
211
- until = oldest;
358
+ break;
212
359
  }
360
+ until = oldest;
213
361
  }
214
362
  return out;
215
363
  }
@@ -1 +1 @@
1
- {"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -46,12 +46,12 @@
46
46
  /** Bumped by hand with the version in package.json — `test/version.test.ts`
47
47
  * pins the two together. It exists so "the upgrade reached the app" can be
48
48
  * checked by grepping a built bundle rather than by trusting a lockfile. */
49
- export declare const PROTOCOL_VERSION = "0.3.0";
49
+ export declare const PROTOCOL_VERSION = "0.5.0";
50
50
  export { type NostrTag, type UnsignedEvent, type SignedEvent, type Profile, type ThreadRef, type ChannelVisibility, type ChannelKind, type MemberRole, type Label, type ResolutionAction, KIND, MAX_EMOJI_CHARS, MAX_MESSAGE_BYTES, MAX_MENTIONS, MAX_RATIONALE_BYTES, RELAY_AUTH_TOLERANCE_SECS, ASSERTION_SUBTYPE, computeEventId, toNostrSeconds, canonicalChannelName, threadTags, addr, relayAuthUrl, buildProfile, parseProfile, buildCreateChannel, buildDeleteChannel, buildEditChannelMetadata, buildAddMember, buildReaction, buildDeletion, buildMessage, buildResolution, buildFile, buildComponent, buildHighlight, buildUnsignedRelayAuthEvent, } from './events.js';
51
51
  export { type AddressPointer, type EventPointer, encodeNevent, decodeNevent, convertBits, bech32Encode, bech32Decode, encodeNaddr, decodeNaddr, pointerToAddress, addressToPointer, addrToNaddr, naddrToAddr, referenceToPointer, NADDR_RE, findNaddrs, stripNaddrs, } from './nip19.js';
52
52
  export { type AuthEventArgs, TIMESTAMP_TOLERANCE_SECS, normalizeUrl, buildUnsignedAuthEvent, base64, authorizationHeaderFor, authorizationHeader, } from './nip98.js';
53
53
  export { type Signer, type SignerKind, publicKeyFromSecret, signEvent, secretKeySigner, } from './sign.js';
54
- export { type PublishResult, type QueryResult, type RelayHeaders, type RelayOptions, type FetchLike, parsePublishResponse, parseQueryResponse, RELAY_PAGE_CEILING, Relay, } from './bridge.js';
54
+ export { type PublishResult, type QueryResult, type RelayHeaders, type RelayOptions, type FetchLike, type QueryAllOptions, parsePublishResponse, parseQueryResponse, RELAY_PAGE_CEILING, DEFAULT_QUERY_CONCURRENCY, Relay, } from './bridge.js';
55
55
  export { type RelayState, type SocketLike, type RelayCredential, type LiveRelayOptions, type Subscription, type LiveRelay, toWebSocketUrl, parseFrame, createLiveRelay, } from './live.js';
56
56
  export { type ChannelEventHandler, type ChannelSubscription, type ChannelSubscriptions, createChannelSubscriptions, } from './subscriptions.js';
57
57
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH;;4EAE4E;AAC5E,eAAO,MAAM,gBAAgB,UAAU,CAAA;AAEvC,OAAO,EAEL,KAAK,QAAQ,EACb,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,OAAO,EACZ,KAAK,SAAS,EACd,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,KAAK,EACV,KAAK,gBAAgB,EAErB,IAAI,EACJ,eAAe,EACf,iBAAiB,EACjB,YAAY,EACZ,mBAAmB,EACnB,yBAAyB,EACzB,iBAAiB,EAEjB,cAAc,EACd,cAAc,EACd,oBAAoB,EACpB,UAAU,EACV,IAAI,EACJ,YAAY,EAEZ,YAAY,EACZ,YAAY,EACZ,kBAAkB,EAClB,kBAAkB,EAClB,wBAAwB,EACxB,cAAc,EACd,aAAa,EACb,aAAa,EACb,YAAY,EACZ,eAAe,EACf,SAAS,EACT,cAAc,EACd,cAAc,EACd,2BAA2B,GAC5B,MAAM,aAAa,CAAA;AAEpB,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,WAAW,EACX,gBAAgB,EAChB,gBAAgB,EAChB,WAAW,EACX,WAAW,EACX,kBAAkB,EAClB,QAAQ,EACR,UAAU,EACV,WAAW,GACZ,MAAM,YAAY,CAAA;AAEnB,OAAO,EACL,KAAK,aAAa,EAClB,wBAAwB,EACxB,YAAY,EACZ,sBAAsB,EACtB,MAAM,EACN,sBAAsB,EACtB,mBAAmB,GACpB,MAAM,YAAY,CAAA;AAEnB,OAAO,EACL,KAAK,MAAM,EACX,KAAK,UAAU,EACf,mBAAmB,EACnB,SAAS,EACT,eAAe,GAChB,MAAM,WAAW,CAAA;AAElB,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,oBAAoB,EACpB,kBAAkB,EAClB,kBAAkB,EAClB,KAAK,GACN,MAAM,aAAa,CAAA;AAEpB,OAAO,EACL,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,cAAc,EACd,UAAU,EACV,eAAe,GAChB,MAAM,WAAW,CAAA;AAElB,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,0BAA0B,GAC3B,MAAM,oBAAoB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH;;4EAE4E;AAC5E,eAAO,MAAM,gBAAgB,UAAU,CAAA;AAEvC,OAAO,EAEL,KAAK,QAAQ,EACb,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,OAAO,EACZ,KAAK,SAAS,EACd,KAAK,iBAAiB,EACtB,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,KAAK,EACV,KAAK,gBAAgB,EAErB,IAAI,EACJ,eAAe,EACf,iBAAiB,EACjB,YAAY,EACZ,mBAAmB,EACnB,yBAAyB,EACzB,iBAAiB,EAEjB,cAAc,EACd,cAAc,EACd,oBAAoB,EACpB,UAAU,EACV,IAAI,EACJ,YAAY,EAEZ,YAAY,EACZ,YAAY,EACZ,kBAAkB,EAClB,kBAAkB,EAClB,wBAAwB,EACxB,cAAc,EACd,aAAa,EACb,aAAa,EACb,YAAY,EACZ,eAAe,EACf,SAAS,EACT,cAAc,EACd,cAAc,EACd,2BAA2B,GAC5B,MAAM,aAAa,CAAA;AAEpB,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,WAAW,EACX,gBAAgB,EAChB,gBAAgB,EAChB,WAAW,EACX,WAAW,EACX,kBAAkB,EAClB,QAAQ,EACR,UAAU,EACV,WAAW,GACZ,MAAM,YAAY,CAAA;AAEnB,OAAO,EACL,KAAK,aAAa,EAClB,wBAAwB,EACxB,YAAY,EACZ,sBAAsB,EACtB,MAAM,EACN,sBAAsB,EACtB,mBAAmB,GACpB,MAAM,YAAY,CAAA;AAEnB,OAAO,EACL,KAAK,MAAM,EACX,KAAK,UAAU,EACf,mBAAmB,EACnB,SAAS,EACT,eAAe,GAChB,MAAM,WAAW,CAAA;AAElB,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,KAAK,eAAe,EACpB,oBAAoB,EACpB,kBAAkB,EAClB,kBAAkB,EAClB,yBAAyB,EACzB,KAAK,GACN,MAAM,aAAa,CAAA;AAEpB,OAAO,EACL,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACrB,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,cAAc,EACd,UAAU,EACV,eAAe,GAChB,MAAM,WAAW,CAAA;AAElB,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,0BAA0B,GAC3B,MAAM,oBAAoB,CAAA"}
package/dist/index.js CHANGED
@@ -46,7 +46,7 @@
46
46
  /** Bumped by hand with the version in package.json — `test/version.test.ts`
47
47
  * pins the two together. It exists so "the upgrade reached the app" can be
48
48
  * checked by grepping a built bundle rather than by trusting a lockfile. */
49
- export const PROTOCOL_VERSION = '0.3.0';
49
+ export const PROTOCOL_VERSION = '0.5.0';
50
50
  export {
51
51
  // constants
52
52
  KIND, MAX_EMOJI_CHARS, MAX_MESSAGE_BYTES, MAX_MENTIONS, MAX_RATIONALE_BYTES, RELAY_AUTH_TOLERANCE_SECS, ASSERTION_SUBTYPE,
@@ -57,7 +57,7 @@ buildProfile, parseProfile, buildCreateChannel, buildDeleteChannel, buildEditCha
57
57
  export { encodeNevent, decodeNevent, convertBits, bech32Encode, bech32Decode, encodeNaddr, decodeNaddr, pointerToAddress, addressToPointer, addrToNaddr, naddrToAddr, referenceToPointer, NADDR_RE, findNaddrs, stripNaddrs, } from './nip19.js';
58
58
  export { TIMESTAMP_TOLERANCE_SECS, normalizeUrl, buildUnsignedAuthEvent, base64, authorizationHeaderFor, authorizationHeader, } from './nip98.js';
59
59
  export { publicKeyFromSecret, signEvent, secretKeySigner, } from './sign.js';
60
- export { parsePublishResponse, parseQueryResponse, RELAY_PAGE_CEILING, Relay, } from './bridge.js';
60
+ export { parsePublishResponse, parseQueryResponse, RELAY_PAGE_CEILING, DEFAULT_QUERY_CONCURRENCY, Relay, } from './bridge.js';
61
61
  export { toWebSocketUrl, parseFrame, createLiveRelay, } from './live.js';
62
62
  export { createChannelSubscriptions, } from './subscriptions.js';
63
63
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH;;4EAE4E;AAC5E,MAAM,CAAC,MAAM,gBAAgB,GAAG,OAAO,CAAA;AAEvC,OAAO;AAYL,YAAY;AACZ,IAAI,EACJ,eAAe,EACf,iBAAiB,EACjB,YAAY,EACZ,mBAAmB,EACnB,yBAAyB,EACzB,iBAAiB;AACjB,8CAA8C;AAC9C,cAAc,EACd,cAAc,EACd,oBAAoB,EACpB,UAAU,EACV,IAAI,EACJ,YAAY;AACZ,WAAW;AACX,YAAY,EACZ,YAAY,EACZ,kBAAkB,EAClB,kBAAkB,EAClB,wBAAwB,EACxB,cAAc,EACd,aAAa,EACb,aAAa,EACb,YAAY,EACZ,eAAe,EACf,SAAS,EACT,cAAc,EACd,cAAc,EACd,2BAA2B,GAC5B,MAAM,aAAa,CAAA;AAEpB,OAAO,EAGL,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,WAAW,EACX,gBAAgB,EAChB,gBAAgB,EAChB,WAAW,EACX,WAAW,EACX,kBAAkB,EAClB,QAAQ,EACR,UAAU,EACV,WAAW,GACZ,MAAM,YAAY,CAAA;AAEnB,OAAO,EAEL,wBAAwB,EACxB,YAAY,EACZ,sBAAsB,EACtB,MAAM,EACN,sBAAsB,EACtB,mBAAmB,GACpB,MAAM,YAAY,CAAA;AAEnB,OAAO,EAGL,mBAAmB,EACnB,SAAS,EACT,eAAe,GAChB,MAAM,WAAW,CAAA;AAElB,OAAO,EAML,oBAAoB,EACpB,kBAAkB,EAClB,kBAAkB,EAClB,KAAK,GACN,MAAM,aAAa,CAAA;AAEpB,OAAO,EAOL,cAAc,EACd,UAAU,EACV,eAAe,GAChB,MAAM,WAAW,CAAA;AAElB,OAAO,EAIL,0BAA0B,GAC3B,MAAM,oBAAoB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAEH;;4EAE4E;AAC5E,MAAM,CAAC,MAAM,gBAAgB,GAAG,OAAO,CAAA;AAEvC,OAAO;AAYL,YAAY;AACZ,IAAI,EACJ,eAAe,EACf,iBAAiB,EACjB,YAAY,EACZ,mBAAmB,EACnB,yBAAyB,EACzB,iBAAiB;AACjB,8CAA8C;AAC9C,cAAc,EACd,cAAc,EACd,oBAAoB,EACpB,UAAU,EACV,IAAI,EACJ,YAAY;AACZ,WAAW;AACX,YAAY,EACZ,YAAY,EACZ,kBAAkB,EAClB,kBAAkB,EAClB,wBAAwB,EACxB,cAAc,EACd,aAAa,EACb,aAAa,EACb,YAAY,EACZ,eAAe,EACf,SAAS,EACT,cAAc,EACd,cAAc,EACd,2BAA2B,GAC5B,MAAM,aAAa,CAAA;AAEpB,OAAO,EAGL,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,WAAW,EACX,gBAAgB,EAChB,gBAAgB,EAChB,WAAW,EACX,WAAW,EACX,kBAAkB,EAClB,QAAQ,EACR,UAAU,EACV,WAAW,GACZ,MAAM,YAAY,CAAA;AAEnB,OAAO,EAEL,wBAAwB,EACxB,YAAY,EACZ,sBAAsB,EACtB,MAAM,EACN,sBAAsB,EACtB,mBAAmB,GACpB,MAAM,YAAY,CAAA;AAEnB,OAAO,EAGL,mBAAmB,EACnB,SAAS,EACT,eAAe,GAChB,MAAM,WAAW,CAAA;AAElB,OAAO,EAOL,oBAAoB,EACpB,kBAAkB,EAClB,kBAAkB,EAClB,yBAAyB,EACzB,KAAK,GACN,MAAM,aAAa,CAAA;AAEpB,OAAO,EAOL,cAAc,EACd,UAAU,EACV,eAAe,GAChB,MAAM,WAAW,CAAA;AAElB,OAAO,EAIL,0BAA0B,GAC3B,MAAM,oBAAoB,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@estiva-app/protocol",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "The Estiva wire format: Buzz-shaped Nostr event builders, NIP-01 ids, NIP-19 naddr, NIP-98 HTTP auth, and the relay clients. Speaks the protocol; interprets nothing.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/bridge.ts CHANGED
@@ -67,6 +67,50 @@ export interface PublishResult {
67
67
  */
68
68
  export const RELAY_PAGE_CEILING = 1000
69
69
 
70
+ /**
71
+ * How many of a {@link Relay.queryAll} call's filters may be in flight at once.
72
+ *
73
+ * Filters are independent — only paging *within* one is a cursor walk — and
74
+ * running them one after another made a read as slow as the sum of its parts.
75
+ * Measured on Ship's `loadAll` against production: **33 filters, 66 sequential
76
+ * round trips, median 206 ms, 100% of wall clock spent inside `query` one
77
+ * request at a time.** At a concurrency of 8 the same read returned a
78
+ * byte-identical answer in 1.9 s instead of 9.0 s.
79
+ *
80
+ * Bounded rather than unbounded on purpose. A workspace's filter count grows
81
+ * with its Folders, so `Promise.all` over all of them would open a fan-out
82
+ * whose width is set by the *data* — fine at 33, an accidental flood at 500,
83
+ * and the relay is shared. Eight is enough to hide the latency without any
84
+ * caller having to think about it.
85
+ *
86
+ * Pass `concurrency: 1` to restore the strictly serial read.
87
+ *
88
+ * ## This does not buy a caller more relay budget
89
+ *
90
+ * Concurrency changes how fast a read spends its requests, never how many it
91
+ * makes. Buzz meters `POST /query` against `human_api_calls_per_min` — a fixed
92
+ * 60-second window, **default 300, applied to every bridge call whatever tier
93
+ * the caller is** (`enforce_http_admission` in `api/bridge.rs` reads the human
94
+ * limit unconditionally). At 66 requests, one Ship workspace read is 22% of a
95
+ * minute's entire allowance, so no client gets more than ~4.5 of them a minute
96
+ * however it schedules them.
97
+ *
98
+ * A poller therefore has to slow down as its reads get faster, or it simply
99
+ * spends the same budget sooner and starts collecting
100
+ * `rate-limited: quota exceeded`. Making a read cheap is the fix for that;
101
+ * making it parallel is not.
102
+ */
103
+ export const DEFAULT_QUERY_CONCURRENCY = 8
104
+
105
+ export interface QueryAllOptions {
106
+ /** Events per request. Defaults to {@link RELAY_PAGE_CEILING}. */
107
+ pageSize?: number
108
+ /** Give up after this many pages *per filter*, rather than looping forever. */
109
+ maxPages?: number
110
+ /** Filters in flight at once. Defaults to {@link DEFAULT_QUERY_CONCURRENCY}; 1 is serial. */
111
+ concurrency?: number
112
+ }
113
+
70
114
  export function parsePublishResponse(status: number, text: string): PublishResult {
71
115
  let parsed: { accepted?: boolean; event_id?: string; message?: string; error?: string }
72
116
  try {
@@ -161,6 +205,18 @@ export class Relay {
161
205
  private readonly signer: Signer
162
206
  private readonly headers: RelayHeaders
163
207
  private readonly transport: FetchLike | undefined
208
+ /**
209
+ * The largest page this relay has ever handed back, across every call.
210
+ *
211
+ * A *lower bound* on the relay's real clamp, and the only one obtainable
212
+ * without trusting anybody: the relay returned this many events in one
213
+ * response, so its ceiling is at least this large. {@link Relay.pageFilter}
214
+ * uses it to tell a short page apart from a clamped one — see there.
215
+ *
216
+ * Per instance and never reset, so the second read through a long-lived
217
+ * `Relay` is cheaper than the first.
218
+ */
219
+ private observedPageCeiling = 0
164
220
 
165
221
  constructor(url: string, signer: Signer, options: RelayOptions | RelayHeaders = {}) {
166
222
  this.url = url.replace(/\/+$/, '')
@@ -241,44 +297,154 @@ export class Relay {
241
297
  * value of `until` reaches past them without skipping some, and no correct
242
298
  * answer exists from this API — so it says so instead of returning a
243
299
  * plausible subset.
300
+ *
301
+ * **Filters run concurrently, up to {@link DEFAULT_QUERY_CONCURRENCY}.** They
302
+ * are independent of one another; only the pages inside one are a cursor
303
+ * walk. Doing them in sequence made a read cost the sum of every filter's
304
+ * latency, which is what put Ship's workspace read at nine seconds against a
305
+ * five-second poll — long enough that a write's re-read had usually not
306
+ * finished before the next one began.
307
+ *
308
+ * The concurrency is deliberately **not observable in the answer**: results
309
+ * are collected per filter and concatenated in filter order, so the returned
310
+ * array is identical to the serial one, and a failure reports the
311
+ * lowest-indexed filter's error rather than whichever lost the race.
244
312
  */
245
313
  async queryAll(
246
314
  filters: Record<string, unknown>[],
247
- options: { pageSize?: number; maxPages?: number } = {},
315
+ options: QueryAllOptions = {},
248
316
  ): Promise<SignedEvent[]> {
249
317
  const pageSize = options.pageSize ?? RELAY_PAGE_CEILING
250
318
  const maxPages = options.maxPages ?? 100
319
+ const concurrency = Math.max(1, options.concurrency ?? DEFAULT_QUERY_CONCURRENCY)
320
+
321
+ /*
322
+ Per filter, in filter order — not one shared accumulator.
323
+
324
+ Concurrency must not be observable in the answer. Collecting each
325
+ filter's pages into its own slot and concatenating in order at the end
326
+ makes the output byte-identical to the serial version, whatever order the
327
+ responses actually arrive in. Dedup then runs over that ordered
328
+ concatenation, so an id first seen under filter 0 still belongs to filter
329
+ 0's run of events, exactly as before.
330
+ */
331
+ const perFilter: SignedEvent[][] = new Array(filters.length)
332
+ const failures: unknown[] = new Array(filters.length)
333
+
334
+ let next = 0
335
+ let failed = false
336
+
337
+ const worker = async (): Promise<void> => {
338
+ for (;;) {
339
+ // Stop *starting* filters once one has failed. The serial version never
340
+ // reached them at all; this is as close as a fan-out gets, and it keeps
341
+ // a broken read from firing the whole remaining queue at the relay.
342
+ if (failed) return
343
+ const index = next++
344
+ if (index >= filters.length) return
345
+ try {
346
+ perFilter[index] = await this.pageFilter(filters[index], pageSize, maxPages)
347
+ } catch (error) {
348
+ failures[index] = error
349
+ failed = true
350
+ return
351
+ }
352
+ }
353
+ }
354
+
355
+ await Promise.all(
356
+ Array.from({ length: Math.min(concurrency, filters.length) }, () => worker()),
357
+ )
358
+
359
+ /*
360
+ The lowest-indexed failure, not the first to reject in wall-clock time.
361
+
362
+ Under `Promise.all` the error a caller sees would otherwise depend on
363
+ which request happened to lose the race, so two runs of the same broken
364
+ read could report different filters. Serial order is the one order that
365
+ is reproducible, and it is what the serial version reported.
366
+ */
367
+ const firstFailure = failures.findIndex((error) => error !== undefined)
368
+ if (firstFailure !== -1) throw failures[firstFailure]
369
+
251
370
  const seen = new Set<string>()
252
371
  const out: SignedEvent[] = []
372
+ for (const events of perFilter) {
373
+ for (const e of events) {
374
+ if (seen.has(e.id)) continue
375
+ seen.add(e.id)
376
+ out.push(e)
377
+ }
378
+ }
379
+ return out
380
+ }
381
+
382
+ /**
383
+ * One filter, paged to exhaustion. The sequential half, and it has to be:
384
+ * `until` is a cursor, so page N+1's request is not known until page N has
385
+ * answered. Only the filters are independent, which is why they are the
386
+ * axis {@link Relay.queryAll} parallelises.
387
+ */
388
+ private async pageFilter(
389
+ filter: Record<string, unknown>,
390
+ pageSize: number,
391
+ maxPages: number,
392
+ ): Promise<SignedEvent[]> {
393
+ const out: SignedEvent[] = []
394
+ let until: number | undefined
395
+ for (let page = 0; ; page++) {
396
+ if (page >= maxPages) {
397
+ throw new Error(
398
+ `queryAll: gave up after ${maxPages} pages — refusing to return a partial set`,
399
+ )
400
+ }
401
+ const events = await this.query([
402
+ { ...filter, limit: pageSize, ...(until === undefined ? {} : { until }) },
403
+ ])
404
+ if (events.length === 0) break
405
+ out.push(...events)
406
+
407
+ /*
408
+ A page smaller than one this relay has already delivered is the end of
409
+ the filter, and no confirming round trip is needed to know it.
410
+
411
+ The reason the confirmation existed is real: a page shorter than the
412
+ `limit` we asked for is ambiguous, because the relay clamps to
413
+ `min(requested, ceiling)` and might have clamped at exactly this many.
414
+ But that requires the ceiling to *equal* this page's size — and the
415
+ ceiling is one constant for the relay, already known to be at least
416
+ `observedPageCeiling`. So `n < observedPageCeiling` rules the clamp out
417
+ arithmetically rather than by trusting anything.
253
418
 
254
- for (const filter of filters) {
255
- let until: number | undefined
256
- for (let page = 0; ; page++) {
257
- if (page >= maxPages) {
419
+ Deliberately not NIP-11's `limitation.max_limit`. That number is a
420
+ claim: a relay advertising more than it clamps to would make every page
421
+ look short and the first one get mistaken for the whole set, which is
422
+ exactly the SHA-8 bug this loop exists to prevent. An observed page is
423
+ evidence — the relay demonstrably produced it.
424
+
425
+ Conservative before it has evidence: the first filter through a fresh
426
+ `Relay` still pays the confirmation, and so does any page that ties the
427
+ largest seen so far — which means **the widest filter always confirms**,
428
+ since it ties its own bound on every read. So the floor is one request
429
+ per filter plus one, not one per filter.
430
+
431
+ Measured on Ship's `loadAll` against production: **66 requests to 35
432
+ cold and 34 warm**, the same fold event for event, and 8.8 s to 0.9 s
433
+ alongside the concurrency of 0.4.0.
434
+ */
435
+ if (events.length > this.observedPageCeiling) this.observedPageCeiling = events.length
436
+ if (events.length < pageSize && events.length < this.observedPageCeiling) break
437
+
438
+ const oldest = Math.min(...events.map((e) => e.created_at))
439
+ if (until !== undefined && oldest >= until) {
440
+ if (events.length >= pageSize) {
258
441
  throw new Error(
259
- `queryAll: gave up after ${maxPages} pages refusing to return a partial set`,
442
+ `queryAll: more than ${pageSize} events share created_at=${until} cannot page without skipping`,
260
443
  )
261
444
  }
262
- const events = await this.query([
263
- { ...filter, limit: pageSize, ...(until === undefined ? {} : { until }) },
264
- ])
265
- if (events.length === 0) break
266
- for (const e of events) {
267
- if (seen.has(e.id)) continue
268
- seen.add(e.id)
269
- out.push(e)
270
- }
271
- const oldest = Math.min(...events.map((e) => e.created_at))
272
- if (until !== undefined && oldest >= until) {
273
- if (events.length >= pageSize) {
274
- throw new Error(
275
- `queryAll: more than ${pageSize} events share created_at=${until} — cannot page without skipping`,
276
- )
277
- }
278
- break
279
- }
280
- until = oldest
445
+ break
281
446
  }
447
+ until = oldest
282
448
  }
283
449
  return out
284
450
  }
package/src/index.ts CHANGED
@@ -47,7 +47,7 @@
47
47
  /** Bumped by hand with the version in package.json — `test/version.test.ts`
48
48
  * pins the two together. It exists so "the upgrade reached the app" can be
49
49
  * checked by grepping a built bundle rather than by trusting a lockfile. */
50
- export const PROTOCOL_VERSION = '0.3.0'
50
+ export const PROTOCOL_VERSION = '0.5.0'
51
51
 
52
52
  export {
53
53
  // types
@@ -137,9 +137,11 @@ export {
137
137
  type RelayHeaders,
138
138
  type RelayOptions,
139
139
  type FetchLike,
140
+ type QueryAllOptions,
140
141
  parsePublishResponse,
141
142
  parseQueryResponse,
142
143
  RELAY_PAGE_CEILING,
144
+ DEFAULT_QUERY_CONCURRENCY,
143
145
  Relay,
144
146
  } from './bridge.js'
145
147