@estiva-app/protocol 0.3.0 → 0.4.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,72 @@ 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.4.0 — 2026-09-01
8
+
9
+ **Wire behaviour: unchanged.** No builder, tag layout, id computation or
10
+ signature input moved. `queryAll` returns the same array it always did — see
11
+ below, that is the property the tests are about.
12
+
13
+ - **`queryAll` runs its filters concurrently**, bounded by the new
14
+ `DEFAULT_QUERY_CONCURRENCY` (8) and overridable per call via
15
+ `QueryAllOptions.concurrency`. `concurrency: 1` is the old serial read
16
+ exactly.
17
+
18
+ Filters handed to one call are independent; only the pages *inside* a filter
19
+ are a cursor walk, and those stay sequential because page N+1's `until` is
20
+ not known until page N has answered. Running the filters in sequence made a
21
+ read cost the sum of every filter's latency. Measured on Ship's `loadAll`
22
+ against production: **33 filters, 66 round trips, median 206 ms, 100% of wall
23
+ clock spent inside `query` one request at a time.** The same read, same relay,
24
+ same credentials, through this build: **9.0 s → 1.68 s, and the fold compares
25
+ equal event for event** — every project and issue address with its status,
26
+ every change id, every conversation id.
27
+
28
+ The concurrency is deliberately not observable in the answer. Results are
29
+ collected per filter and concatenated in filter order, and a failure reports
30
+ the **lowest-indexed** filter's error rather than whichever request lost the
31
+ race — so a broken read names the same filter twice running.
32
+
33
+ - **What this does not buy you: relay budget.** Buzz meters `POST /query`
34
+ against `human_api_calls_per_min` — a fixed 60-second window, default 300,
35
+ applied to every bridge call whatever tier the caller is. One Ship workspace
36
+ read is 66 of those. Concurrency changes when a read spends its requests,
37
+ never how many, so a poller that simply reads more often now spends the same
38
+ allowance sooner. Documented at `DEFAULT_QUERY_CONCURRENCY` because it is the
39
+ first thing a caller will get wrong.
40
+
41
+ Additive, and behaviour under a caller's existing call changes (requests now
42
+ overlap), so a MINOR: ADR 0002 §4b puts the break on MINOR within `0.x`.
43
+
44
+ ## 0.3.0 — 2026-09-01
45
+
46
+ *Backfilled 2026-09-01. This release shipped without an entry, and so did 0.2.0
47
+ below — the file's own rule is that every release answers the wire question, and
48
+ twice it went unanswered. Recorded now from the release commits rather than left
49
+ as a gap.*
50
+
51
+ **Wire behaviour: unchanged.** NIP-19 is an encoding of a pointer, not of an
52
+ event; nothing an app publishes moved.
53
+
54
+ - **`nevent`** — `EventPointer`, `encodeNevent`, `decodeNevent`. An event that
55
+ is not addressable had no reference form at all, so a plain `kind:9` message
56
+ could be resolved by nothing. TLV type 0 carries the event id as 32 raw
57
+ bytes, where `naddr` carries a UTF-8 `d`; that difference is the whole of the
58
+ codec.
59
+
60
+ ## 0.2.0 — 2026-08-28
61
+
62
+ *Backfilled 2026-09-01, from the release commit — see the note under 0.3.0.*
63
+
64
+ **Wire behaviour: unchanged.** A read-path fix; no published bytes moved.
65
+
66
+ - **`RELAY_PAGE_CEILING`, and `queryAll` pages instead of truncating.** Buzz
67
+ clamps a REQ to its advertised NIP-11 `max_limit`, which halved from 10000 to
68
+ 1000, and NIP-01 has no truncation signal — so a caller asking for 2000 got
69
+ 1000 and no indication why. `limit: 2000` was a literal at four call sites
70
+ across two apps and the agent, none of which could know when the relay changed
71
+ it.
72
+
7
73
  ## 0.1.2 — 2026-08-27
8
74
 
9
75
  **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;
