@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 +111 -0
- package/dist/bridge.d.ts +74 -4
- package/dist/bridge.d.ts.map +1 -1
- package/dist/bridge.js +172 -24
- package/dist/bridge.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/bridge.ts +191 -25
- package/src/index.ts +3 -1
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
|
-
|
|
136
|
-
pageSize?: number;
|
|
137
|
-
maxPages?: number;
|
|
138
|
-
}): Promise<SignedEvent[]>;
|
|
208
|
+
private pageFilter;
|
|
139
209
|
}
|
|
140
210
|
//# sourceMappingURL=bridge.d.ts.map
|
package/dist/bridge.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
358
|
+
break;
|
|
212
359
|
}
|
|
360
|
+
until = oldest;
|
|
213
361
|
}
|
|
214
362
|
return out;
|
|
215
363
|
}
|
package/dist/bridge.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAchD;;;;;;;;GAQG;AACH;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,IAAI,CAAA;AAEtC,MAAM,UAAU,oBAAoB,CAAC,MAAc,EAAE,IAAY;IAC/D,IAAI,MAAmF,CAAA;IACvF,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,yBAAyB,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,CAAA;IACjG,CAAC;IACD,IAAI,MAAM,GAAG,GAAG,IAAI,MAAM,IAAI,GAAG,EAAE,CAAC;QAClC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,CAAA;IACtF,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,KAAK,KAAK,EAAE,CAAC;QAC9B,MAAM,SAAS,GAAG,CAAC,MAAM,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,UAAU,CAAC,YAAY,CAAC,CAAA;QACjE,OAAO,EAAE,EAAE,EAAE,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,CAAA;IAC3G,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,QAAQ,EAAE,UAAU,EAAE,MAAM,EAAE,CAAA;AACnE,CAAC;AASD,wDAAwD;AACxD,MAAM,UAAU,kBAAkB,CAAC,MAAc,EAAE,IAAY;IAC7D,IAAI,MAAM,GAAG,GAAG,IAAI,MAAM,IAAI,GAAG,EAAE,CAAC;QAClC,IAAI,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;QAC/B,IAAI,CAAC;YACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,IAAI,MAAM,CAAA;QAC3C,CAAC;QAAC,MAAM,CAAC;YACP,oBAAoB;QACtB,CAAC;QACD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,CAAA;IAC9D,CAAC;IACD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QAC/B,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,CAAA;IACtF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,yBAAyB,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,EAAE,UAAU,EAAE,MAAM,EAAE,CAAA;IAC7G,CAAC;AACH,CAAC;AAyCD;;;;;;GAMG;AACH,MAAM,OAAO,KAAK;IACC,GAAG,CAAQ;IACX,MAAM,CAAQ;IACd,OAAO,CAAc;IACrB,SAAS,CAAuB;
|
|
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.
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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.
|
|
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,
|
|
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
|
+
"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:
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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:
|
|
442
|
+
`queryAll: more than ${pageSize} events share created_at=${until} — cannot page without skipping`,
|
|
260
443
|
)
|
|
261
444
|
}
|
|
262
|
-
|
|
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.
|
|
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
|
|