@qntx/nostr 0.1.0 → 0.2.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.
Files changed (185) hide show
  1. package/dist/abort-Ba8tDrOI.mjs +38 -0
  2. package/dist/abort-Ba8tDrOI.mjs.map +1 -0
  3. package/dist/{builder-BphJa-Y-.d.mts → builder-BWn55Paf.d.mts} +20 -14
  4. package/dist/{builder-q0VsvnYE.mjs → builder-CyrGKRVP.mjs} +32 -16
  5. package/dist/builder-CyrGKRVP.mjs.map +1 -0
  6. package/dist/{client-Bd8B1mYm.mjs → client-BWsX9Cv1.mjs} +354 -195
  7. package/dist/client-BWsX9Cv1.mjs.map +1 -0
  8. package/dist/client.d.mts +1 -1
  9. package/dist/client.mjs +1 -1
  10. package/dist/core.d.mts +8 -8
  11. package/dist/core.mjs +8 -9
  12. package/dist/error-BW7re1UN.d.mts +9 -0
  13. package/dist/error-B__OMBRY.mjs +14 -0
  14. package/dist/error-B__OMBRY.mjs.map +1 -0
  15. package/dist/{error-WEFS-R-4.d.mts → error-CR8Urc84.d.mts} +20 -8
  16. package/dist/{error-CwBxle6N.mjs → error-DrEIM7FO.mjs} +5 -3
  17. package/dist/error-DrEIM7FO.mjs.map +1 -0
  18. package/dist/{event-CzoQEFMF.mjs → event-D8Nfoh4K.mjs} +40 -32
  19. package/dist/event-D8Nfoh4K.mjs.map +1 -0
  20. package/dist/{event-B5D2jxt_.d.mts → event-fU3Y01PR.d.mts} +23 -20
  21. package/dist/{filter-BxwjEkZp.mjs → filter-BQl3GlHt.mjs} +42 -35
  22. package/dist/filter-BQl3GlHt.mjs.map +1 -0
  23. package/dist/filter-C0IMByE3.d.mts +37 -0
  24. package/dist/{gossip-C5RVB1UB.d.mts → gossip-BRQwIM_q.d.mts} +25 -23
  25. package/dist/{gossip-3xkRRCm_.mjs → gossip-oloDMWEj.mjs} +52 -34
  26. package/dist/gossip-oloDMWEj.mjs.map +1 -0
  27. package/dist/gossip.d.mts +1 -1
  28. package/dist/gossip.mjs +1 -1
  29. package/dist/{http-6vevbBlu.d.mts → http-C4HwIu4B.d.mts} +7 -7
  30. package/dist/{http-D2rhokZo.mjs → http-CuazNRIP.mjs} +10 -6
  31. package/dist/http-CuazNRIP.mjs.map +1 -0
  32. package/dist/index-B425CAcQ.d.mts +328 -0
  33. package/dist/index-BVyfUBuG.d.mts +181 -0
  34. package/dist/{index-BYMnhRIC.d.mts → index-Ba7bJ7DD.d.mts} +68 -50
  35. package/dist/{index-DaRyjo7O.d.mts → index-Boe8FshM.d.mts} +13 -7
  36. package/dist/index-CUIvFQdE.d.mts +73 -0
  37. package/dist/index-JdaS_uce.d.mts +207 -0
  38. package/dist/index.d.mts +22 -20
  39. package/dist/index.mjs +19 -18
  40. package/dist/{key-Dn-aX4x4.mjs → key-Bbtys6o7.mjs} +22 -14
  41. package/dist/key-Bbtys6o7.mjs.map +1 -0
  42. package/dist/{key-BiGxGLhg.d.mts → key-BlsYhN1V.d.mts} +9 -6
  43. package/dist/{kind-BLl4cLOc.mjs → kind-Cxwhhkx8.mjs} +2 -1
  44. package/dist/kind-Cxwhhkx8.mjs.map +1 -0
  45. package/dist/{loaders-BNQszDAn.mjs → loaders-o6YXB_Ri.mjs} +329 -305
  46. package/dist/loaders-o6YXB_Ri.mjs.map +1 -0
  47. package/dist/loaders.d.mts +3 -3
  48. package/dist/loaders.mjs +2 -2
  49. package/dist/memory-C2EVx0_O.mjs +46 -0
  50. package/dist/memory-C2EVx0_O.mjs.map +1 -0
  51. package/dist/{memory-BCmhZPek.mjs → memory-index-Do1mz0WF.mjs} +212 -82
  52. package/dist/memory-index-Do1mz0WF.mjs.map +1 -0
  53. package/dist/{message-C_Uj-Y-c.mjs → message-DcY6Fi9o.mjs} +38 -24
  54. package/dist/message-DcY6Fi9o.mjs.map +1 -0
  55. package/dist/{message-DJC-Jz6T.d.mts → message-iGPIqnn0.d.mts} +15 -8
  56. package/dist/{nip17-BWq-Kfq5.d.mts → nip17-DNEkz8rM.d.mts} +18 -17
  57. package/dist/{nip19-D7905oIv.d.mts → nip19-BevCJZsp.d.mts} +23 -3
  58. package/dist/{nip46-C_DoxiS_.d.mts → nip46-DDV4dC56.d.mts} +13 -12
  59. package/dist/{nip59-DljW_sYh.d.mts → nip59-zQgdLcEr.d.mts} +28 -25
  60. package/dist/{nip65-Di5hZRuV.d.mts → nip65-DBfh2smt.d.mts} +8 -3
  61. package/dist/{nip77-ByxlgoQO.d.mts → nip77-DH4RhS5r.d.mts} +9 -8
  62. package/dist/nips/blossom.d.mts +37 -37
  63. package/dist/nips/blossom.mjs +67 -52
  64. package/dist/nips/blossom.mjs.map +1 -1
  65. package/dist/nips/nip04.d.mts +2 -3
  66. package/dist/nips/nip04.mjs +6 -10
  67. package/dist/nips/nip04.mjs.map +1 -1
  68. package/dist/nips/nip05.d.mts +37 -43
  69. package/dist/nips/nip05.mjs +57 -57
  70. package/dist/nips/nip05.mjs.map +1 -1
  71. package/dist/nips/nip10.d.mts +18 -19
  72. package/dist/nips/nip10.mjs +76 -61
  73. package/dist/nips/nip10.mjs.map +1 -1
  74. package/dist/nips/nip11.d.mts +9 -8
  75. package/dist/nips/nip11.mjs +14 -11
  76. package/dist/nips/nip11.mjs.map +1 -1
  77. package/dist/nips/nip13.d.mts +9 -9
  78. package/dist/nips/nip13.mjs +15 -17
  79. package/dist/nips/nip13.mjs.map +1 -1
  80. package/dist/nips/nip17.d.mts +1 -1
  81. package/dist/nips/nip17.mjs +17 -12
  82. package/dist/nips/nip17.mjs.map +1 -1
  83. package/dist/nips/nip19.d.mts +1 -1
  84. package/dist/nips/nip19.mjs +52 -29
  85. package/dist/nips/nip19.mjs.map +1 -1
  86. package/dist/nips/nip21.d.mts +5 -6
  87. package/dist/nips/nip21.mjs +8 -7
  88. package/dist/nips/nip21.mjs.map +1 -1
  89. package/dist/nips/nip27.d.mts +7 -8
  90. package/dist/nips/nip27.mjs +26 -25
  91. package/dist/nips/nip27.mjs.map +1 -1
  92. package/dist/nips/nip42.d.mts +5 -6
  93. package/dist/nips/nip42.mjs +3 -3
  94. package/dist/nips/nip42.mjs.map +1 -1
  95. package/dist/nips/nip44.d.mts +24 -7
  96. package/dist/nips/nip44.mjs +25 -15
  97. package/dist/nips/nip44.mjs.map +1 -1
  98. package/dist/nips/nip46.d.mts +1 -1
  99. package/dist/nips/nip46.mjs +52 -49
  100. package/dist/nips/nip46.mjs.map +1 -1
  101. package/dist/nips/nip49.d.mts +8 -7
  102. package/dist/nips/nip49.mjs +16 -12
  103. package/dist/nips/nip49.mjs.map +1 -1
  104. package/dist/nips/nip51.d.mts +25 -27
  105. package/dist/nips/nip51.mjs +47 -36
  106. package/dist/nips/nip51.mjs.map +1 -1
  107. package/dist/nips/nip57.d.mts +12 -13
  108. package/dist/nips/nip57.mjs +35 -38
  109. package/dist/nips/nip57.mjs.map +1 -1
  110. package/dist/nips/nip59.d.mts +1 -1
  111. package/dist/nips/nip59.mjs +14 -18
  112. package/dist/nips/nip59.mjs.map +1 -1
  113. package/dist/nips/nip65.d.mts +1 -1
  114. package/dist/nips/nip65.mjs +11 -8
  115. package/dist/nips/nip65.mjs.map +1 -1
  116. package/dist/nips/nip77.d.mts +1 -1
  117. package/dist/nips/nip77.mjs +56 -34
  118. package/dist/nips/nip77.mjs.map +1 -1
  119. package/dist/nips/nip96.d.mts +21 -12
  120. package/dist/nips/nip96.mjs +59 -36
  121. package/dist/nips/nip96.mjs.map +1 -1
  122. package/dist/nips/nip98.d.mts +12 -12
  123. package/dist/nips/nip98.mjs +18 -19
  124. package/dist/nips/nip98.mjs.map +1 -1
  125. package/dist/{pool-C_j1OmBB.mjs → pool-XmAGaufS.mjs} +611 -487
  126. package/dist/pool-XmAGaufS.mjs.map +1 -0
  127. package/dist/pool-tEr7c5bG.d.mts +302 -0
  128. package/dist/reactive-DDlThDSZ.mjs +324 -0
  129. package/dist/reactive-DDlThDSZ.mjs.map +1 -0
  130. package/dist/reactive-DFpqtIf_.d.mts +66 -0
  131. package/dist/relay.d.mts +5 -4
  132. package/dist/relay.mjs +2 -2
  133. package/dist/report-CHld4uWj.mjs +32 -0
  134. package/dist/report-CHld4uWj.mjs.map +1 -0
  135. package/dist/{signer-CZ6K6FUc.mjs → signer-CKR9EHce.mjs} +107 -72
  136. package/dist/signer-CKR9EHce.mjs.map +1 -0
  137. package/dist/signer.d.mts +3 -3
  138. package/dist/signer.mjs +3 -2
  139. package/dist/storage-xXxLGCVb.mjs +1376 -0
  140. package/dist/storage-xXxLGCVb.mjs.map +1 -0
  141. package/dist/storage.d.mts +4 -4
  142. package/dist/storage.mjs +5 -4
  143. package/dist/store.d.mts +2 -0
  144. package/dist/store.mjs +2 -0
  145. package/dist/testing.d.mts +125 -0
  146. package/dist/testing.mjs +957 -0
  147. package/dist/testing.mjs.map +1 -0
  148. package/dist/types-2pCUprRU.d.mts +47 -0
  149. package/dist/types-CRQEk0Oi.d.mts +19 -0
  150. package/dist/util-LfQirgDk.mjs +113 -0
  151. package/dist/util-LfQirgDk.mjs.map +1 -0
  152. package/dist/wasm.d.mts +4 -4
  153. package/dist/wasm.mjs +54 -55
  154. package/dist/wasm.mjs.map +1 -1
  155. package/dist/websocket-CmeqvAkp.d.mts +27 -0
  156. package/package.json +31 -9
  157. package/dist/builder-q0VsvnYE.mjs.map +0 -1
  158. package/dist/client-Bd8B1mYm.mjs.map +0 -1
  159. package/dist/error-B49rbDzT.d.mts +0 -7
  160. package/dist/error-CwBxle6N.mjs.map +0 -1
  161. package/dist/error-DZEs5OuZ.mjs +0 -24
  162. package/dist/error-DZEs5OuZ.mjs.map +0 -1
  163. package/dist/event-CzoQEFMF.mjs.map +0 -1
  164. package/dist/filter-BxwjEkZp.mjs.map +0 -1
  165. package/dist/filter-DUEKP3q1.d.mts +0 -40
  166. package/dist/gossip-3xkRRCm_.mjs.map +0 -1
  167. package/dist/http-D2rhokZo.mjs.map +0 -1
  168. package/dist/index-C0C4e-Gn.d.mts +0 -55
  169. package/dist/index-DgOugrpF.d.mts +0 -275
  170. package/dist/index-EwL6fHLo.d.mts +0 -267
  171. package/dist/index-k2YhCgsh.d.mts +0 -57
  172. package/dist/key-Dn-aX4x4.mjs.map +0 -1
  173. package/dist/kind-BLl4cLOc.mjs.map +0 -1
  174. package/dist/loaders-BNQszDAn.mjs.map +0 -1
  175. package/dist/memory-BCmhZPek.mjs.map +0 -1
  176. package/dist/message-C_Uj-Y-c.mjs.map +0 -1
  177. package/dist/pool-C_j1OmBB.mjs.map +0 -1
  178. package/dist/pool-DhRi7l-b.d.mts +0 -297
  179. package/dist/signer-CZ6K6FUc.mjs.map +0 -1
  180. package/dist/storage-DOy0iLRE.mjs +0 -870
  181. package/dist/storage-DOy0iLRE.mjs.map +0 -1
  182. package/dist/types-BzyADqnw.d.mts +0 -19
  183. package/dist/types-Dn24TPFs.d.mts +0 -43
  184. package/dist/util-6tfLQ9JF.mjs +0 -73
  185. package/dist/util-6tfLQ9JF.mjs.map +0 -1
