@qntx/nostr 0.1.0 → 0.2.1

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