@@ -131,10 +173,26 @@ export declare class Relay {
131
173
  * value of `until` reaches past them without skipping some, and no correct
132
174
  * answer exists from this API — so it says so instead of returning a
133
175
  * plausible subset.
176
+ *
177
+ * **Filters run concurrently, up to {@link DEFAULT_QUERY_CONCURRENCY}.** They
178
+ * are independent of one another; only the pages inside one are a cursor
179
+ * walk. Doing them in sequence made a read cost the sum of every filter's
180
+ * latency, which is what put Ship's workspace read at nine seconds against a
181
+ * five-second poll — long enough that a write's re-read had usually not
182
+ * finished before the next one began.
183
+ *
184
+ * The concurrency is deliberately **not observable in the answer**: results
185
+ * are collected per filter and concatenated in filter order, so the returned
186
+ * array is identical to the serial one, and a failure reports the
187
+ * lowest-indexed filter's error rather than whichever lost the race.
188
+ */
189
+ queryAll(filters: Record<string, unknown>[], options?: QueryAllOptions): Promise<SignedEvent[]>;
190
+ /**
191
+ * One filter, paged to exhaustion. The sequential half, and it has to be:
192
+ * `until` is a cursor, so page N+1's request is not known until page N has
193
+ * answered. Only the filters are independent, which is why they are the
194
+ * axis {@link Relay.queryAll} parallelises.
134
195
  */
135
- queryAll(filters: Record<string, unknown>[], options?: {
136
- pageSize?: number;
137
- maxPages?: number;
138
- }): Promise<SignedEvent[]>;
196
+ private pageFilter;
139
197
  }
140
198
  //# 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 {