@@ -1,21 +1,33 @@
1
- import { s as normalizeURL } from "./util-6tfLQ9JF.mjs";
2
- import { a as NostrError } from "./error-DZEs5OuZ.mjs";
3
- import { t as Kind } from "./kind-BLl4cLOc.mjs";
4
- import { d as sortedEvents } from "./event-CzoQEFMF.mjs";
5
- import { t as EventBuilder } from "./builder-q0VsvnYE.mjs";
6
- import { n as canonicalizeFilters, t as canonicalizeFilter } from "./filter-BxwjEkZp.mjs";
7
- import { n as fanIn, r as fetchRouted, t as Pool } from "./pool-C_j1OmBB.mjs";
1
+ import { g as NostrError, l as normalizeURL } from "./util-LfQirgDk.mjs";
2
+ import { t as Kind } from "./kind-Cxwhhkx8.mjs";
3
+ import { f as sortedEvents, o as itemCompare } from "./event-D8Nfoh4K.mjs";
4
+ import { t as EventBuilder } from "./builder-CyrGKRVP.mjs";
5
+ import { n as canonicalizeFilters, o as matchFilters, t as canonicalizeFilter } from "./filter-BQl3GlHt.mjs";
6
+ import { t as NoSignerError } from "./error-B__OMBRY.mjs";
7
+ import { t as invokeSafely } from "./report-CHld4uWj.mjs";
8
+ import { n as fanIn, r as fetchRouted, t as Pool } from "./pool-XmAGaufS.mjs";
9
+ import { r as throwIfAborted } from "./abort-Ba8tDrOI.mjs";
8
10
  import { storageFromItems } from "./nips/nip77.mjs";
