@qntx/nostr 0.0.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 (146) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +25 -0
  3. package/dist/builder-BphJa-Y-.d.mts +67 -0
  4. package/dist/builder-q0VsvnYE.mjs +158 -0
  5. package/dist/builder-q0VsvnYE.mjs.map +1 -0
  6. package/dist/client-Bd8B1mYm.mjs +835 -0
  7. package/dist/client-Bd8B1mYm.mjs.map +1 -0
  8. package/dist/client.d.mts +2 -0
  9. package/dist/client.mjs +2 -0
  10. package/dist/core.d.mts +8 -0
  11. package/dist/core.mjs +9 -0
  12. package/dist/error-B49rbDzT.d.mts +7 -0
  13. package/dist/error-CwBxle6N.mjs +13 -0
  14. package/dist/error-CwBxle6N.mjs.map +1 -0
  15. package/dist/error-DZEs5OuZ.mjs +24 -0
  16. package/dist/error-DZEs5OuZ.mjs.map +1 -0
  17. package/dist/error-WEFS-R-4.d.mts +20 -0
  18. package/dist/event-B5D2jxt_.d.mts +108 -0
  19. package/dist/event-CzoQEFMF.mjs +188 -0
  20. package/dist/event-CzoQEFMF.mjs.map +1 -0
  21. package/dist/filter-BxwjEkZp.mjs +113 -0
  22. package/dist/filter-BxwjEkZp.mjs.map +1 -0
  23. package/dist/filter-DUEKP3q1.d.mts +40 -0
  24. package/dist/gossip-3xkRRCm_.mjs +220 -0
  25. package/dist/gossip-3xkRRCm_.mjs.map +1 -0
  26. package/dist/gossip-C5RVB1UB.d.mts +71 -0
  27. package/dist/gossip.d.mts +3 -0
  28. package/dist/gossip.mjs +2 -0
  29. package/dist/http-6vevbBlu.d.mts +22 -0
  30. package/dist/http-D2rhokZo.mjs +24 -0
  31. package/dist/http-D2rhokZo.mjs.map +1 -0
  32. package/dist/index-BYMnhRIC.d.mts +161 -0
  33. package/dist/index-C0C4e-Gn.d.mts +55 -0
  34. package/dist/index-CaxrMD1A.d.mts +1 -0
  35. package/dist/index-DaRyjo7O.d.mts +77 -0
  36. package/dist/index-DgOugrpF.d.mts +275 -0
  37. package/dist/index-EwL6fHLo.d.mts +267 -0
  38. package/dist/index-k2YhCgsh.d.mts +57 -0
  39. package/dist/index.d.mts +22 -0
  40. package/dist/index.mjs +23 -0
  41. package/dist/key-BiGxGLhg.d.mts +37 -0
  42. package/dist/key-Dn-aX4x4.mjs +130 -0
  43. package/dist/key-Dn-aX4x4.mjs.map +1 -0
  44. package/dist/kind-BLl4cLOc.mjs +59 -0
  45. package/dist/kind-BLl4cLOc.mjs.map +1 -0
  46. package/dist/loaders-BNQszDAn.mjs +767 -0
  47. package/dist/loaders-BNQszDAn.mjs.map +1 -0
  48. package/dist/loaders.d.mts +3 -0
  49. package/dist/loaders.mjs +2 -0
  50. package/dist/memory-BCmhZPek.mjs +461 -0
  51. package/dist/memory-BCmhZPek.mjs.map +1 -0
  52. package/dist/message-C_Uj-Y-c.mjs +179 -0
  53. package/dist/message-C_Uj-Y-c.mjs.map +1 -0
  54. package/dist/message-DJC-Jz6T.d.mts +32 -0
  55. package/dist/nip17-BWq-Kfq5.d.mts +38 -0
  56. package/dist/nip19-D7905oIv.d.mts +63 -0
  57. package/dist/nip46-C_DoxiS_.d.mts +51 -0
  58. package/dist/nip59-DljW_sYh.d.mts +76 -0
  59. package/dist/nip65-Di5hZRuV.d.mts +21 -0
  60. package/dist/nip77-ByxlgoQO.d.mts +55 -0
  61. package/dist/nips/blossom.d.mts +93 -0
  62. package/dist/nips/blossom.mjs +325 -0
  63. package/dist/nips/blossom.mjs.map +1 -0
  64. package/dist/nips/nip04.d.mts +8 -0
  65. package/dist/nips/nip04.mjs +41 -0
  66. package/dist/nips/nip04.mjs.map +1 -0
  67. package/dist/nips/nip05.d.mts +89 -0
  68. package/dist/nips/nip05.mjs +147 -0
  69. package/dist/nips/nip05.mjs.map +1 -0
  70. package/dist/nips/nip10.d.mts +43 -0
  71. package/dist/nips/nip10.mjs +194 -0
  72. package/dist/nips/nip10.mjs.map +1 -0
  73. package/dist/nips/nip11.d.mts +43 -0
  74. package/dist/nips/nip11.mjs +99 -0
  75. package/dist/nips/nip11.mjs.map +1 -0
  76. package/dist/nips/nip13.d.mts +23 -0
  77. package/dist/nips/nip13.mjs +88 -0
  78. package/dist/nips/nip13.mjs.map +1 -0
  79. package/dist/nips/nip17.d.mts +2 -0
  80. package/dist/nips/nip17.mjs +138 -0
  81. package/dist/nips/nip17.mjs.map +1 -0
  82. package/dist/nips/nip19.d.mts +2 -0
  83. package/dist/nips/nip19.mjs +174 -0
  84. package/dist/nips/nip19.mjs.map +1 -0
  85. package/dist/nips/nip21.d.mts +17 -0
  86. package/dist/nips/nip21.mjs +29 -0
  87. package/dist/nips/nip21.mjs.map +1 -0
  88. package/dist/nips/nip27.d.mts +42 -0
  89. package/dist/nips/nip27.mjs +175 -0
  90. package/dist/nips/nip27.mjs.map +1 -0
  91. package/dist/nips/nip42.d.mts +12 -0
  92. package/dist/nips/nip42.mjs +22 -0
  93. package/dist/nips/nip42.mjs.map +1 -0
  94. package/dist/nips/nip44.d.mts +12 -0
  95. package/dist/nips/nip44.mjs +125 -0
  96. package/dist/nips/nip44.mjs.map +1 -0
  97. package/dist/nips/nip46.d.mts +2 -0
  98. package/dist/nips/nip46.mjs +132 -0
  99. package/dist/nips/nip46.mjs.map +1 -0
  100. package/dist/nips/nip49.d.mts +12 -0
  101. package/dist/nips/nip49.mjs +80 -0
  102. package/dist/nips/nip49.mjs.map +1 -0
  103. package/dist/nips/nip51.d.mts +88 -0
  104. package/dist/nips/nip51.mjs +246 -0
  105. package/dist/nips/nip51.mjs.map +1 -0
  106. package/dist/nips/nip57.d.mts +43 -0
  107. package/dist/nips/nip57.mjs +230 -0
  108. package/dist/nips/nip57.mjs.map +1 -0
  109. package/dist/nips/nip59.d.mts +2 -0
  110. package/dist/nips/nip59.mjs +171 -0
  111. package/dist/nips/nip59.mjs.map +1 -0
  112. package/dist/nips/nip65.d.mts +2 -0
  113. package/dist/nips/nip65.mjs +76 -0
  114. package/dist/nips/nip65.mjs.map +1 -0
  115. package/dist/nips/nip77.d.mts +2 -0
  116. package/dist/nips/nip77.mjs +421 -0
  117. package/dist/nips/nip77.mjs.map +1 -0
  118. package/dist/nips/nip96.d.mts +31 -0
  119. package/dist/nips/nip96.mjs +95 -0
  120. package/dist/nips/nip96.mjs.map +1 -0
  121. package/dist/nips/nip98.d.mts +28 -0
  122. package/dist/nips/nip98.mjs +91 -0
  123. package/dist/nips/nip98.mjs.map +1 -0
  124. package/dist/nostr_crypto_wasm_bg-hi5aXi5P.mjs +4 -0
  125. package/dist/nostr_crypto_wasm_bg.wasm +0 -0
  126. package/dist/pool-C_j1OmBB.mjs +1881 -0
  127. package/dist/pool-C_j1OmBB.mjs.map +1 -0
  128. package/dist/pool-DhRi7l-b.d.mts +297 -0
  129. package/dist/relay.d.mts +4 -0
  130. package/dist/relay.mjs +2 -0
  131. package/dist/signer-CZ6K6FUc.mjs +473 -0
  132. package/dist/signer-CZ6K6FUc.mjs.map +1 -0
  133. package/dist/signer.d.mts +3 -0
  134. package/dist/signer.mjs +2 -0
  135. package/dist/storage-DOy0iLRE.mjs +870 -0
  136. package/dist/storage-DOy0iLRE.mjs.map +1 -0
  137. package/dist/storage.d.mts +4 -0
  138. package/dist/storage.mjs +4 -0
  139. package/dist/types-BzyADqnw.d.mts +19 -0
  140. package/dist/types-Dn24TPFs.d.mts +43 -0
  141. package/dist/util-6tfLQ9JF.mjs +73 -0
  142. package/dist/util-6tfLQ9JF.mjs.map +1 -0
  143. package/dist/wasm.d.mts +19 -0
  144. package/dist/wasm.mjs +237 -0
  145. package/dist/wasm.mjs.map +1 -0
  146. package/package.json +204 -0