@@ -178,38 +212,108 @@ export class Relay {
178
212
  * value of `until` reaches past them without skipping some, and no correct
179
213
  * answer exists from this API — so it says so instead of returning a
180
214
  * plausible subset.
215
+ *
216
+ * **Filters run concurrently, up to {@link DEFAULT_QUERY_CONCURRENCY}.** They
217
+ * are independent of one another; only the pages inside one are a cursor
218
+ * walk. Doing them in sequence made a read cost the sum of every filter's
219
+ * latency, which is what put Ship's workspace read at nine seconds against a
220
+ * five-second poll — long enough that a write's re-read had usually not
221
+ * finished before the next one began.
222
+ *
223
+ * The concurrency is deliberately **not observable in the answer**: results
224
+ * are collected per filter and concatenated in filter order, so the returned
225
+ * array is identical to the serial one, and a failure reports the
226
+ * lowest-indexed filter's error rather than whichever lost the race.
181
227
  */
182
228
  async queryAll(filters, options = {}) {
183
229
  const pageSize = options.pageSize ?? RELAY_PAGE_CEILING;
184
230
  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`);
231
+ const concurrency = Math.max(1, options.concurrency ?? DEFAULT_QUERY_CONCURRENCY);
232
+ /*
233
+ Per filter, in filter order not one shared accumulator.
234
+
235
+ Concurrency must not be observable in the answer. Collecting each
236
+ filter's pages into its own slot and concatenating in order at the end
237
+ makes the output byte-identical to the serial version, whatever order the
238
+ responses actually arrive in. Dedup then runs over that ordered
239
+ concatenation, so an id first seen under filter 0 still belongs to filter
240
+ 0's run of events, exactly as before.
241
+ */
242
+ const perFilter = new Array(filters.length);
243
+ const failures = new Array(filters.length);
244
+ let next = 0;
245
+ let failed = false;
246
+ const worker = async () => {
247
+ for (;;) {
248
+ // Stop *starting* filters once one has failed. The serial version never
249
+ // reached them at all; this is as close as a fan-out gets, and it keeps
250
+ // a broken read from firing the whole remaining queue at the relay.
251
+ if (failed)
252
+ return;
253
+ const index = next++;
254
+ if (index >= filters.length)
255
+ return;
256
+ try {
257
+ perFilter[index] = await this.pageFilter(filters[index], pageSize, maxPages);
192
258
  }
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);
259
+ catch (error) {
260
+ failures[index] = error;
261
+ failed = true;
262
+ return;
203
263
  }
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;
264
+ }
265
+ };
266
+ await Promise.all(Array.from({ length: Math.min(concurrency, filters.length) }, () => worker()));
267
+ /*
268
+ The lowest-indexed failure, not the first to reject in wall-clock time.
269
+
270
+ Under `Promise.all` the error a caller sees would otherwise depend on
271
+ which request happened to lose the race, so two runs of the same broken
272
+ read could report different filters. Serial order is the one order that
273
+ is reproducible, and it is what the serial version reported.
274
+ */
275
+ const firstFailure = failures.findIndex((error) => error !== undefined);
276
+ if (firstFailure !== -1)
277
+ throw failures[firstFailure];
278
+ const seen = new Set();
279
+ const out = [];
280
+ for (const events of perFilter) {
281
+ for (const e of events) {
282
+ if (seen.has(e.id))
283
+ continue;
284
+ seen.add(e.id);
285
+ out.push(e);
286
+ }
287
+ }
288
+ return out;
289
+ }
290
+ /**
291
+ * One filter, paged to exhaustion. The sequential half, and it has to be:
292
+ * `until` is a cursor, so page N+1's request is not known until page N has
293
+ * answered. Only the filters are independent, which is why they are the
294
+ * axis {@link Relay.queryAll} parallelises.
295
+ */
296
+ async pageFilter(filter, pageSize, maxPages) {
297
+ const out = [];
298
+ let until;
299
+ for (let page = 0;; page++) {
300
+ if (page >= maxPages) {
301
+ throw new Error(`queryAll: gave up after ${maxPages} pages — refusing to return a partial set`);
302
+ }
303
+ const events = await this.query([
304
+ { ...filter, limit: pageSize, ...(until === undefined ? {} : { until }) },
305
+ ]);
306
+ if (events.length === 0)
307
+ break;
308
+ out.push(...events);
309
+ const oldest = Math.min(...events.map((e) => e.created_at));
310
+ if (until !== undefined && oldest >= until) {
311
+ if (events.length >= pageSize) {
312
+ throw new Error(`queryAll: more than ${pageSize} events share created_at=${until} — cannot page without skipping`);
210
313
  }
211
- until = oldest;
314
+ break;
212
315
  }
316
+ until = oldest;
213
317
  }
214
318
  return out;
215
319
  }
@@ -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.4.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.4.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.4.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 {
@@ -241,44 +285,122 @@ export class Relay {
241
285
  * value of `until` reaches past them without skipping some, and no correct
242
286
  * answer exists from this API — so it says so instead of returning a
243
287
  * plausible subset.
288
+ *
289
+ * **Filters run concurrently, up to {@link DEFAULT_QUERY_CONCURRENCY}.** They
290
+ * are independent of one another; only the pages inside one are a cursor
291
+ * walk. Doing them in sequence made a read cost the sum of every filter's
292
+ * latency, which is what put Ship's workspace read at nine seconds against a
293
+ * five-second poll — long enough that a write's re-read had usually not
294
+ * finished before the next one began.
295
+ *
296
+ * The concurrency is deliberately **not observable in the answer**: results
297
+ * are collected per filter and concatenated in filter order, so the returned
298
+ * array is identical to the serial one, and a failure reports the
299
+ * lowest-indexed filter's error rather than whichever lost the race.
244
300
  */
245
301
  async queryAll(
246
302
  filters: Record<string, unknown>[],
247
- options: { pageSize?: number; maxPages?: number } = {},
303
+ options: QueryAllOptions = {},
248
304
  ): Promise<SignedEvent[]> {
249
305
  const pageSize = options.pageSize ?? RELAY_PAGE_CEILING
250
306
  const maxPages = options.maxPages ?? 100
307
+ const concurrency = Math.max(1, options.concurrency ?? DEFAULT_QUERY_CONCURRENCY)
308
+
309
+ /*
310
+ Per filter, in filter order — not one shared accumulator.
311
+
312
+ Concurrency must not be observable in the answer. Collecting each
313
+ filter's pages into its own slot and concatenating in order at the end
314
+ makes the output byte-identical to the serial version, whatever order the
315
+ responses actually arrive in. Dedup then runs over that ordered
316
+ concatenation, so an id first seen under filter 0 still belongs to filter
317
+ 0's run of events, exactly as before.
318
+ */
319
+ const perFilter: SignedEvent[][] = new Array(filters.length)
320
+ const failures: unknown[] = new Array(filters.length)
321
+
322
+ let next = 0
323
+ let failed = false
324
+
325
+ const worker = async (): Promise<void> => {
326
+ for (;;) {
327
+ // Stop *starting* filters once one has failed. The serial version never
328
+ // reached them at all; this is as close as a fan-out gets, and it keeps
329
+ // a broken read from firing the whole remaining queue at the relay.
330
+ if (failed) return
331
+ const index = next++
332
+ if (index >= filters.length) return
333
+ try {
334
+ perFilter[index] = await this.pageFilter(filters[index], pageSize, maxPages)
335
+ } catch (error) {
336
+ failures[index] = error
337
+ failed = true
338
+ return
339
+ }
340
+ }
341
+ }
342
+
343
+ await Promise.all(
344
+ Array.from({ length: Math.min(concurrency, filters.length) }, () => worker()),
345
+ )
346
+
347
+ /*
348
+ The lowest-indexed failure, not the first to reject in wall-clock time.
349
+
350
+ Under `Promise.all` the error a caller sees would otherwise depend on
351
+ which request happened to lose the race, so two runs of the same broken
352
+ read could report different filters. Serial order is the one order that
353
+ is reproducible, and it is what the serial version reported.
354
+ */
355
+ const firstFailure = failures.findIndex((error) => error !== undefined)
356
+ if (firstFailure !== -1) throw failures[firstFailure]
357
+
251
358
  const seen = new Set<string>()
252
359
  const out: SignedEvent[] = []
360
+ for (const events of perFilter) {
361
+ for (const e of events) {
362
+ if (seen.has(e.id)) continue
363
+ seen.add(e.id)
364
+ out.push(e)
365
+ }
366
+ }
367
+ return out
368
+ }
253
369
 
254
- for (const filter of filters) {
255
- let until: number | undefined
256
- for (let page = 0; ; page++) {
257
- if (page >= maxPages) {
370
+ /**
371
+ * One filter, paged to exhaustion. The sequential half, and it has to be:
372
+ * `until` is a cursor, so page N+1's request is not known until page N has
373
+ * answered. Only the filters are independent, which is why they are the
374
+ * axis {@link Relay.queryAll} parallelises.
375
+ */
376
+ private async pageFilter(
377
+ filter: Record<string, unknown>,
378
+ pageSize: number,
379
+ maxPages: number,
380
+ ): Promise<SignedEvent[]> {
381
+ const out: SignedEvent[] = []
382
+ let until: number | undefined
383
+ for (let page = 0; ; page++) {
384
+ if (page >= maxPages) {
385
+ throw new Error(
386
+ `queryAll: gave up after ${maxPages} pages — refusing to return a partial set`,
387
+ )
388
+ }
389
+ const events = await this.query([
390
+ { ...filter, limit: pageSize, ...(until === undefined ? {} : { until }) },
391
+ ])
392
+ if (events.length === 0) break
393
+ out.push(...events)
394
+ const oldest = Math.min(...events.map((e) => e.created_at))
395
+ if (until !== undefined && oldest >= until) {
396
+ if (events.length >= pageSize) {
258
397
  throw new Error(
259
- `queryAll: gave up after ${maxPages} pages refusing to return a partial set`,
398
+ `queryAll: more than ${pageSize} events share created_at=${until} cannot page without skipping`,
260
399
  )
261
400
  }
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
401
+ break
281
402
  }
403
+ until = oldest
282
404
  }
283
405
  return out
284
406
  }
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.4.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