9
11
  import { isGiftWrapKind, requireNip59Crypto, unwrap } from "./nips/nip59.mjs";
10
12
  import { Nip17Error, buildChatMessageRumor, dmRelayListEventBuilder, normalizeRecipients, requireDmRelays, wrapDirectMessage } from "./nips/nip17.mjs";
11
- import { t as Gossip } from "./gossip-3xkRRCm_.mjs";
12
- import { i as createOutboxFeed, t as createLoaders } from "./loaders-BNQszDAn.mjs";
13
- import { n as toStorageError } from "./error-CwBxle6N.mjs";
14
- import { t as MemoryEventStore } from "./memory-BCmhZPek.mjs";
13
+ import { t as Gossip } from "./gossip-oloDMWEj.mjs";
14
+ import { i as createOutboxFeed, t as createLoaders } from "./loaders-o6YXB_Ri.mjs";
15
+ import { n as toStorageError } from "./error-DrEIM7FO.mjs";
16
+ import { t as MemoryIndex } from "./memory-index-Do1mz0WF.mjs";
17
+ import { t as MemoryEventStore } from "./memory-C2EVx0_O.mjs";
18
+ import { t as ReactiveEventStore } from "./reactive-DDlThDSZ.mjs";
15
19
  //#region src/client/builder.ts
16
- /** Fluent constructor for {@link Client}. */
20
+ /**
21
+ * Fluent constructor for {@link Client}. The constructor is supplied by {@link Client.builder} so
22
+ * this module never imports the `Client` value, keeping the client/builder dependency
23
+ * one-directional.
24
+ */
17
25
  var ClientBuilder = class {
18
26
  #opts = {};
27
+ #newClient;
28
+ constructor(newClient) {
29
+ this.#newClient = newClient;
30
+ }
19
31
  signer(signer) {
20
32
  this.#opts = {
21
33
  ...this.#opts,
@@ -72,6 +84,46 @@ var ClientBuilder = class {
72
84
  };
73
85
  return this;
74
86
  }
87
+ /** Allow `ws://` relays. Default false. */
88
+ allowInsecure(allow) {
89
+ this.#opts = {
90
+ ...this.#opts,
91
+ allowInsecure: allow
92
+ };
93
+ return this;
94
+ }
95
+ /** `ws://` URLs allowed despite `allowInsecure` being off. */
96
+ trustedInsecureUrls(urls) {
97
+ this.#opts = {
98
+ ...this.#opts,
99
+ trustedInsecureUrls: urls
100
+ };
101
+ return this;
102
+ }
103
+ /** Close idle relays after this many ms. Unset = disabled. */
104
+ idleTimeoutMs(ms) {
105
+ this.#opts = {
106
+ ...this.#opts,
107
+ idleTimeoutMs: ms
108
+ };
109
+ return this;
110
+ }
111
+ /** Soft cap on connected non-pinned relays. */
112
+ maxRelays(n) {
113
+ this.#opts = {
114
+ ...this.#opts,
115
+ maxRelays: n
116
+ };
117
+ return this;
118
+ }
119
+ /** Relays never closed by idle cleanup or `maxRelays` eviction. */
120
+ pinnedUrls(urls) {
121
+ this.#opts = {
122
+ ...this.#opts,
123
+ pinnedUrls: urls
124
+ };
125
+ return this;
126
+ }
75
127
  enableReconnect(enabled) {
76
128
  this.#opts = {
77
129
  ...this.#opts,
@@ -101,8 +153,8 @@ var ClientBuilder = class {
101
153
  return this;
102
154
  }
103
155
  /**
104
- * Local event store. Defaults to {@link MemoryEventStore}.
105
- * Browser apps that want persistence must pass {@link IndexedDbEventStore} and `await open()`.
156
+ * Local event store. Defaults to {@link MemoryEventStore}. Browser apps that want persistence must
157
+ * pass {@link IndexedDbEventStore} and `await open()`.
106
158
  */
107
159
  storage(store) {
108
160
  this.#opts = {
@@ -111,6 +163,17 @@ var ClientBuilder = class {
111
163
  };
112
164
  return this;
113
165
  }
166
+ /**
167
+ * Synchronous reactive index mirroring ingested events. Defaults to a new
168
+ * {@link ReactiveEventStore}.
169
+ */
170
+ index(index) {
171
+ this.#opts = {
172
+ ...this.#opts,
173
+ index
174
+ };
175
+ return this;
176
+ }
114
177
  /** When false, Client will not auto-write events to storage. Default true. */