@@ -0,0 +1,835 @@
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";
8
+ import { storageFromItems } from "./nips/nip77.mjs";
9
+ import { isGiftWrapKind, requireNip59Crypto, unwrap } from "./nips/nip59.mjs";
10
+ 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";
15
+ //#region src/client/builder.ts
16
+ /** Fluent constructor for {@link Client}. */
17
+ var ClientBuilder = class {
18
+ #opts = {};
19
+ signer(signer) {
20
+ this.#opts = {
21
+ ...this.#opts,
22
+ signer
23
+ };
24
+ return this;
25
+ }
26
+ relays(urls) {
27
+ this.#opts = {
28
+ ...this.#opts,
29
+ relays: [...urls]
30
+ };
31
+ return this;
32
+ }
33
+ addRelay(url) {
34
+ this.#opts = {
35
+ ...this.#opts,
36
+ relays: [...this.#opts.relays ?? [], url]
37
+ };
38
+ return this;
39
+ }
40
+ websocketImplementation(impl) {
41
+ this.#opts = {
42
+ ...this.#opts,
43
+ websocketImplementation: impl
44
+ };
45
+ return this;
46
+ }
47
+ verifyEvent(fn) {
48
+ this.#opts = {
49
+ ...this.#opts,
50
+ verifyEvent: fn
51
+ };
52
+ return this;
53
+ }
54
+ connectTimeoutMs(ms) {
55
+ this.#opts = {
56
+ ...this.#opts,
57
+ connectTimeoutMs: ms
58
+ };
59
+ return this;
60
+ }
61
+ publishTimeoutMs(ms) {
62
+ this.#opts = {
63
+ ...this.#opts,
64
+ publishTimeoutMs: ms
65
+ };
66
+ return this;
67
+ }
68
+ automaticAuth(enabled) {
69
+ this.#opts = {
70
+ ...this.#opts,
71
+ automaticAuth: enabled
72
+ };
73
+ return this;
74
+ }
75
+ enableReconnect(enabled) {
76
+ this.#opts = {
77
+ ...this.#opts,
78
+ enableReconnect: enabled
79
+ };
80
+ return this;
81
+ }
82
+ enablePing(enabled) {
83
+ this.#opts = {
84
+ ...this.#opts,
85
+ enablePing: enabled
86
+ };
87
+ return this;
88
+ }
89
+ pingIntervalMs(ms) {
90
+ this.#opts = {
91
+ ...this.#opts,
92
+ pingIntervalMs: ms
93
+ };
94
+ return this;
95
+ }
96
+ pingTimeoutMs(ms) {
97
+ this.#opts = {
98
+ ...this.#opts,
99
+ pingTimeoutMs: ms
100
+ };
101
+ return this;
102
+ }
103
+ /**
104
+ * Local event store. Defaults to {@link MemoryEventStore}.
105
+ * Browser apps that want persistence must pass {@link IndexedDbEventStore} and `await open()`.
106
+ */
107
+ storage(store) {
108
+ this.#opts = {
109
+ ...this.#opts,
110
+ storage: store
111
+ };
112
+ return this;
113
+ }
114
+ /** When false, Client will not auto-write events to storage. Default true. */
115
+ persistEvents(enabled) {
116
+ this.#opts = {
117
+ ...this.#opts,
118
+ persistEvents: enabled
119
+ };
120
+ return this;
121
+ }
122
+ gossip(gossip) {
123
+ this.#opts = {
124
+ ...this.#opts,
125
+ gossip
126
+ };
127
+ return this;
128
+ }
129
+ onstorageerror(fn) {
130
+ this.#opts = {
131
+ ...this.#opts,
132
+ onstorageerror: fn
133
+ };
134
+ return this;
135
+ }
136
+ build() {
137
+ return new Client(this.#opts);
138
+ }
139
+ };
140
+ //#endregion
141
+ //#region src/client/dm.ts
142
+ function giftWrapRelays(gossip, event) {
143
+ const targets = [];
144
+ for (const tag of event.tags) if (tag[0] === "p" && tag[1]) targets.push(tag[1].toLowerCase());
145
+ 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]);
148
+ if (relays.length === 0) throw new Nip17Error("no kind 10050 in gossip; use sendPrivateMessage or pass relays");
149
+ return relays;
150
+ }
151
+ async function setDmRelays(deps, relays, opts) {
152
+ return deps.publish(dmRelayListEventBuilder(relays), {
153
+ gossip: true,
154
+ ...opts
155
+ });
156
+ }
157
+ async function sendPrivateMessage(deps, recipients, content, opts) {
158
+ deps.assertAlive();
159
+ const crypto = deps.requireNip59Crypto();
160
+ const sender = await crypto.getPublicKey();
161
+ const list = normalizeRecipients(recipients);
162
+ if (list.length === 0) throw new Nip17Error("recipients must not be empty");
163
+ const targets = /* @__PURE__ */ new Set([sender.toLowerCase(), ...list.map((r) => r.pubkey)]);
164
+ await deps.hydrateGossip([...targets]);
165
+ for (const pk of targets) requireDmRelays(pk, deps.gossip.dmRelays(pk));
166
+ const rumor = buildChatMessageRumor(sender, list, content, {
167
+ created_at: opts?.created_at,
168
+ subject: opts?.subject,
169
+ replyTo: opts?.replyTo
170
+ });
171
+ const wraps = await wrapDirectMessage(crypto, list, rumor);
172
+ return {
173
+ rumor,
174
+ wraps: await Promise.all(wraps.map(async ({ recipient, wrap }) => {
175
+ const relays = requireDmRelays(recipient, deps.gossip.dmRelays(recipient));
176
+ 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);
178
+ return {
179
+ recipient,
180
+ wrap,
181
+ results
182
+ };
183
+ }))
184
+ };
185
+ }
186
+ async function fetchPrivateMessages(deps, opts) {
187
+ deps.assertAlive();
188
+ const crypto = deps.requireNip59Crypto();
189
+ const self = await crypto.getPublicKey();
190
+ deps.throwIfAborted(opts?.signal);
191
+ await deps.hydrateGossip([self]);
192
+ deps.throwIfAborted(opts?.signal);
193
+ const relays = requireDmRelays(self, deps.gossip.dmRelays(self));
194
+ const events = await deps.pool.fetch(relays, [{
195
+ kinds: [Kind.GiftWrap],
196
+ "#p": [self],
197
+ since: opts?.since,
198
+ until: opts?.until
199
+ }], {
200
+ timeoutMs: opts?.timeoutMs,
201
+ signal: opts?.signal
202
+ });
203
+ const byRumor = /* @__PURE__ */ new Map();
204
+ for (const wrap of events) try {
205
+ const rumor = await unwrap(crypto, wrap);
206
+ if (deps.wantObserve(opts?.observe)) deps.observe(wrap);
207
+ byRumor.set(rumor.id, {
208
+ wrap,
209
+ rumor
210
+ });
211
+ } 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
+ });
216
+ }
217
+ async function subscribePrivateMessages(deps, opts) {
218
+ deps.assertAlive();
219
+ const crypto = deps.requireNip59Crypto();
220
+ const self = await crypto.getPublicKey();
221
+ deps.throwIfAborted(opts?.signal);
222
+ await deps.hydrateGossip([self]);
223
+ deps.throwIfAborted(opts?.signal);
224
+ const relays = requireDmRelays(self, deps.gossip.dmRelays(self));
225
+ const seen = /* @__PURE__ */ new Set();
226
+ let tail = Promise.resolve();
227
+ let closed = false;
228
+ const markClosed = () => {
229
+ closed = true;
230
+ };
231
+ if (opts?.signal?.aborted) markClosed();
232
+ else opts?.signal?.addEventListener("abort", markClosed, { once: true });
233
+ const inner = deps.pool.subscribe(relays, [{
234
+ kinds: [Kind.GiftWrap, Kind.GiftWrapEphemeral],
235
+ "#p": [self],
236
+ since: opts?.since
237
+ }], {
238
+ signal: opts?.signal,
239
+ oneose: opts?.oneose,
240
+ onclose: (reason) => {
241
+ markClosed();
242
+ opts?.onclose?.(reason);
243
+ },
244
+ eoseTimeoutMs: opts?.eoseTimeoutMs,
245
+ onevent: (wrap) => {
246
+ if (closed) return;
247
+ tail = tail.then(async () => {
248
+ if (closed) return;
249
+ try {
250
+ const rumor = await unwrap(crypto, wrap);
251
+ if (closed) return;
252
+ if (seen.has(rumor.id)) return;
253
+ seen.add(rumor.id);
254
+ if (deps.wantObserve(opts?.observe)) deps.observe(wrap);
255
+ opts?.onevent?.({
256
+ wrap,
257
+ rumor
258
+ });
259
+ } catch {}
260
+ }).catch(() => {});
261
+ }
262
+ });
263
+ return { close: (reason) => {
264
+ markClosed();
265
+ inner.close(reason);
266
+ } };
267
+ }
268
+ //#endregion
269
+ //#region src/client/gossip-io.ts
270
+ /** Remainder is one job on defaults; throw before any REQ when defaults are empty. */
271
+ function jobsForFilters(gossip, filters, defaultRelays) {
272
+ const routed = filters.map((f) => gossip.route(f));
273
+ const defaults = routed.some((r) => r.remainder !== void 0) ? defaultRelays() : void 0;
274
+ const jobs = [];
275
+ for (const r of routed) {
276
+ for (const [url, sub] of r.perRelay) jobs.push({
277
+ urls: [url],
278
+ filters: [sub]
279
+ });
280
+ if (r.remainder) jobs.push({
281
+ urls: defaults,
282
+ filters: [r.remainder]
283
+ });
284
+ }
285
+ return jobs;
286
+ }
287
+ function fetchGossip(pool, gossip, filters, defaultRelays, opts) {
288
+ return fetchRouted(pool, jobsForFilters(gossip, filters, defaultRelays), {
289
+ timeoutMs: opts?.timeoutMs,
290
+ signal: opts?.signal
291
+ });
292
+ }
293
+ function subscribeGossip(pool, gossip, filters, defaultRelays, opts) {
294
+ const jobs = jobsForFilters(gossip, filters, defaultRelays);
295
+ if (jobs.length === 0) {
296
+ queueMicrotask(() => opts.oneose?.());
297
+ return { close: () => {} };
298
+ }
299
+ return fanIn(pool, jobs, opts);
300
+ }
301
+ //#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
+ //#region src/client/sync.ts
312
+ const SYNC_ID_BATCH = 100;
313
+ const SYNC_UPLOAD_CONCURRENCY = 8;
314
+ function uniqueIds(ids) {
315
+ return [...new Set(ids)];
316
+ }
317
+ function emptySummary() {
318
+ return {
319
+ local: [],
320
+ remote: [],
321
+ sent: [],
322
+ received: [],
323
+ sendFailures: {},
324
+ persistFailures: {}
325
+ };
326
+ }
327
+ function mergeSyncSummary(into, other) {
328
+ const sendFailures = {
329
+ ...into.sendFailures,
330
+ ...other.sendFailures
331
+ };
332
+ const persistFailures = {
333
+ ...into.persistFailures,
334
+ ...other.persistFailures
335
+ };
336
+ return {
337
+ local: uniqueIds([...into.local, ...other.local]),
338
+ remote: uniqueIds([...into.remote, ...other.remote]),
339
+ sent: uniqueIds([...into.sent, ...other.sent]),
340
+ received: uniqueIds([...into.received, ...other.received]),
341
+ sendFailures,
342
+ persistFailures
343
+ };
344
+ }
345
+ /**
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.
350
+ */
351
+ async function syncToRelay(deps, url, filter, opts) {
352
+ deps.assertAlive();
353
+ deps.throwIfAborted(opts?.signal);
354
+ const direction = opts?.direction ?? SyncDirection.Down;
355
+ filter = canonicalizeFilter(filter);
356
+ const items = await deps.storage.negentropyItems(filter);
357
+ const storage = storageFromItems(items);
358
+ const { have, need } = await (await deps.pool.ensureRelay(url, { signal: opts?.signal })).negReconcile(filter, storage, {
359
+ timeoutMs: opts?.timeoutMs,
360
+ signal: opts?.signal
361
+ });
362
+ const summary = {
363
+ local: have,
364
+ remote: need,
365
+ sent: [],
366
+ received: [],
367
+ sendFailures: {},
368
+ persistFailures: {}
369
+ };
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
+ }
388
+ }
389
+ }
390
+ if ((direction === SyncDirection.Down || direction === SyncDirection.Both) && need.length > 0) {
391
+ const shouldObserve = deps.wantObserve(opts?.observe);
392
+ for (let i = 0; i < need.length; i += SYNC_ID_BATCH) {
393
+ const batch = need.slice(i, i + SYNC_ID_BATCH);
394
+ deps.throwIfAborted(opts?.signal);
395
+ const events = await deps.pool.fetch([url], [{ ids: batch }], {
396
+ timeoutMs: opts?.timeoutMs,
397
+ signal: opts?.signal
398
+ });
399
+ if (!shouldObserve) {
400
+ for (const event of events) summary.received.push(event.id);
401
+ continue;
402
+ }
403
+ if (!deps.persistEvents) {
404
+ for (const event of events) {
405
+ deps.ingestMeta(event);
406
+ summary.received.push(event.id);
407
+ }
408
+ continue;
409
+ }
410
+ let results;
411
+ try {
412
+ results = await deps.storage.putMany(events);
413
+ } catch (err) {
414
+ const message = err instanceof Error ? err.message : String(err);
415
+ for (const event of events) summary.persistFailures[event.id] = message;
416
+ break;
417
+ }
418
+ for (let j = 0; j < events.length; j++) {
419
+ const event = events[j];
420
+ if (results[j] === "rejected") continue;
421
+ deps.ingestMeta(event);
422
+ summary.received.push(event.id);
423
+ }
424
+ }
425
+ }
426
+ return summary;
427
+ }
428
+ /**
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.
432
+ */
433
+ async function sync(deps, filter, opts) {
434
+ deps.assertAlive();
435
+ const urls = deps.defaultRelays(opts?.relays ? [...opts.relays] : void 0);
436
+ const results = await Promise.allSettled(urls.map((url) => syncToRelay(deps, url, filter, opts)));
437
+ let merged = emptySummary();
438
+ let fulfilled = 0;
439
+ let firstRejection;
440
+ for (const result of results) if (result.status === "fulfilled") {
441
+ fulfilled += 1;
442
+ merged = mergeSyncSummary(merged, result.value);
443
+ } else firstRejection ??= result.reason;
444
+ if (urls.length > 0 && fulfilled === 0) throw firstRejection;
445
+ return merged;
446
+ }
447
+ //#endregion
448
+ //#region src/client/client.ts
449
+ /**
450
+ * Layer-5 facade: signer + default relays + pool + loaders + gossip + event store.
451
+ */
452
+ var Client = class {
453
+ pool;
454
+ loaders;
455
+ gossip;
456
+ storage;
457
+ onstorageerror;
458
+ #signer;
459
+ #relays;
460
+ #shutdown = false;
461
+ #persistEvents;
462
+ #persistQueue = [];
463
+ #flushing;
464
+ constructor(opts = {}) {
465
+ this.#signer = opts.signer;
466
+ this.#relays = [...opts.relays ?? []];
467
+ this.gossip = opts.gossip ?? new Gossip();
468
+ this.storage = opts.storage ?? new MemoryEventStore();
469
+ this.#persistEvents = opts.persistEvents ?? true;
470
+ this.onstorageerror = opts.onstorageerror ?? null;
471
+ const autoAuth = opts.automaticAuth ?? Boolean(opts.signer);
472
+ this.pool = new Pool({
473
+ websocketImplementation: opts.websocketImplementation,
474
+ verifyEvent: opts.verifyEvent,
475
+ enablePing: opts.enablePing,
476
+ pingIntervalMs: opts.pingIntervalMs,
477
+ pingTimeoutMs: opts.pingTimeoutMs,
478
+ connectTimeoutMs: opts.connectTimeoutMs,
479
+ publishTimeoutMs: opts.publishTimeoutMs,
480
+ enableReconnect: opts.enableReconnect ?? true,
481
+ automaticallyAuth: autoAuth ? () => {
482
+ 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
+ };
491
+ } : void 0
492
+ });
493
+ this.loaders = createLoaders({
494
+ pool: this.pool,
495
+ relays: this.#relays
496
+ });
497
+ }
498
+ static builder() {
499
+ return new ClientBuilder();
500
+ }
501
+ get signer() {
502
+ return this.#signer;
503
+ }
504
+ get relays() {
505
+ return this.#relays;
506
+ }
507
+ get isShutdown() {
508
+ return this.#shutdown;
509
+ }
510
+ setSigner(signer) {
511
+ this.#signer = signer;
512
+ }
513
+ addRelay(url) {
514
+ if (!this.#relays.includes(url)) {
515
+ this.#relays.push(url);
516
+ this.loaders.context.addRelay(url);
517
+ }
518
+ }
519
+ removeRelay(url) {
520
+ this.#relays = this.#relays.filter((r) => r !== url);
521
+ this.loaders.context.removeRelay(url);
522
+ this.pool.close([url]);
523
+ }
524
+ /** Connect all configured relays (best-effort; failures are ignored). */
525
+ async connect(opts) {
526
+ this.#assertAlive();
527
+ await Promise.allSettled(this.#relays.map((url) => this.pool.ensureRelay(url, { signal: opts?.signal })));
528
+ }
529
+ async shutdown() {
530
+ this.#shutdown = true;
531
+ while (this.#flushing) await this.#flushing;
532
+ this.pool.close();
533
+ this.loaders.context.cache.clear();
534
+ }
535
+ #assertAlive() {
536
+ if (this.#shutdown) throw new ClientError("client is shut down");
537
+ }
538
+ #defaultRelays(urls) {
539
+ const list = urls ?? this.#relays;
540
+ if (list.length === 0) throw new ClientError("no relays configured");
541
+ return list;
542
+ }
543
+ #wantObserve(flag) {
544
+ return flag !== false;
545
+ }
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();
555
+ }
556
+ /** Observe many events (deduped by id, order preserved) as one persist batch. */
557
+ observeAll(events) {
558
+ const seen = /* @__PURE__ */ new Set();
559
+ for (const event of events) {
560
+ if (seen.has(event.id)) continue;
561
+ seen.add(event.id);
562
+ this.#ingestMeta(event);
563
+ if (this.#persistEvents) this.#persistQueue.push(event);
564
+ }
565
+ if (this.#persistEvents && this.#persistQueue.length > 0) this.#armFlush();
566
+ }
567
+ #ingestMeta(event) {
568
+ 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
+ }
571
+ #armFlush() {
572
+ if (this.#flushing) return;
573
+ this.#flushing = this.#flushLoop();
574
+ }
575
+ async #flushLoop() {
576
+ try {
577
+ await new Promise((resolve) => {
578
+ queueMicrotask(resolve);
579
+ });
580
+ while (this.#persistQueue.length > 0) {
581
+ const batch = this.#persistQueue.splice(0);
582
+ try {
583
+ await this.storage.putMany(batch);
584
+ } catch (err) {
585
+ this.onstorageerror?.(toStorageError(err));
586
+ }
587
+ }
588
+ } finally {
589
+ this.#flushing = void 0;
590
+ if (this.#persistQueue.length > 0) this.#armFlush();
591
+ }
592
+ }
593
+ /**
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).
596
+ */
597
+ async hydrateGossip(pubkeys, opts) {
598
+ this.#assertAlive();
599
+ await Promise.all(pubkeys.map(async (pk) => {
600
+ const style = opts?.force ? "force" : "default";
601
+ const [relayList, dmList] = await Promise.all([this.loaders.relayList(pk, {
602
+ hints: opts?.hints,
603
+ style
604
+ }), this.loaders.dmRelayList(pk, {
605
+ hints: opts?.hints,
606
+ style
607
+ })]);
608
+ if (relayList.event) this.observe(relayList.event);
609
+ if (dmList.event) this.observe(dmList.event);
610
+ }));
611
+ }
612
+ /**
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.
615
+ */
616
+ outbox(opts) {
617
+ this.#assertAlive();
618
+ return createOutboxFeed({
619
+ pool: this.pool,
620
+ gossip: this.gossip,
621
+ storage: this.storage,
622
+ discoveryRelays: this.#relays,
623
+ authors: opts.authors,
624
+ kinds: opts.kinds,
625
+ onEvent: opts.onEvent,
626
+ maxRelaysPerAuthor: opts.maxRelaysPerAuthor,
627
+ observe: (event) => this.observe(event),
628
+ applySync: async (events) => {
629
+ if (!this.#persistEvents) {
630
+ for (const event of events) this.#ingestMeta(event);
631
+ return [];
632
+ }
633
+ const results = await this.storage.putMany(events);
634
+ 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]);
639
+ }
640
+ return applied;
641
+ },
642
+ hydrate: (pubkeys) => this.hydrateGossip(pubkeys)
643
+ });
644
+ }
645
+ async getPublicKey() {
646
+ if (!this.#signer) throw new ClientError("no signer configured");
647
+ return this.#signer.getPublicKey();
648
+ }
649
+ async signEvent(unsigned) {
650
+ if (!this.#signer) throw new ClientError("no signer configured");
651
+ return this.#signer.signEvent(unsigned);
652
+ }
653
+ async signEventBuilder(builder) {
654
+ if (!this.#signer) throw new ClientError("no signer configured");
655
+ return builder.sign(this.#signer);
656
+ }
657
+ async signTemplate(template) {
658
+ if (!this.#signer) throw new ClientError("no signer configured");
659
+ const pubkey = await this.#signer.getPublicKey();
660
+ return this.#signer.signEvent({
661
+ ...template,
662
+ pubkey
663
+ });
664
+ }
665
+ /**
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.
670
+ */
671
+ async publish(eventOrBuilder, opts) {
672
+ this.#assertAlive();
673
+ const event = eventOrBuilder instanceof EventBuilder ? await this.signEventBuilder(eventOrBuilder) : eventOrBuilder;
674
+ let relays = opts?.relays;
675
+ if (!relays && isGiftWrapKind(event.kind)) relays = giftWrapRelays(this.gossip, event);
676
+ else if (!relays && opts?.gossip) {
677
+ const urls = [];
678
+ const add = (list) => {
679
+ for (const url of list) if (!urls.includes(url)) urls.push(url);
680
+ };
681
+ 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]));
683
+ let hintCount = 0;
684
+ for (const tag of event.tags) {
685
+ if (hintCount >= 5) break;
686
+ if (tag[0] !== "e" && tag[0] !== "a" || !tag[2]) continue;
687
+ try {
688
+ const url = normalizeURL(tag[2]);
689
+ if (!urls.includes(url)) {
690
+ urls.push(url);
691
+ hintCount += 1;
692
+ }
693
+ } catch {}
694
+ }
695
+ if (urls.length > 0) relays = urls;
696
+ }
697
+ 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);
699
+ return results;
700
+ }
701
+ /**
702
+ * Fetch events. With `gossip: true`, routes filters by NIP-65 when possible.
703
+ * With `localFirst: true`, merges storage hits with network results.
704
+ */
705
+ async fetchEvents(filter, opts) {
706
+ this.#assertAlive();
707
+ const filters = canonicalizeFilters(Array.isArray(filter) ? filter : [filter]);
708
+ const shouldObserve = this.#wantObserve(opts?.observe);
709
+ const byId = /* @__PURE__ */ new Map();
710
+ if (opts?.localFirst) try {
711
+ 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);
720
+ }
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()]);
728
+ }
729
+ ingest(await fetchGossip(this.pool, this.gossip, filters, () => this.#defaultRelays(), {
730
+ timeoutMs: opts?.timeoutMs,
731
+ signal: opts?.signal
732
+ }));
733
+ return sortedEvents([...byId.values()]);
734
+ }
735
+ /**
736
+ * Query local storage only (no network).
737
+ */
738
+ async queryLocal(filter) {
739
+ this.#assertAlive();
740
+ const filters = canonicalizeFilters(Array.isArray(filter) ? filter : [filter]);
741
+ return this.storage.query(filters);
742
+ }
743
+ subscribe(filter, opts) {
744
+ this.#assertAlive();
745
+ const filters = canonicalizeFilters(Array.isArray(filter) ? filter : [filter]);
746
+ const shouldObserve = this.#wantObserve(opts?.observe);
747
+ const wrapEvent = (event) => {
748
+ if (shouldObserve) this.observe(event);
749
+ opts?.onevent?.(event);
750
+ };
751
+ if (!opts?.gossip || opts.relays) return this.pool.subscribe(this.#defaultRelays(opts?.relays), filters, {
752
+ onevent: wrapEvent,
753
+ oneose: opts?.oneose,
754
+ onclose: opts?.onclose,
755
+ signal: opts?.signal,
756
+ id: opts?.id,
757
+ eoseTimeoutMs: opts?.eoseTimeoutMs
758
+ });
759
+ return subscribeGossip(this.pool, this.gossip, filters, () => this.#defaultRelays(), {
760
+ onevent: wrapEvent,
761
+ oneose: opts?.oneose,
762
+ onclose: opts?.onclose,
763
+ signal: opts?.signal,
764
+ eoseTimeoutMs: opts?.eoseTimeoutMs
765
+ });
766
+ }
767
+ #requireNip59Crypto() {
768
+ if (!this.#signer) throw new ClientError("no signer configured");
769
+ return requireNip59Crypto(this.#signer);
770
+ }
771
+ #throwIfAborted(signal) {
772
+ if (!signal?.aborted) return;
773
+ if (signal.reason instanceof Error) throw signal.reason;
774
+ throw new ClientError("aborted");
775
+ }
776
+ #dmDeps() {
777
+ return {
778
+ pool: this.pool,
779
+ gossip: this.gossip,
780
+ hydrateGossip: (pubkeys) => this.hydrateGossip(pubkeys),
781
+ observe: (event) => this.observe(event),
782
+ assertAlive: () => this.#assertAlive(),
783
+ requireNip59Crypto: () => this.#requireNip59Crypto(),
784
+ throwIfAborted: (signal) => this.#throwIfAborted(signal),
785
+ wantObserve: (flag) => this.#wantObserve(flag),
786
+ publish: (eventOrBuilder, opts) => this.publish(eventOrBuilder, opts)
787
+ };
788
+ }
789
+ #syncDeps() {
790
+ return {
791
+ pool: this.pool,
792
+ storage: this.storage,
793
+ persistEvents: this.#persistEvents,
794
+ assertAlive: () => this.#assertAlive(),
795
+ throwIfAborted: (signal) => this.#throwIfAborted(signal),
796
+ wantObserve: (flag) => this.#wantObserve(flag),
797
+ ingestMeta: (event) => this.#ingestMeta(event),
798
+ defaultRelays: (urls) => this.#defaultRelays(urls)
799
+ };
800
+ }
801
+ /** Publish a kind:10050 NIP-17 DM relay list (`relay` tags). Not kind 10002. */
802
+ async setDmRelays(relays, opts) {
803
+ return setDmRelays(this.#dmDeps(), relays, opts);
804
+ }
805
+ async sendPrivateMessage(recipients, content, opts) {
806
+ return sendPrivateMessage(this.#dmDeps(), recipients, content, opts);
807
+ }
808
+ async fetchPrivateMessages(opts) {
809
+ return fetchPrivateMessages(this.#dmDeps(), opts);
810
+ }
811
+ async subscribePrivateMessages(opts) {
812
+ return subscribePrivateMessages(this.#dmDeps(), opts);
813
+ }
814
+ /**
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.
819
+ */
820
+ async syncToRelay(url, filter, opts) {
821
+ return syncToRelay(this.#syncDeps(), url, filter, opts);
822
+ }
823
+ /**
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.
827
+ */
828
+ async sync(filter, opts) {
829
+ return sync(this.#syncDeps(), filter, opts);
830
+ }
831
+ };
832
+ //#endregion
833
+ export { ClientBuilder as i, ClientError as n, SyncDirection as r, Client as t };
834
+
835
+ //# sourceMappingURL=client-Bd8B1mYm.mjs.map