115
178
  persistEvents(enabled) {
116
179
  this.#opts = {
@@ -134,17 +197,18 @@ var ClientBuilder = class {
134
197
  return this;
135
198
  }
136
199
  build() {
137
- return new Client(this.#opts);
200
+ return this.#newClient(this.#opts);
138
201
  }
139
202
  };
140
203
  //#endregion
141
204
  //#region src/client/dm.ts
142
205
  function giftWrapRelays(gossip, event) {
143
206
  const targets = [];
144
- for (const tag of event.tags) if (tag[0] === "p" && tag[1]) targets.push(tag[1].toLowerCase());
207
+ for (const tag of event.tags) if (tag[0] === "p" && tag[1] !== void 0 && tag[1] !== "") targets.push(tag[1].toLowerCase());
145
208
  if (targets.length === 0) throw new Nip17Error("gift wrap has no p-tag recipient");
146
- if (targets.length !== 1) throw new Nip17Error("gift wrap must have exactly one p-tag recipient");
147
- const relays = gossip.dmRelays(targets[0]);
209
+ const [target] = targets;
210
+ if (targets.length !== 1 || target === void 0) throw new Nip17Error("gift wrap must have exactly one p-tag recipient");
211
+ const relays = gossip.dmRelays(target);
148
212
  if (relays.length === 0) throw new Nip17Error("no kind 10050 in gossip; use sendPrivateMessage or pass relays");
149
213
  return relays;
150
214
  }
@@ -174,7 +238,7 @@ async function sendPrivateMessage(deps, recipients, content, opts) {
174
238
  wraps: await Promise.all(wraps.map(async ({ recipient, wrap }) => {
175
239
  const relays = requireDmRelays(recipient, deps.gossip.dmRelays(recipient));
176
240
  const results = await deps.pool.publish(relays, wrap, { timeoutMs: opts?.timeoutMs });
177
- if (results.some((r) => r.result?.ok) && deps.wantObserve(opts?.observe)) deps.observe(wrap);
241
+ if (results.some((r) => r.result?.ok === true) && deps.wantObserve(opts?.observe)) deps.ingest(wrap);
178
242
  return {
179
243
  recipient,
180
244
  wrap,
@@ -191,6 +255,7 @@ async function fetchPrivateMessages(deps, opts) {
191
255
  await deps.hydrateGossip([self]);
192
256
  deps.throwIfAborted(opts?.signal);
193
257
  const relays = requireDmRelays(self, deps.gossip.dmRelays(self));
258
+ const urls = /* @__PURE__ */ new Map();
194
259
  const events = await deps.pool.fetch(relays, [{
195
260
  kinds: [Kind.GiftWrap],
196
261
  "#p": [self],
@@ -198,21 +263,31 @@ async function fetchPrivateMessages(deps, opts) {
198
263
  until: opts?.until
199
264
  }], {
200
265
  timeoutMs: opts?.timeoutMs,
201
- signal: opts?.signal
266
+ signal: opts?.signal,
267
+ onevent: (event, relayUrl) => {
268
+ let set = urls.get(event.id);
269
+ if (!set) urls.set(event.id, set = /* @__PURE__ */ new Set());
270
+ set.add(relayUrl);
271
+ }
202
272
  });
203
273
  const byRumor = /* @__PURE__ */ new Map();
204
274
  for (const wrap of events) try {
205
275
  const rumor = await unwrap(crypto, wrap);
206
- if (deps.wantObserve(opts?.observe)) deps.observe(wrap);
276
+ const wrapUrls = urls.get(wrap.id);
277
+ const firstUrl = wrapUrls?.values().next().value;
278
+ if (deps.wantObserve(opts?.observe)) {
279
+ deps.ingest(wrap, firstUrl);
280
+ if (wrapUrls) {
281
+ for (const url of wrapUrls) if (url !== firstUrl) deps.markSeen(wrap.id, url);
282
+ }
283
+ }
207
284
  byRumor.set(rumor.id, {
208
285
  wrap,
209
- rumor
286
+ rumor,
287
+ ...firstUrl === void 0 ? {} : { relayUrl: firstUrl }
210
288
  });
211
289
  } catch {}
212
- return [...byRumor.values()].sort((a, b) => {
213
- if (a.rumor.created_at !== b.rumor.created_at) return a.rumor.created_at - b.rumor.created_at;
214
- return a.rumor.id.localeCompare(b.rumor.id);
215
- });
290
+ return [...byRumor.values()].sort((a, b) => itemCompare(a.rumor, b.rumor));
216
291
  }
217
292
  async function subscribePrivateMessages(deps, opts) {
218
293
  deps.assertAlive();
@@ -223,12 +298,21 @@ async function subscribePrivateMessages(deps, opts) {
223
298
  deps.throwIfAborted(opts?.signal);
224
299
  const relays = requireDmRelays(self, deps.gossip.dmRelays(self));
225
300
  const seen = /* @__PURE__ */ new Set();
301
+ const processed = /* @__PURE__ */ new Set();
302
+ const pendingUrls = /* @__PURE__ */ new Map();
303
+ const flushSeen = (wrapId) => {
304
+ processed.add(wrapId);
305
+ const extra = pendingUrls.get(wrapId);
306
+ if (extra === void 0) return;
307
+ pendingUrls.delete(wrapId);
308
+ for (const url of extra) deps.markSeen(wrapId, url);
309
+ };
226
310
  let tail = Promise.resolve();
227
311
  let closed = false;
228
312
  const markClosed = () => {
229
313
  closed = true;
230
314
  };
231
- if (opts?.signal?.aborted) markClosed();
315
+ if (opts?.signal?.aborted === true) markClosed();
232
316
  else opts?.signal?.addEventListener("abort", markClosed, { once: true });
233
317
  const inner = deps.pool.subscribe(relays, [{
234
318
  kinds: [Kind.GiftWrap, Kind.GiftWrapEphemeral],
@@ -242,22 +326,45 @@ async function subscribePrivateMessages(deps, opts) {
242
326
  opts?.onclose?.(reason);
243
327
  },
244
328
  eoseTimeoutMs: opts?.eoseTimeoutMs,
245
- onevent: (wrap) => {
329
+ receivedEvent: (id, relayUrl) => {
330
+ if (closed) return;
331
+ if (processed.has(id)) {
332
+ deps.markSeen(id, relayUrl);
333
+ return;
334
+ }
335
+ const list = pendingUrls.get(id);
336
+ if (list === void 0) pendingUrls.set(id, [relayUrl]);
337
+ else if (!list.includes(relayUrl)) list.push(relayUrl);
338
+ },
339
+ onevent: (wrap, relayUrl) => {
246
340
  if (closed) return;
247
- tail = tail.then(async () => {
248
- if (closed) return;
341
+ const prev = tail;
342
+ let release;
343
+ tail = new Promise((resolve) => {
344
+ release = resolve;
345
+ });
346
+ (async () => {
347
+ await prev;
249
348
  try {
349
+ if (closed) return;
250
350
  const rumor = await unwrap(crypto, wrap);
251
351
  if (closed) return;
252
- if (seen.has(rumor.id)) return;
352
+ if (seen.has(rumor.id)) {
353
+ flushSeen(wrap.id);
354
+ return;
355
+ }
253
356
  seen.add(rumor.id);
254
- if (deps.wantObserve(opts?.observe)) deps.observe(wrap);
357
+ if (deps.wantObserve(opts?.observe)) deps.ingest(wrap, relayUrl);
358
+ flushSeen(wrap.id);
255
359
  opts?.onevent?.({
256
360
  wrap,
257
- rumor
361
+ rumor,
362
+ relayUrl
258
363
  });
259
- } catch {}
260
- }).catch(() => {});
364
+ } catch {} finally {
365
+ release?.();
366
+ }
367
+ })();
261
368
  }
262
369
  });
263
370
  return { close: (reason) => {
@@ -266,6 +373,18 @@ async function subscribePrivateMessages(deps, opts) {
266
373
  } };
267
374
  }
268
375
  //#endregion
376
+ //#region src/client/types.ts
377
+ /** Direction of a {@link Client.sync} run: upload, download, or both. */
378
+ const SyncDirection = {
379
+ Up: "up",
380
+ Down: "down",
381
+ Both: "both"
382
+ };
383
+ /** Client lifecycle, configuration, or abort failure (not cryptographic). */
384
+ var ClientError = class extends NostrError {
385
+ name = "ClientError";
386
+ };
387
+ //#endregion
269
388
  //#region src/client/gossip-io.ts
270
389
  /** Remainder is one job on defaults; throw before any REQ when defaults are empty. */
271
390
  function jobsForFilters(gossip, filters, defaultRelays) {
@@ -277,17 +396,21 @@ function jobsForFilters(gossip, filters, defaultRelays) {
277
396
  urls: [url],
278
397
  filters: [sub]
279
398
  });
280
- if (r.remainder) jobs.push({
281
- urls: defaults,
282
- filters: [r.remainder]
283
- });
399
+ if (r.remainder !== void 0) {
400
+ if (defaults === void 0) throw new ClientError("unrouted remainder without default relays");
401
+ jobs.push({
402
+ urls: defaults,
403
+ filters: [r.remainder]
404
+ });
405
+ }
284
406
  }
285
407
  return jobs;
286
408
  }
287
- function fetchGossip(pool, gossip, filters, defaultRelays, opts) {
409
+ async function fetchGossip(pool, gossip, filters, defaultRelays, opts) {
288
410
  return fetchRouted(pool, jobsForFilters(gossip, filters, defaultRelays), {
289
411
  timeoutMs: opts?.timeoutMs,
290
- signal: opts?.signal
412
+ signal: opts?.signal,
413
+ onevent: opts?.onevent
291
414
  });
292
415
  }
293
416
  function subscribeGossip(pool, gossip, filters, defaultRelays, opts) {
@@ -299,15 +422,6 @@ function subscribeGossip(pool, gossip, filters, defaultRelays, opts) {
299
422
  return fanIn(pool, jobs, opts);
300
423
  }
301
424
  //#endregion
302
- //#region src/client/types.ts
303
- const SyncDirection = {
304
- Up: "up",
305
- Down: "down",
306
- Both: "both"
307
- };
308
- /** Client lifecycle, configuration, or abort failure (not cryptographic). */
309
- var ClientError = class extends NostrError {};
310
- //#endregion
311
425
  //#region src/client/sync.ts
312
426
  const SYNC_ID_BATCH = 100;
313
427
  const SYNC_UPLOAD_CONCURRENCY = 8;
@@ -343,19 +457,18 @@ function mergeSyncSummary(into, other) {
343
457
  };
344
458
  }
345
459
  /**
346
- * NIP-77 sync against one relay: reconcile, then optionally upload
347
- * local-only events and/or download remote-only events.
348
- * `observe: false` skips putMany and ingestMeta; received ids are still listed.
349
- * `persistEvents: false` skips putMany, still ingestMeta when observe is on.
460
+ * NIP-77 sync against one relay: reconcile, then optionally upload local-only events and/or
461
+ * download remote-only events. `observe: false` skips putMany and ingest; received ids are still
462
+ * listed. `persistEvents: false` skips putMany, still ingests when observe is on.
350
463
  */
351
464
  async function syncToRelay(deps, url, filter, opts) {
352
465
  deps.assertAlive();
353
466
  deps.throwIfAborted(opts?.signal);
354
467
  const direction = opts?.direction ?? SyncDirection.Down;
355
- filter = canonicalizeFilter(filter);
356
- const items = await deps.storage.negentropyItems(filter);
468
+ const canon = canonicalizeFilter(filter);
469
+ const items = await deps.storage.negentropyItems(canon);
357
470
  const storage = storageFromItems(items);
358
- const { have, need } = await (await deps.pool.ensureRelay(url, { signal: opts?.signal })).negReconcile(filter, storage, {
471
+ const { have, need } = await (await deps.pool.ensureRelay(url, { signal: opts?.signal })).negReconcile(canon, storage, {
359
472
  timeoutMs: opts?.timeoutMs,
360
473
  signal: opts?.signal
361
474
  });
@@ -367,24 +480,22 @@ async function syncToRelay(deps, url, filter, opts) {
367
480
  sendFailures: {},
368
481
  persistFailures: {}
369
482
  };
370
- if (opts?.dryRun) return summary;
371
- if (direction === SyncDirection.Up || direction === SyncDirection.Both) {
372
- if (have.length > 0) {
373
- const found = await deps.storage.query([{ ids: have }]);
374
- const foundById = new Map(found.map((event) => [event.id, event]));
375
- for (const id of have) if (!foundById.has(id)) summary.sendFailures[id] = "event not found in local store";
376
- for (let i = 0; i < found.length; i += SYNC_UPLOAD_CONCURRENCY) {
377
- const chunk = found.slice(i, i + SYNC_UPLOAD_CONCURRENCY);
378
- await Promise.all(chunk.map(async (event) => {
379
- try {
380
- const results = await deps.pool.publish([url], event, { timeoutMs: opts?.timeoutMs });
381
- if (results.some((r) => r.result?.ok)) summary.sent.push(event.id);
382
- else summary.sendFailures[event.id] = results[0]?.error ?? results[0]?.result?.message ?? "publish failed";
383
- } catch (error) {
384
- summary.sendFailures[event.id] = error instanceof Error ? error.message : String(error);
385
- }
386
- }));
387
- }
483
+ if (opts?.dryRun === true) return summary;
484
+ if ((direction === SyncDirection.Up || direction === SyncDirection.Both) && have.length > 0) {
485
+ const found = await deps.storage.query([{ ids: have }]);
486
+ const foundById = new Map(found.map((event) => [event.id, event]));
487
+ for (const id of have) if (!foundById.has(id)) summary.sendFailures[id] = "event not found in local store";
488
+ for (let i = 0; i < found.length; i += SYNC_UPLOAD_CONCURRENCY) {
489
+ const chunk = found.slice(i, i + SYNC_UPLOAD_CONCURRENCY);
490
+ await Promise.all(chunk.map(async (event) => {
491
+ try {
492
+ const results = await deps.pool.publish([url], event, { timeoutMs: opts?.timeoutMs });
493
+ if (results.some((r) => r.result?.ok === true)) summary.sent.push(event.id);
494
+ else summary.sendFailures[event.id] = results[0]?.error ?? results[0]?.result?.message ?? "publish failed";
495
+ } catch (error) {
496
+ summary.sendFailures[event.id] = error instanceof Error ? error.message : String(error);
497
+ }
498
+ }));
388
499
  }
389
500
  }
390
501
  if ((direction === SyncDirection.Down || direction === SyncDirection.Both) && need.length > 0) {
@@ -392,17 +503,28 @@ async function syncToRelay(deps, url, filter, opts) {
392
503
  for (let i = 0; i < need.length; i += SYNC_ID_BATCH) {
393
504
  const batch = need.slice(i, i + SYNC_ID_BATCH);
394
505
  deps.throwIfAborted(opts?.signal);
506
+ const urlsById = /* @__PURE__ */ new Map();
395
507
  const events = await deps.pool.fetch([url], [{ ids: batch }], {
396
508
  timeoutMs: opts?.timeoutMs,
397
- signal: opts?.signal
509
+ signal: opts?.signal,
510
+ onevent: shouldObserve ? (event, relayUrl) => {
511
+ const urls = urlsById.get(event.id);
512
+ if (urls === void 0) urlsById.set(event.id, [relayUrl]);
513
+ else if (!urls.includes(relayUrl)) urls.push(relayUrl);
514
+ } : void 0
398
515
  });
399
516
  if (!shouldObserve) {
400
517
  for (const event of events) summary.received.push(event.id);
401
518
  continue;
402
519
  }
520
+ const ingestAll = (event) => {
521
+ const urls = urlsById.get(event.id) ?? [];
522
+ deps.ingest(event, urls[0], { persist: false });
523
+ for (const url of urls.slice(1)) deps.markSeen(event.id, url);
524
+ };
403
525
  if (!deps.persistEvents) {
404
526
  for (const event of events) {
405
- deps.ingestMeta(event);
527
+ ingestAll(event);
406
528
  summary.received.push(event.id);
407
529
  }
408
530
  continue;
@@ -410,15 +532,14 @@ async function syncToRelay(deps, url, filter, opts) {
410
532
  let results;
411
533
  try {
412
534
  results = await deps.storage.putMany(events);
413
- } catch (err) {
414
- const message = err instanceof Error ? err.message : String(err);
535
+ } catch (error) {
536
+ const message = error instanceof Error ? error.message : String(error);
415
537
  for (const event of events) summary.persistFailures[event.id] = message;
416
538
  break;
417
539
  }
418
- for (let j = 0; j < events.length; j++) {
419
- const event = events[j];
420
- if (results[j] === "rejected") continue;
421
- deps.ingestMeta(event);
540
+ for (const [j, event] of events.entries()) {
541
+ if (results[j] === "rejected" || results[j] === "invalid") continue;
542
+ ingestAll(event);
422
543
  summary.received.push(event.id);
423
544
  }
424
545
  }
@@ -426,14 +547,14 @@ async function syncToRelay(deps, url, filter, opts) {
426
547
  return summary;
427
548
  }
428
549
  /**
429
- * NIP-77 sync against the given relays (or Client default relays).
430
- * Independent sessions run in parallel. Fulfilled summaries are merged;
431
- * if every relay rejects, throws the first rejection in URL order.
550
+ * NIP-77 sync against the given relays (or Client default relays). Independent sessions run in
551
+ * parallel. Fulfilled summaries are merged; if every relay rejects, throws the first rejection in
552
+ * URL order.
432
553
  */
433
554
  async function sync(deps, filter, opts) {
434
555
  deps.assertAlive();
435
556
  const urls = deps.defaultRelays(opts?.relays ? [...opts.relays] : void 0);
436
- const results = await Promise.allSettled(urls.map((url) => syncToRelay(deps, url, filter, opts)));
557
+ const results = await Promise.allSettled(urls.map(async (url) => syncToRelay(deps, url, filter, opts)));
437
558
  let merged = emptySummary();
438
559
  let fulfilled = 0;
439
560
  let firstRejection;
@@ -446,14 +567,13 @@ async function sync(deps, filter, opts) {
446
567
  }
447
568
  //#endregion
448
569
  //#region src/client/client.ts
449
- /**
450
- * Layer-5 facade: signer + default relays + pool + loaders + gossip + event store.
451
- */
452
- var Client = class {
570
+ /** Layer-5 facade: signer + default relays + pool + loaders + gossip + event store. */
571
+ var Client = class Client {
453
572
  pool;
454
573
  loaders;
455
574
  gossip;
456
575
  storage;
576
+ index;
457
577
  onstorageerror;
458
578
  #signer;
459
579
  #relays;
@@ -463,12 +583,16 @@ var Client = class {
463
583
  #flushing;
464
584
  constructor(opts = {}) {
465
585
  this.#signer = opts.signer;
466
- this.#relays = [...opts.relays ?? []];
586
+ this.#relays = [];
587
+ for (const raw of opts.relays ?? []) {
588
+ const url = normalizeURL(raw);
589
+ if (!this.#relays.includes(url)) this.#relays.push(url);
590
+ }
467
591
  this.gossip = opts.gossip ?? new Gossip();
468
592
  this.storage = opts.storage ?? new MemoryEventStore();
593
+ this.index = opts.index ?? new ReactiveEventStore();
469
594
  this.#persistEvents = opts.persistEvents ?? true;
470
- this.onstorageerror = opts.onstorageerror ?? null;
471
- const autoAuth = opts.automaticAuth ?? Boolean(opts.signer);
595
+ this.onstorageerror = opts.onstorageerror;
472
596
  this.pool = new Pool({
473
597
  websocketImplementation: opts.websocketImplementation,
474
598
  verifyEvent: opts.verifyEvent,
@@ -478,25 +602,30 @@ var Client = class {
478
602
  connectTimeoutMs: opts.connectTimeoutMs,
479
603
  publishTimeoutMs: opts.publishTimeoutMs,
480
604
  enableReconnect: opts.enableReconnect ?? true,
481
- automaticallyAuth: autoAuth ? () => {
605
+ allowInsecure: opts.allowInsecure,
606
+ trustedInsecureUrls: opts.trustedInsecureUrls,
607
+ idleTimeoutMs: opts.idleTimeoutMs,
608
+ maxRelays: opts.maxRelays,
609
+ pinnedUrls: opts.pinnedUrls,
610
+ automaticallyAuth: opts.automaticAuth ?? true ? () => async (template) => {
482
611
  const signer = this.#signer;
483
- if (!signer) return null;
484
- return async (template) => {
485
- const pk = await signer.getPublicKey();
486
- return signer.signEvent({
487
- ...template,
488
- pubkey: pk
489
- });
490
- };
612
+ if (!signer) throw new NoSignerError("no signer configured for AUTH");
613
+ const pk = await signer.getPublicKey();
614
+ return signer.signEvent({
615
+ ...template,
616
+ pubkey: pk
617
+ });
491
618
  } : void 0
492
619
  });
493
620
  this.loaders = createLoaders({
494
621
  pool: this.pool,
495
- relays: this.#relays
622
+ relays: this.#relays,
623
+ index: this.index,
624
+ ingest: (event, relayUrl) => this.#ingest(event, relayUrl)
496
625
  });
497
626
  }
498
627
  static builder() {
499
- return new ClientBuilder();
628
+ return new ClientBuilder((opts) => new Client(opts));
500
629
  }
501
630
  get signer() {
502
631
  return this.#signer;
@@ -509,28 +638,30 @@ var Client = class {
509
638
  }
510
639
  setSigner(signer) {
511
640
  this.#signer = signer;
641
+ this.pool.resetAuth();
512
642
  }
513
643
  addRelay(url) {
514
- if (!this.#relays.includes(url)) {
515
- this.#relays.push(url);
516
- this.loaders.context.addRelay(url);
644
+ const normalized = normalizeURL(url);
645
+ if (!this.#relays.includes(normalized)) {
646
+ this.#relays.push(normalized);
647
+ this.loaders.addRelay(normalized);
517
648
  }
518
649
  }
519
650
  removeRelay(url) {
520
- this.#relays = this.#relays.filter((r) => r !== url);
521
- this.loaders.context.removeRelay(url);
522
- this.pool.close([url]);
651
+ const normalized = normalizeURL(url);
652
+ this.#relays = this.#relays.filter((r) => r !== normalized);
653
+ this.loaders.removeRelay(normalized);
654
+ this.pool.close([normalized]);
523
655
  }
524
656
  /** Connect all configured relays (best-effort; failures are ignored). */
525
657
  async connect(opts) {
526
658
  this.#assertAlive();
527
- await Promise.allSettled(this.#relays.map((url) => this.pool.ensureRelay(url, { signal: opts?.signal })));
659
+ await Promise.allSettled(this.#relays.map(async (url) => this.pool.ensureRelay(url, { signal: opts?.signal })));
528
660
  }
529
661
  async shutdown() {
530
662
  this.#shutdown = true;
531
- while (this.#flushing) await this.#flushing;
663
+ while (this.#flushing !== void 0) await this.#flushing;
532
664
  this.pool.close();
533
- this.loaders.context.cache.clear();
534
665
  }
535
666
  #assertAlive() {
536
667
  if (this.#shutdown) throw new ClientError("client is shut down");
@@ -543,30 +674,38 @@ var Client = class {
543
674
  #wantObserve(flag) {
544
675
  return flag !== false;
545
676
  }
546
- /**
547
- * Unified ingest pipeline: gossip + replaceable loader cache immediately;
548
- * storage writes are coalesced into a single-flight `putMany`.
549
- */
550
- observe(event) {
551
- this.#ingestMeta(event);
552
- if (!this.#persistEvents) return;
553
- this.#persistQueue.push(event);
554
- this.#armFlush();
677
+ /** Ingest one inbound event: index (with relay URL) → meta → persistence. */
678
+ observe(event, relayUrl) {
679
+ this.#ingest(event, relayUrl);
555
680
  }
556
- /** Observe many events (deduped by id, order preserved) as one persist batch. */
681
+ /** Ingest many events (deduped by id, order preserved) as one persist batch. */
557
682
  observeAll(events) {
558
683
  const seen = /* @__PURE__ */ new Set();
559
684
  for (const event of events) {
560
685
  if (seen.has(event.id)) continue;
561
686
  seen.add(event.id);
562
- this.#ingestMeta(event);
563
- if (this.#persistEvents) this.#persistQueue.push(event);
687
+ this.#ingest(event);
564
688
  }
565
- if (this.#persistEvents && this.#persistQueue.length > 0) this.#armFlush();
689
+ }
690
+ /**
691
+ * The single inbound-event pipeline: reactive index add (with the source relay URL recorded in
692
+ * seenOn), gossip routing meta, then the persistence decision. `persist: false` skips the storage
693
+ * queue for callers that persist via their own awaited `putMany` (down-sync); `meta: false` is a
694
+ * pure sighting/index add.
695
+ */
696
+ #ingest(event, relayUrl, opts) {
697
+ this.index.add(event, relayUrl);
698
+ if (opts?.meta !== false) this.#ingestMeta(event);
699
+ if (opts?.persist === false || !this.#persistEvents) return;
700
+ this.#persistQueue.push(event);
701
+ this.#armFlush();
702
+ }
703
+ /** Record a relay sighting for an id already in the index. */
704
+ #markSeen(id, relayUrl) {
705
+ this.index.markSeen(id, relayUrl);
566
706
  }
567
707
  #ingestMeta(event) {
568
708
  this.gossip.ingest(event);
569
- if (event.kind === Kind.Metadata || event.kind === Kind.Contacts || event.kind === Kind.MuteList || event.kind === Kind.RelayList || event.kind === Kind.DirectMessageRelaysList) this.loaders.context.cache.putIfNewer(event);
570
709
  }
571
710
  #armFlush() {
572
711
  if (this.#flushing) return;
@@ -581,8 +720,8 @@ var Client = class {
581
720
  const batch = this.#persistQueue.splice(0);
582
721
  try {
583
722
  await this.storage.putMany(batch);
584
- } catch (err) {
585
- this.onstorageerror?.(toStorageError(err));
723
+ } catch (error) {
724
+ invokeSafely(() => this.onstorageerror?.(toStorageError(error)));
586
725
  }
587
726
  }
588
727
  } finally {
@@ -591,13 +730,13 @@ var Client = class {
591
730
  }
592
731
  }
593
732
  /**
594
- * Explicitly load kind:10002 and kind:10050 lists for pubkeys and feed gossip.
595
- * Does not background-fetch unknown authors on every subscribe (predictable DX).
733
+ * Explicitly load kind:10002 and kind:10050 lists for pubkeys and feed gossip. Does not
734
+ * background-fetch unknown authors on every subscribe (predictable DX).
596
735
  */
597
736
  async hydrateGossip(pubkeys, opts) {
598
737
  this.#assertAlive();
599
738
  await Promise.all(pubkeys.map(async (pk) => {
600
- const style = opts?.force ? "force" : "default";
739
+ const style = opts?.force === true ? "force" : "default";
601
740
  const [relayList, dmList] = await Promise.all([this.loaders.relayList(pk, {
602
741
  hints: opts?.hints,
603
742
  style
@@ -610,8 +749,8 @@ var Client = class {
610
749
  }));
611
750
  }
612
751
  /**
613
- * Create an outbox-model feed for the given authors (NIP-65 write relays).
614
- * Live events go through {@link observe}; sync persists via applySync.
752
+ * Create an outbox-model feed for the given authors (NIP-65 write relays). Live events go through
753
+ * {@link observe}; sync persists via applySync.
615
754
  */
616
755
  outbox(opts) {
617
756
  this.#assertAlive();
@@ -624,22 +763,27 @@ var Client = class {
624
763
  kinds: opts.kinds,
625
764
  onEvent: opts.onEvent,
626
765
  maxRelaysPerAuthor: opts.maxRelaysPerAuthor,
627
- observe: (event) => this.observe(event),
766
+ observe: (event, relayUrl) => this.observe(event, relayUrl),
767
+ seen: (event, relayUrl) => this.#ingest(event, relayUrl, {
768
+ persist: false,
769
+ meta: false
770
+ }),
628
771
  applySync: async (events) => {
629
772
  if (!this.#persistEvents) {
630
- for (const event of events) this.#ingestMeta(event);
773
+ for (const event of events) this.#ingest(event, void 0, { persist: false });
631
774
  return [];
632
775
  }
633
776
  const results = await this.storage.putMany(events);
634
777
  const applied = [];
635
- for (let i = 0; i < events.length; i++) {
636
- if (results[i] === "rejected" || results[i] === "ephemeral") continue;
637
- this.#ingestMeta(events[i]);
638
- applied.push(events[i]);
778
+ for (const [i, event] of events.entries()) {
779
+ const result = results[i];
780
+ if (result === "rejected" || result === "ephemeral" || result === "invalid") continue;
781
+ this.#ingest(event, void 0, { persist: false });
782
+ applied.push(event);
639
783
  }
640
784
  return applied;
641
785
  },
642
- hydrate: (pubkeys) => this.hydrateGossip(pubkeys)
786
+ hydrate: async (pubkeys) => this.hydrateGossip(pubkeys)
643
787
  });
644
788
  }
645
789
  async getPublicKey() {
@@ -663,27 +807,26 @@ var Client = class {
663
807
  });
664
808
  }
665
809
  /**
666
- * Sign (if builder) and publish to relays.
667
- * With `gossip: true`, prefers the author's NIP-65 outbox relays, tagged `p`
668
- * inboxes, and up to 5 `e`/`a` tag relay hints when known.
669
- * On any successful OK, observes the event into storage/gossip.
810
+ * Sign (if builder) and publish to relays. With `gossip: true`, prefers the author's NIP-65
811
+ * outbox relays, tagged `p` inboxes, and up to 5 `e`/`a` tag relay hints when known. On any
812
+ * successful OK, observes the event into storage/gossip.
670
813
  */
671
814
  async publish(eventOrBuilder, opts) {
672
815
  this.#assertAlive();
673
816
  const event = eventOrBuilder instanceof EventBuilder ? await this.signEventBuilder(eventOrBuilder) : eventOrBuilder;
674
817
  let relays = opts?.relays;
675
818
  if (!relays && isGiftWrapKind(event.kind)) relays = giftWrapRelays(this.gossip, event);
676
- else if (!relays && opts?.gossip) {
819
+ else if (!relays && opts?.gossip === true) {
677
820
  const urls = [];
678
821
  const add = (list) => {
679
822
  for (const url of list) if (!urls.includes(url)) urls.push(url);
680
823
  };
681
824
  add(this.gossip.outboxRelays(event.pubkey));
682
- for (const tag of event.tags) if (tag[0] === "p" && tag[1]) add(this.gossip.inboxRelays(tag[1]));
825
+ for (const tag of event.tags) if (tag[0] === "p" && tag[1] !== void 0) add(this.gossip.inboxRelays(tag[1]));
683
826
  let hintCount = 0;
684
827
  for (const tag of event.tags) {
685
828
  if (hintCount >= 5) break;
686
- if (tag[0] !== "e" && tag[0] !== "a" || !tag[2]) continue;
829
+ if (tag[0] !== "e" && tag[0] !== "a" || tag[2] === void 0) continue;
687
830
  try {
688
831
  const url = normalizeURL(tag[2]);
689
832
  if (!urls.includes(url)) {
@@ -695,46 +838,57 @@ var Client = class {
695
838
  if (urls.length > 0) relays = urls;
696
839
  }
697
840
  const results = await this.pool.publish(this.#defaultRelays(relays), event, { timeoutMs: opts?.timeoutMs });
698
- if (results.some((r) => r.result?.ok) && this.#wantObserve(opts?.observe)) this.observe(event);
841
+ if (results.some((r) => r.result?.ok === true) && this.#wantObserve(opts?.observe)) this.observe(event);
699
842
  return results;
700
843
  }
701
844
  /**
702
- * Fetch events. With `gossip: true`, routes filters by NIP-65 when possible.
703
- * With `localFirst: true`, merges storage hits with network results.
845
+ * Fetch events. With `gossip: true`, routes filters by NIP-65 when possible. With `localFirst:
846
+ * true`, merges storage hits with network results.
704
847
  */
705
848
  async fetchEvents(filter, opts) {
706
849
  this.#assertAlive();
707
850
  const filters = canonicalizeFilters(Array.isArray(filter) ? filter : [filter]);
708
851
  const shouldObserve = this.#wantObserve(opts?.observe);
709
- const byId = /* @__PURE__ */ new Map();
710
- if (opts?.localFirst) try {
852
+ const candidates = [];
853
+ if (opts?.localFirst === true) try {
711
854
  const local = await this.storage.query(filters);
712
- for (const e of local) byId.set(e.id, e);
713
- } catch (err) {
714
- this.onstorageerror?.(toStorageError(err));
715
- }
716
- const ingest = (events) => {
717
- for (const e of events) {
718
- byId.set(e.id, e);
719
- if (shouldObserve) this.observe(e);
855
+ for (const e of local) {
856
+ candidates.push(e);
857
+ if (shouldObserve) this.#ingest(e, void 0, {
858
+ persist: false,
859
+ meta: false
860
+ });
720
861
  }
721
- };
722
- if (!opts?.gossip || opts.relays) {
723
- ingest(await this.pool.fetch(this.#defaultRelays(opts?.relays), filters, {
724
- timeoutMs: opts?.timeoutMs,
725
- signal: opts?.signal
726
- }));
727
- return sortedEvents([...byId.values()]);
862
+ } catch (error) {
863
+ invokeSafely(() => this.onstorageerror?.(toStorageError(error)));
728
864
  }
729
- ingest(await fetchGossip(this.pool, this.gossip, filters, () => this.#defaultRelays(), {
865
+ const onevent = (event, relayUrl) => {
866
+ if (shouldObserve) this.#ingest(event, relayUrl, {
867
+ persist: false,
868
+ meta: false
869
+ });
870
+ opts?.onevent?.(event, relayUrl);
871
+ };
872
+ const batch = opts?.gossip !== true || opts.relays !== void 0 ? await this.pool.fetch(this.#defaultRelays(opts?.relays), filters, {
730
873
  timeoutMs: opts?.timeoutMs,
731
- signal: opts?.signal
732
- }));
733
- return sortedEvents([...byId.values()]);
874
+ signal: opts?.signal,
875
+ onevent
876
+ }) : await fetchGossip(this.pool, this.gossip, filters, () => this.#defaultRelays(), {
877
+ timeoutMs: opts?.timeoutMs,
878
+ signal: opts?.signal,
879
+ onevent
880
+ });
881
+ for (const e of batch) {
882
+ candidates.push(e);
883
+ if (shouldObserve) this.observe(e);
884
+ }
885
+ const tmp = new MemoryIndex();
886
+ const ephemeral = /* @__PURE__ */ new Map();
887
+ const merged = opts?.localFirst === true ? [...this.index.query(filters), ...candidates] : candidates;
888
+ for (const e of merged) if (tmp.put(e) === "ephemeral" && matchFilters(filters, e)) ephemeral.set(e.id, e);
889
+ return sortedEvents([...tmp.query(filters), ...ephemeral.values()]);
734
890
  }
735
- /**
736
- * Query local storage only (no network).
737
- */
891
+ /** Query local storage only (no network). */
738
892
  async queryLocal(filter) {
739
893
  this.#assertAlive();
740
894
  const filters = canonicalizeFilters(Array.isArray(filter) ? filter : [filter]);
@@ -744,12 +898,17 @@ var Client = class {
744
898
  this.#assertAlive();
745
899
  const filters = canonicalizeFilters(Array.isArray(filter) ? filter : [filter]);
746
900
  const shouldObserve = this.#wantObserve(opts?.observe);
747
- const wrapEvent = (event) => {
748
- if (shouldObserve) this.observe(event);
749
- opts?.onevent?.(event);
901
+ const wrapEvent = (event, relayUrl) => {
902
+ if (shouldObserve) this.observe(event, relayUrl);
903
+ opts?.onevent?.(event, relayUrl);
904
+ };
905
+ const wrapReceived = (id, relayUrl) => {
906
+ if (shouldObserve) this.#markSeen(id, relayUrl);
907
+ opts?.receivedEvent?.(id, relayUrl);
750
908
  };
751
- if (!opts?.gossip || opts.relays) return this.pool.subscribe(this.#defaultRelays(opts?.relays), filters, {
909
+ if (opts?.gossip !== true || opts.relays !== void 0) return this.pool.subscribe(this.#defaultRelays(opts?.relays), filters, {
752
910
  onevent: wrapEvent,
911
+ receivedEvent: wrapReceived,
753
912
  oneose: opts?.oneose,
754
913
  onclose: opts?.onclose,
755
914
  signal: opts?.signal,
@@ -758,6 +917,7 @@ var Client = class {
758
917
  });
759
918
  return subscribeGossip(this.pool, this.gossip, filters, () => this.#defaultRelays(), {
760
919
  onevent: wrapEvent,
920
+ receivedEvent: wrapReceived,
761
921
  oneose: opts?.oneose,
762
922
  onclose: opts?.onclose,
763
923
  signal: opts?.signal,
@@ -769,21 +929,20 @@ var Client = class {
769
929
  return requireNip59Crypto(this.#signer);
770
930
  }
771
931
  #throwIfAborted(signal) {
772
- if (!signal?.aborted) return;
773
- if (signal.reason instanceof Error) throw signal.reason;
774
- throw new ClientError("aborted");
932
+ throwIfAborted(signal);
775
933
  }
776
934
  #dmDeps() {
777
935
  return {
778
936
  pool: this.pool,
779
937
  gossip: this.gossip,
780
- hydrateGossip: (pubkeys) => this.hydrateGossip(pubkeys),
781
- observe: (event) => this.observe(event),
938
+ hydrateGossip: async (pubkeys) => this.hydrateGossip(pubkeys),
939
+ ingest: (event, relayUrl) => this.#ingest(event, relayUrl),
940
+ markSeen: (id, relayUrl) => this.#markSeen(id, relayUrl),
782
941
  assertAlive: () => this.#assertAlive(),
783
942
  requireNip59Crypto: () => this.#requireNip59Crypto(),
784
943
  throwIfAborted: (signal) => this.#throwIfAborted(signal),
785
944
  wantObserve: (flag) => this.#wantObserve(flag),
786
- publish: (eventOrBuilder, opts) => this.publish(eventOrBuilder, opts)
945
+ publish: async (eventOrBuilder, opts) => this.publish(eventOrBuilder, opts)
787
946
  };
788
947
  }
789
948
  #syncDeps() {
@@ -794,7 +953,8 @@ var Client = class {
794
953
  assertAlive: () => this.#assertAlive(),
795
954
  throwIfAborted: (signal) => this.#throwIfAborted(signal),
796
955
  wantObserve: (flag) => this.#wantObserve(flag),
797
- ingestMeta: (event) => this.#ingestMeta(event),
956
+ ingest: (event, relayUrl, opts) => this.#ingest(event, relayUrl, opts),
957
+ markSeen: (id, relayUrl) => this.#markSeen(id, relayUrl),
798
958
  defaultRelays: (urls) => this.#defaultRelays(urls)
799
959
  };
800
960
  }
@@ -812,18 +972,17 @@ var Client = class {
812
972
  return subscribePrivateMessages(this.#dmDeps(), opts);
813
973
  }
814
974
  /**
815
- * NIP-77 sync against one relay: reconcile, then optionally upload
816
- * local-only events and/or download remote-only events.
817
- * `observe: false` skips putMany and ingestMeta; received ids are still listed.
818
- * `persistEvents: false` skips putMany, still ingestMeta when observe is on.
975
+ * NIP-77 sync against one relay: reconcile, then optionally upload local-only events and/or
976
+ * download remote-only events. `observe: false` skips putMany and ingest; received ids are still
977
+ * listed. `persistEvents: false` skips putMany, still ingests when observe is on.
819
978
  */
820
979
  async syncToRelay(url, filter, opts) {
821
980
  return syncToRelay(this.#syncDeps(), url, filter, opts);
822
981
  }
823
982
  /**
824
- * NIP-77 sync against the given relays (or Client default relays).
825
- * Independent sessions run in parallel. Fulfilled summaries are merged;
826
- * if every relay rejects, throws the first rejection in URL order.
983
+ * NIP-77 sync against the given relays (or Client default relays). Independent sessions run in
984
+ * parallel. Fulfilled summaries are merged; if every relay rejects, throws the first rejection in
985
+ * URL order.
827
986
  */
828
987
  async sync(filter, opts) {
829
988
  return sync(this.#syncDeps(), filter, opts);
@@ -832,4 +991,4 @@ var Client = class {
832
991
  //#endregion
833
992
  export { ClientBuilder as i, ClientError as n, SyncDirection as r, Client as t };
834
993
 
835
- //# sourceMappingURL=client-Bd8B1mYm.mjs.map
994
+ //# sourceMappingURL=client-BWsX9Cv1.mjs.map