@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,60 +1,26 @@
1
- import { a as isHex32, s as normalizeURL } from "./util-6tfLQ9JF.mjs";
2
- import { a as NostrError } from "./error-DZEs5OuZ.mjs";
3
- import { r as isAddressableKind, t as Kind } from "./kind-BLl4cLOc.mjs";
4
- import { _ as getDTag, d as sortedEvents, i as isReplaceableWinner } from "./event-CzoQEFMF.mjs";
1
+ import { g as NostrError, l as normalizeURL, o as isHex32 } from "./util-LfQirgDk.mjs";
2
+ import { r as isAddressableKind, t as Kind } from "./kind-Cxwhhkx8.mjs";
3
+ import { _ as formatEventAddress, f as sortedEvents } from "./event-D8Nfoh4K.mjs";
4
+ import { t as invokeSafely } from "./report-CHld4uWj.mjs";
5
5
  import { Nip19Error, decode, npubEncode } from "./nips/nip19.mjs";
6
6
  import { parseRelayList } from "./nips/nip65.mjs";
7
+ import { r as throwIfAborted } from "./abort-Ba8tDrOI.mjs";
7
8
  import { parseDmRelayList } from "./nips/nip17.mjs";
8
- import { n as toStorageError } from "./error-CwBxle6N.mjs";
9
- //#region src/loaders/cache.ts
10
- function keyOf(spec) {
11
- return `${spec.kind}:${spec.pubkey.toLowerCase()}:${spec.dTag ?? ""}`;
12
- }
13
- /** In-memory replaceable-event cache used by loaders (no DOM / localStorage). */
14
- var ReplaceableCache = class {
15
- #events = /* @__PURE__ */ new Map();
16
- get(spec) {
17
- return this.#events.get(keyOf(spec));
18
- }
19
- set(spec, event, fetchedAt = Math.floor(Date.now() / 1e3)) {
20
- this.#events.set(keyOf(spec), {
21
- event,
22
- fetchedAt
23
- });
24
- }
25
- /** Keep newer event by created_at. */
26
- putIfNewer(event, dTag) {
27
- const spec = {
28
- kind: event.kind,
29
- pubkey: event.pubkey,
30
- dTag
31
- };
32
- const prev = this.get(spec);
33
- if (!prev?.event || isReplaceableWinner(event, prev.event)) this.set(spec, event);
34
- }
35
- clear(spec) {
36
- if (spec) this.#events.delete(keyOf(spec));
37
- else this.#events.clear();
38
- }
39
- get size() {
40
- return this.#events.size;
41
- }
42
- };
43
- //#endregion
9
+ import { n as toStorageError } from "./error-DrEIM7FO.mjs";
44
10
  //#region src/loaders/context.ts
45
- /**
46
- * Explicit dependency bag for loaders — never a module-level singleton.
47
- */
11
+ /** Internal dependency bag for loaders — never a module-level singleton. */
48
12
  var LoaderContext = class {
49
13
  pool;
50
- cache;
14
+ index;
15
+ ingest;
51
16
  staleAfterSec;
52
17
  fetchTimeoutMs;
53
18
  #relays;
54
19
  constructor(opts) {
55
20
  this.pool = opts.pool;
56
21
  this.#relays = [...opts.relays];
57
- this.cache = opts.cache ?? new ReplaceableCache();
22
+ this.index = opts.index;
23
+ this.ingest = opts.ingest ?? ((event, relayUrl) => this.index.add(event, relayUrl));
58
24
  this.staleAfterSec = opts.staleAfterSec ?? 172800;
59
25
  this.fetchTimeoutMs = opts.fetchTimeoutMs ?? 4400;
60
26
  }
@@ -81,42 +47,60 @@ function dFilter(kind, identifier) {
81
47
  if (isAddressableKind(kind)) return { "#d": [identifier] };
82
48
  return {};
83
49
  }
50
+ function addressRef(kind, pubkey, identifier) {
51
+ return formatEventAddress(kind, pubkey.toLowerCase(), identifier);
52
+ }
84
53
  function parseRef(ref) {
85
54
  if (typeof ref === "string") {
86
- if (isHex32(ref)) return {
87
- filter: { ids: [ref.toLowerCase()] },
88
- hints: [],
89
- cacheKey: `id:${ref.toLowerCase()}`
90
- };
55
+ if (isHex32(ref.toLowerCase())) {
56
+ const id = ref.toLowerCase();
57
+ return {
58
+ filter: { ids: [id] },
59
+ hints: [],
60
+ lookup: (ctx) => ctx.index.get(id),
61
+ cacheKey: `id:${id}`
62
+ };
63
+ }
91
64
  const decoded = decode(ref);
92
65
  switch (decoded.type) {
93
66
  case "note": return {
94
67
  filter: { ids: [decoded.data] },
95
68
  hints: [],
69
+ lookup: (ctx) => ctx.index.get(decoded.data),
96
70
  cacheKey: `id:${decoded.data}`
97
71
  };
98
72
  case "nevent": return {
99
73
  filter: { ids: [decoded.data.id] },
100
74
  hints: decoded.data.relays ?? [],
75
+ lookup: (ctx) => ctx.index.get(decoded.data.id),
101
76
  cacheKey: `id:${decoded.data.id}`
102
77
  };
103
- case "naddr": return {
104
- filter: {
105
- authors: [decoded.data.pubkey.toLowerCase()],
106
- kinds: [decoded.data.kind],
107
- ...dFilter(decoded.data.kind, decoded.data.identifier)
108
- },
109
- hints: decoded.data.relays ?? [],
110
- cacheKey: `addr:${decoded.data.kind}:${decoded.data.pubkey}:${decoded.data.identifier}`
111
- };
78
+ case "naddr": {
79
+ const addr = addressRef(decoded.data.kind, decoded.data.pubkey, decoded.data.identifier);
80
+ return {
81
+ filter: {
82
+ authors: [decoded.data.pubkey.toLowerCase()],
83
+ kinds: [decoded.data.kind],
84
+ ...dFilter(decoded.data.kind, decoded.data.identifier)
85
+ },
86
+ hints: decoded.data.relays ?? [],
87
+ lookup: (ctx) => ctx.index.getByAddress(addr),
88
+ cacheKey: `addr:${addr}`
89
+ };
90
+ }
112
91
  default: throw new Nip19Error(`cannot load event from ${decoded.type}`);
113
92
  }
114
93
  }
115
- if ("id" in ref) return {
116
- filter: { ids: [ref.id.toLowerCase()] },
117
- hints: ref.relays ?? [],
118
- cacheKey: `id:${ref.id.toLowerCase()}`
119
- };
94
+ if ("id" in ref) {
95
+ const id = ref.id.toLowerCase();
96
+ return {
97
+ filter: { ids: [id] },
98
+ hints: ref.relays ?? [],
99
+ lookup: (ctx) => ctx.index.get(id),
100
+ cacheKey: `id:${id}`
101
+ };
102
+ }
103
+ const addr = addressRef(ref.kind, ref.pubkey, ref.identifier);
120
104
  return {
121
105
  filter: {
122
106
  authors: [ref.pubkey.toLowerCase()],
@@ -124,189 +108,39 @@ function parseRef(ref) {
124
108
  ...dFilter(ref.kind, ref.identifier)
125
109
  },
126
110
  hints: ref.relays ?? [],
127
- cacheKey: `addr:${ref.kind}:${ref.pubkey}:${ref.identifier}`
111
+ lookup: (ctx) => ctx.index.getByAddress(addr),
112
+ cacheKey: `addr:${addr}`
128
113
  };
129
114
  }
130
115
  function createEventLoader(ctx) {
131
116
  const inflight = /* @__PURE__ */ new Map();
132
- const cache = /* @__PURE__ */ new Map();
133
- return {
134
- load(ref) {
135
- const parsed = parseRef(ref);
136
- if (cache.has(parsed.cacheKey)) return Promise.resolve(cache.get(parsed.cacheKey));
137
- const hit = inflight.get(parsed.cacheKey);
138
- if (hit) return hit;
139
- const p = (async () => {
117
+ return { load(ref) {
118
+ const parsed = parseRef(ref);
119
+ const hit = parsed.lookup(ctx);
120
+ if (hit !== void 0) return Promise.resolve(hit);
121
+ const pending = inflight.get(parsed.cacheKey);
122
+ if (pending !== void 0) return pending;
123
+ const p = (async () => {
124
+ try {
140
125
  const relays = [.../* @__PURE__ */ new Set([...parsed.hints, ...ctx.relays])];
141
- if (relays.length === 0) return void 0;
142
- const events = await ctx.pool.fetch(relays, [parsed.filter], { timeoutMs: ctx.fetchTimeoutMs });
143
- let best;
144
- for (const e of events) if (!best || isReplaceableWinner(e, best)) best = e;
145
- cache.set(parsed.cacheKey, best);
146
- return best;
147
- })().finally(() => inflight.delete(parsed.cacheKey));
148
- inflight.set(parsed.cacheKey, p);
149
- return p;
150
- },
151
- clearAll() {
152
- inflight.clear();
153
- cache.clear();
154
- }
155
- };
156
- }
157
- //#endregion
158
- //#region src/loaders/dataloader.ts
159
- var LoaderError = class extends NostrError {};
160
- var DataLoader = class {
161
- #batchLoadFn;
162
- #cacheKeyFn;
163
- #maxBatchSize;
164
- #useCache;
165
- #cache = /* @__PURE__ */ new Map();
166
- #inflight = /* @__PURE__ */ new Map();
167
- #queue = [];
168
- #scheduled = false;
169
- constructor(batchLoadFn, options = {}) {
170
- this.#batchLoadFn = batchLoadFn;
171
- this.#cacheKeyFn = options.cacheKeyFn ?? ((k) => k);
172
- this.#maxBatchSize = options.maxBatchSize ?? Infinity;
173
- this.#useCache = options.cache !== false;
174
- }
175
- load(key) {
176
- const cacheKey = this.#cacheKeyFn(key);
177
- if (this.#useCache) {
178
- const cached = this.#cache.get(cacheKey);
179
- if (cached) return cached;
180
- }
181
- const inflight = this.#inflight.get(cacheKey);
182
- if (inflight) return inflight;
183
- const promise = new Promise((resolve, reject) => {
184
- this.#queue.push({
185
- key,
186
- resolve,
187
- reject
188
- });
189
- if (this.#queue.length >= this.#maxBatchSize) this.#dispatch();
190
- else if (!this.#scheduled) {
191
- this.#scheduled = true;
192
- queueMicrotask(() => this.#dispatch());
193
- }
194
- }).finally(() => {
195
- this.#inflight.delete(cacheKey);
196
- });
197
- this.#inflight.set(cacheKey, promise);
198
- if (this.#useCache) this.#cache.set(cacheKey, promise);
199
- return promise;
200
- }
201
- clear(key) {
202
- this.#cache.delete(this.#cacheKeyFn(key));
203
- this.#inflight.delete(this.#cacheKeyFn(key));
204
- }
205
- clearAll() {
206
- this.#cache.clear();
207
- this.#inflight.clear();
208
- }
209
- #dispatch() {
210
- this.#scheduled = false;
211
- const batch = this.#queue.splice(0, this.#maxBatchSize);
212
- if (batch.length === 0) return;
213
- const keys = batch.map((b) => b.key);
214
- this.#batchLoadFn(keys).then((values) => {
215
- if (values.length !== keys.length) {
216
- const err = new LoaderError(`DataLoader batch function must return array of length ${keys.length}, got ${values.length}`);
217
- for (const item of batch) item.reject(err);
218
- return;
219
- }
220
- for (let i = 0; i < batch.length; i++) {
221
- const value = values[i];
222
- if (value instanceof Error) batch[i].reject(value);
223
- else batch[i].resolve(value);
224
- }
225
- }).catch((err) => {
226
- for (const item of batch) item.reject(err);
227
- });
228
- if (this.#queue.length > 0) {
229
- this.#scheduled = true;
230
- queueMicrotask(() => this.#dispatch());
231
- }
232
- }
233
- };
234
- //#endregion
235
- //#region src/loaders/replaceable.ts
236
- /**
237
- * Batch-fetch replaceable events (kind + authors) via the pool, with cache.
238
- * Durable cache is ReplaceableCache; DataLoader only coalesces in-flight requests.
239
- */
240
- function createReplaceableLoader(ctx, kind) {
241
- const loader = new DataLoader(async (keys) => {
242
- const authors = [...new Set(keys.map((k) => k.pubkey))];
243
- const hintRelays = [...new Set(keys.flatMap((k) => k.hints ?? []).concat(ctx.relays))];
244
- const filter = {
245
- kinds: [kind],
246
- authors
247
- };
248
- const events = hintRelays.length > 0 ? await ctx.pool.fetch(hintRelays, [filter], { timeoutMs: ctx.fetchTimeoutMs }) : [];
249
- const best = /* @__PURE__ */ new Map();
250
- for (const event of events) {
251
- if (event.kind !== kind) continue;
252
- const prev = best.get(event.pubkey);
253
- if (!prev || isReplaceableWinner(event, prev)) best.set(event.pubkey, event);
254
- }
255
- const now = Math.floor(Date.now() / 1e3);
256
- return keys.map((k) => {
257
- const event = best.get(k.pubkey) ?? null;
258
- ctx.cache.set({
259
- kind,
260
- pubkey: k.pubkey,
261
- dTag: event ? getDTag(event.tags) ?? void 0 : void 0
262
- }, event, now);
263
- return event;
264
- });
265
- }, {
266
- cache: false,
267
- cacheKeyFn: (k) => `${kind}:${k.pubkey}:${(k.hints ?? []).join(",")}`,
268
- maxBatchSize: 50
269
- });
270
- return {
271
- async load(pubkey, opts) {
272
- const pk = pubkey.toLowerCase();
273
- const style = opts?.style ?? "default";
274
- const cached = ctx.cache.get({
275
- kind,
276
- pubkey: pk
277
- });
278
- if (style === "cache-only") return {
279
- event: cached?.event ?? null,
280
- fresh: false
281
- };
282
- if (style === "default" && cached && ctx.isFresh(cached.fetchedAt)) return {
283
- event: cached.event,
284
- fresh: false
285
- };
286
- return {
287
- event: await loader.load({
288
- pubkey: pk,
289
- hints: opts?.hints
290
- }),
291
- fresh: true
292
- };
293
- },
294
- clear(pubkey) {
295
- if (pubkey) {
296
- const pk = pubkey.toLowerCase();
297
- loader.clear({ pubkey: pk });
298
- ctx.cache.clear({
299
- kind,
300
- pubkey: pk
126
+ if (relays.length === 0) return;
127
+ await ctx.pool.fetch(relays, [parsed.filter], {
128
+ timeoutMs: ctx.fetchTimeoutMs,
129
+ onevent: (event, relayUrl) => ctx.ingest(event, relayUrl)
301
130
  });
302
- } else loader.clearAll();
303
- }
304
- };
131
+ return parsed.lookup(ctx);
132
+ } finally {
133
+ inflight.delete(parsed.cacheKey);
134
+ }
135
+ })();
136
+ inflight.set(parsed.cacheKey, p);
137
+ return p;
138
+ } };
305
139
  }
306
140
  //#endregion
307
141
  //#region src/loaders/lists.ts
308
142
  function fromTags(event, map) {
309
- if (!event) return [];
143
+ if (event === void 0) return [];
310
144
  const out = [];
311
145
  for (const tag of event.tags) {
312
146
  const item = map(tag);
@@ -314,33 +148,33 @@ function fromTags(event, map) {
314
148
  }
315
149
  return out;
316
150
  }
317
- function createListLoaders(ctx) {
318
- const followsLoader = createReplaceableLoader(ctx, Kind.Contacts);
319
- const muteLoader = createReplaceableLoader(ctx, Kind.MuteList);
320
- const relayListLoader = createReplaceableLoader(ctx, Kind.RelayList);
321
- const dmRelayListLoader = createReplaceableLoader(ctx, Kind.DirectMessageRelaysList);
151
+ function createListLoaders(replaceable) {
152
+ const followsLoader = replaceable(Kind.Contacts);
153
+ const muteLoader = replaceable(Kind.MuteList);
154
+ const relayListLoader = replaceable(Kind.RelayList);
155
+ const dmRelayListLoader = replaceable(Kind.DirectMessageRelaysList);
322
156
  return {
323
157
  async follows(pubkey, opts) {
324
- const { event, fresh } = await followsLoader.load(pubkey, opts);
158
+ const { event, fresh } = await followsLoader(pubkey, opts);
325
159
  return {
326
160
  event,
327
161
  fresh,
328
- items: fromTags(event, (tag) => tag[0] === "p" && tag[1] && isHex32(tag[1]) ? tag[1].toLowerCase() : void 0)
162
+ items: fromTags(event, (tag) => tag[0] === "p" && tag[1] !== void 0 && isHex32(tag[1].toLowerCase()) ? tag[1].toLowerCase() : void 0)
329
163
  };
330
164
  },
331
165
  async muteList(pubkey, opts) {
332
- const { event, fresh } = await muteLoader.load(pubkey, opts);
166
+ const { event, fresh } = await muteLoader(pubkey, opts);
333
167
  return {
334
168
  event,
335
169
  fresh,
336
170
  items: fromTags(event, (tag) => {
337
- if (!tag[1]) return void 0;
171
+ if (tag[1] === void 0 || tag[1] === "") return;
338
172
  switch (tag[0]) {
339
- case "p": return isHex32(tag[1]) ? {
173
+ case "p": return isHex32(tag[1].toLowerCase()) ? {
340
174
  label: "pubkey",
341
175
  value: tag[1].toLowerCase()
342
176
  } : void 0;
343
- case "e": return isHex32(tag[1]) ? {
177
+ case "e": return isHex32(tag[1].toLowerCase()) ? {
344
178
  label: "thread",
345
179
  value: tag[1].toLowerCase()
346
180
  } : void 0;
@@ -358,7 +192,7 @@ function createListLoaders(ctx) {
358
192
  };
359
193
  },
360
194
  async relayList(pubkey, opts) {
361
- const { event, fresh } = await relayListLoader.load(pubkey, opts);
195
+ const { event, fresh } = await relayListLoader(pubkey, opts);
362
196
  let items = [];
363
197
  if (event) try {
364
198
  items = parseRelayList(event);
@@ -382,7 +216,7 @@ function createListLoaders(ctx) {
382
216
  };
383
217
  },
384
218
  async dmRelayList(pubkey, opts) {
385
- const { event, fresh } = await dmRelayListLoader.load(pubkey, opts);
219
+ const { event, fresh } = await dmRelayListLoader(pubkey, opts);
386
220
  let items = [];
387
221
  if (event) try {
388
222
  items = parseDmRelayList(event);
@@ -399,6 +233,7 @@ function createListLoaders(ctx) {
399
233
  }
400
234
  //#endregion
401
235
  //#region src/loaders/profile.ts
236
+ /** A {@link NostrUser} with no metadata — used when no kind:0 event exists. */
402
237
  function bareNostrUser(pubkey) {
403
238
  const pk = pubkey.toLowerCase();
404
239
  let npub;
@@ -413,33 +248,37 @@ function bareNostrUser(pubkey) {
413
248
  shortName: npub.startsWith("npub1") ? `${npub.slice(0, 8)}…${npub.slice(-4)}` : pk.slice(0, 8),
414
249
  metadata: {},
415
250
  lastUpdated: 0,
416
- event: null,
251
+ event: void 0,
417
252
  fresh: false
418
253
  };
419
254
  }
255
+ function nonEmpty(value) {
256
+ return typeof value === "string" && value !== "" ? value : void 0;
257
+ }
420
258
  function parseMetadata(content) {
421
259
  try {
422
260
  const obj = JSON.parse(content);
423
- return typeof obj === "object" && obj !== null ? obj : {};
261
+ if (typeof obj !== "object" || obj === null) return {};
262
+ return obj;
424
263
  } catch {
425
264
  return {};
426
265
  }
427
266
  }
428
- function createProfileLoader(ctx) {
429
- const loader = createReplaceableLoader(ctx, Kind.Metadata);
267
+ function createProfileLoader(replaceable) {
268
+ const loader = replaceable(Kind.Metadata);
430
269
  return { async load(pubkey, opts) {
431
270
  const base = bareNostrUser(pubkey);
432
- const { event, fresh } = await loader.load(pubkey, opts);
271
+ const { event, fresh } = await loader(pubkey, opts);
433
272
  if (!event) return {
434
273
  ...base,
435
274
  fresh
436
275
  };
437
276
  const metadata = parseMetadata(event.content);
438
- const display = metadata.display_name || metadata.name;
277
+ const display = nonEmpty(metadata.display_name) ?? nonEmpty(metadata.name);
439
278
  return {
440
279
  ...base,
441
- shortName: display || base.shortName,
442
- image: metadata.picture,
280
+ shortName: display ?? base.shortName,
281
+ ...metadata.picture === void 0 ? {} : { image: metadata.picture },
443
282
  metadata,
444
283
  lastUpdated: event.created_at,
445
284
  event,
@@ -448,8 +287,177 @@ function createProfileLoader(ctx) {
448
287
  } };
449
288
  }
450
289
  //#endregion
290
+ //#region src/loaders/dataloader.ts
291
+ var LoaderError = class extends NostrError {
292
+ name = "LoaderError";
293
+ };
294
+ var DataLoader = class {
295
+ #batchLoadFn;
296
+ #cacheKeyFn;
297
+ #maxBatchSize;
298
+ #useCache;
299
+ #cache = /* @__PURE__ */ new Map();
300
+ #inflight = /* @__PURE__ */ new Map();
301
+ #queue = [];
302
+ #scheduled = false;
303
+ constructor(batchLoadFn, options = {}) {
304
+ this.#batchLoadFn = batchLoadFn;
305
+ this.#cacheKeyFn = options.cacheKeyFn ?? ((k) => k);
306
+ this.#maxBatchSize = options.maxBatchSize ?? Number.POSITIVE_INFINITY;
307
+ this.#useCache = options.cache !== false;
308
+ }
309
+ async load(key) {
310
+ const cacheKey = this.#cacheKeyFn(key);
311
+ if (this.#useCache) {
312
+ const cached = this.#cache.get(cacheKey);
313
+ if (cached) return cached;
314
+ }
315
+ const inflight = this.#inflight.get(cacheKey);
316
+ if (inflight) return inflight;
317
+ const pending = new Promise((resolve, reject) => {
318
+ this.#queue.push({
319
+ key,
320
+ resolve,
321
+ reject
322
+ });
323
+ if (this.#queue.length >= this.#maxBatchSize) this.#dispatch();
324
+ else if (!this.#scheduled) {
325
+ this.#scheduled = true;
326
+ queueMicrotask(() => this.#dispatch());
327
+ }
328
+ });
329
+ const promise = (async () => {
330
+ try {
331
+ return await pending;
332
+ } finally {
333
+ this.#inflight.delete(cacheKey);
334
+ }
335
+ })();
336
+ this.#inflight.set(cacheKey, promise);
337
+ if (this.#useCache) this.#cache.set(cacheKey, promise);
338
+ return promise;
339
+ }
340
+ clear(key) {
341
+ this.#cache.delete(this.#cacheKeyFn(key));
342
+ this.#inflight.delete(this.#cacheKeyFn(key));
343
+ }
344
+ clearAll() {
345
+ this.#cache.clear();
346
+ this.#inflight.clear();
347
+ }
348
+ #dispatch() {
349
+ this.#scheduled = false;
350
+ const batch = this.#queue.splice(0, this.#maxBatchSize);
351
+ if (batch.length === 0) return;
352
+ const keys = batch.map((b) => b.key);
353
+ (async () => {
354
+ let values;
355
+ try {
356
+ values = await this.#batchLoadFn(keys);
357
+ } catch (error) {
358
+ for (const item of batch) item.reject(error);
359
+ return;
360
+ }
361
+ if (values.length !== keys.length) {
362
+ const err = new LoaderError(`DataLoader batch function must return array of length ${keys.length}, got ${values.length}`);
363
+ for (const item of batch) item.reject(err);
364
+ return;
365
+ }
366
+ for (const [i, value] of values.entries()) {
367
+ const item = batch[i];
368
+ if (item === void 0) continue;
369
+ if (value instanceof Error) item.reject(value);
370
+ else item.resolve(value);
371
+ }
372
+ })();
373
+ if (this.#queue.length > 0) {
374
+ this.#scheduled = true;
375
+ queueMicrotask(() => this.#dispatch());
376
+ }
377
+ }
378
+ };
379
+ //#endregion
380
+ //#region src/loaders/replaceable.ts
381
+ /** FIFO cap on per-address fetch records (misses included). */
382
+ const MAX_FETCHED_AT = 1e4;
383
+ /**
384
+ * Batch-fetch replaceable events (kind + authors) via the pool. Fetched events are written into the
385
+ * reactive index (`onevent` records `seenOn`) and the winner is read back from it; the index — not
386
+ * the loader — owns the stored value and winner selection. The loader only tracks per-address
387
+ * `fetchedAt` timestamps so a recent fetch (including a miss) is not repeated within
388
+ * `staleAfterSec`.
389
+ */
390
+ function createReplaceableLoader(ctx, kind) {
391
+ const fetchedAt = /* @__PURE__ */ new Map();
392
+ const markFetched = (addr, hit, now) => {
393
+ fetchedAt.delete(addr);
394
+ fetchedAt.set(addr, {
395
+ at: now,
396
+ hit
397
+ });
398
+ while (fetchedAt.size > MAX_FETCHED_AT) {
399
+ const oldest = fetchedAt.keys().next().value;
400
+ if (oldest === void 0) break;
401
+ fetchedAt.delete(oldest);
402
+ }
403
+ };
404
+ const loader = new DataLoader(async (keys) => {
405
+ const authors = [...new Set(keys.map((k) => k.pubkey))];
406
+ const relays = [.../* @__PURE__ */ new Set([...keys.flatMap((k) => k.hints ?? []), ...ctx.relays])];
407
+ if (relays.length > 0) {
408
+ const filter = {
409
+ kinds: [kind],
410
+ authors
411
+ };
412
+ await ctx.pool.fetch(relays, [filter], {
413
+ timeoutMs: ctx.fetchTimeoutMs,
414
+ onevent: (event, relayUrl) => ctx.ingest(event, relayUrl)
415
+ });
416
+ }
417
+ const now = Math.floor(Date.now() / 1e3);
418
+ return keys.map((k) => {
419
+ const addr = formatEventAddress(kind, k.pubkey, "");
420
+ const event = ctx.index.getByAddress(addr);
421
+ markFetched(addr, event !== void 0, now);
422
+ return {
423
+ event,
424
+ fresh: true
425
+ };
426
+ });
427
+ }, {
428
+ cache: false,
429
+ cacheKeyFn: (k) => `${kind}:${k.pubkey}:${(k.hints ?? []).join(",")}`,
430
+ maxBatchSize: 50
431
+ });
432
+ return async (pubkey, opts) => {
433
+ const pk = pubkey.toLowerCase();
434
+ const address = formatEventAddress(kind, pk, "");
435
+ const style = opts?.style ?? "default";
436
+ const current = () => ctx.index.getByAddress(address);
437
+ if (style === "cache-only") return {
438
+ event: current(),
439
+ fresh: false
440
+ };
441
+ if (style === "default") {
442
+ const rec = fetchedAt.get(address);
443
+ const event = current();
444
+ if (rec !== void 0 && ctx.isFresh(rec.at) && (!rec.hit || event !== void 0)) return {
445
+ event,
446
+ fresh: false
447
+ };
448
+ }
449
+ return loader.load({
450
+ pubkey: pk,
451
+ hints: opts?.hints
452
+ });
453
+ };
454
+ }
455
+ //#endregion
451
456
  //#region src/loaders/outbox.ts
452
- var OutboxError = class extends NostrError {};
457
+ /** Outbox feed lifecycle error (e.g. use after close). */
458
+ var OutboxError = class extends NostrError {
459
+ name = "OutboxError";
460
+ };
453
461
  function boundKey(pubkey, kind) {
454
462
  return `${pubkey.toLowerCase()}:${kind}`;
455
463
  }
@@ -463,10 +471,9 @@ function canonicalRelayUrls(urls) {
463
471
  return out;
464
472
  }
465
473
  /**
466
- * Group authors by outbox relay (discovery fallback).
467
- * `prefer` reorders existing candidates only; it never appends a URL
468
- * that is not already in the author's outbox or discovery list.
469
- * Returns Map<relayUrl, authors[]>.
474
+ * Group authors by outbox relay (discovery fallback). `prefer` reorders existing candidates only;
475
+ * it never appends a URL that is not already in the author's outbox or discovery list. Returns
476
+ * Map<relayUrl, authors[]>.
470
477
  */
471
478
  function groupAuthorsByOutboxRelay(authors, gossip, discoveryRelays, maxRelaysPerAuthor = 3, prefer = []) {
472
479
  const leftover = new Set((gossip.route({ authors: authors.map((a) => a.toLowerCase()) }).remainder?.authors ?? []).map((a) => a.toLowerCase()));
@@ -480,7 +487,7 @@ function groupAuthorsByOutboxRelay(authors, gossip, discoveryRelays, maxRelaysPe
480
487
  const rest = [];
481
488
  for (const url of urls) if (preferSet.has(url)) preferred.push(url);
482
489
  else rest.push(url);
483
- for (const url of preferred.concat(rest).slice(0, maxRelaysPerAuthor)) {
490
+ for (const url of [...preferred, ...rest].slice(0, maxRelaysPerAuthor)) {
484
491
  const list = map.get(url) ?? [];
485
492
  if (!list.includes(pk)) list.push(pk);
486
493
  map.set(url, list);
@@ -489,8 +496,8 @@ function groupAuthorsByOutboxRelay(authors, gossip, discoveryRelays, maxRelaysPe
489
496
  return map;
490
497
  }
491
498
  /**
492
- * Outbox-model feed: history sync + live subscription for a set of authors,
493
- * routed via NIP-65 write relays (with discovery fallback).
499
+ * Outbox-model feed: history sync + live subscription for a set of authors, routed via NIP-65 write
500
+ * relays (with discovery fallback).
494
501
  */
495
502
  var OutboxFeed = class {
496
503
  #pool;
@@ -502,6 +509,7 @@ var OutboxFeed = class {
502
509
  #timeoutMs;
503
510
  #onEvent;
504
511
  #observe;
512
+ #seen;
505
513
  #applySync;
506
514
  #hydrate;
507
515
  #bounds = /* @__PURE__ */ new Map();
@@ -519,6 +527,7 @@ var OutboxFeed = class {
519
527
  this.#timeoutMs = opts.fetchTimeoutMs ?? 4400;
520
528
  this.#onEvent = opts.onEvent;
521
529
  this.#observe = opts.observe;
530
+ this.#seen = opts.seen;
522
531
  this.#applySync = opts.applySync;
523
532
  this.#hydrate = opts.hydrate;
524
533
  }
@@ -535,9 +544,7 @@ var OutboxFeed = class {
535
544
  getBound(pubkey, kind) {
536
545
  return this.#bounds.get(boundKey(pubkey, kind));
537
546
  }
538
- /**
539
- * Load NIP-65 relay lists for authors that have no outbox routes yet.
540
- */
547
+ /** Load NIP-65 relay lists for authors that have no outbox routes yet. */
541
548
  async hydrate() {
542
549
  this.#assertOpen();
543
550
  if (!this.#hydrate) return;
@@ -545,47 +552,48 @@ var OutboxFeed = class {
545
552
  if (missing.length === 0) return;
546
553
  await this.#hydrate(missing);
547
554
  }
548
- /**
549
- * One-shot history pull. Persistence is `applySync`; bounds move only for events it returns.
550
- */
555
+ /** One-shot history pull. Persistence is `applySync`; bounds move only for events it returns. */
551
556
  async sync(opts) {
552
557
  this.#assertOpen();
553
- if (!opts?.skipHydrate) await this.hydrate();
558
+ if (opts?.skipHydrate !== true) await this.hydrate();
554
559
  await this.#hydrateBoundsFromStore();
555
560
  const byRelay = groupAuthorsByOutboxRelay(this.#authors, this.#gossip, this.#discovery, this.#maxRelays, this.#pool.connectedUrls());
556
561
  if (byRelay.size === 0) return [];
557
562
  const byId = /* @__PURE__ */ new Map();
558
563
  await Promise.all([...byRelay.entries()].map(async ([url, authors]) => {
559
- if (opts?.signal?.aborted) return;
564
+ throwIfAborted(opts?.signal);
560
565
  const filters = this.#syncFilters(authors, opts);
561
566
  try {
562
567
  const batch = await this.#pool.fetch([url], filters, {
563
568
  timeoutMs: this.#timeoutMs,
564
- signal: opts?.signal
569
+ signal: opts?.signal,
570
+ onevent: (event, relayUrl) => invokeSafely(() => this.#seen?.(event, relayUrl))
565
571
  });
566
572
  for (const event of batch) byId.set(event.id, event);
567
- } catch {}
573
+ } catch (error) {
574
+ if (opts?.signal?.aborted === true) throw error;
575
+ }
568
576
  }));
569
577
  const unique = [...byId.values()];
570
578
  if (unique.length === 0) return [];
571
579
  let applied = [];
572
580
  if (this.#applySync) try {
573
581
  applied = await this.#applySync(unique);
574
- } catch (err) {
575
- throw toStorageError(err);
582
+ } catch (error) {
583
+ throw toStorageError(error);
576
584
  }
577
585
  const dirty = /* @__PURE__ */ new Set();
578
586
  for (const event of applied) {
579
587
  this.#updateBounds(event);
580
588
  dirty.add(boundKey(event.pubkey, event.kind));
581
- this.#onEvent?.(event);
589
+ invokeSafely(() => this.#onEvent?.(event));
582
590
  }
583
591
  await this.#persistBounds(dirty);
584
592
  return sortedEvents(unique);
585
593
  }
586
594
  /**
587
- * Start a live subscription across outbox relays for the current authors.
588
- * Closes any previous live subscription.
595
+ * Start a live subscription across outbox relays for the current authors. Closes any previous
596
+ * live subscription.
589
597
  */
590
598
  startLive(opts) {
591
599
  this.#assertOpen();
@@ -602,10 +610,11 @@ var OutboxFeed = class {
602
610
  };
603
611
  closers.push(this.#pool.subscribe([url], [filter], {
604
612
  signal: opts?.signal,
605
- onevent: (event) => {
613
+ onevent: (event, relayUrl) => {
614
+ invokeSafely(() => this.#seen?.(event, relayUrl));
606
615
  if (seen.has(event.id)) return;
607
616
  seen.add(event.id);
608
- this.#noteEvent(event);
617
+ this.#noteEvent(event, relayUrl);
609
618
  }
610
619
  }));
611
620
  }
@@ -653,7 +662,7 @@ var OutboxFeed = class {
653
662
  const base = {
654
663
  kinds: this.#kinds,
655
664
  limit: opts?.limit ?? 50,
656
- ...opts?.until !== void 0 ? { until: opts.until } : {}
665
+ ...opts?.until === void 0 ? {} : { until: opts.until }
657
666
  };
658
667
  if (opts?.since !== void 0) return [{
659
668
  ...base,
@@ -672,7 +681,7 @@ var OutboxFeed = class {
672
681
  complete = false;
673
682
  break;
674
683
  }
675
- if (newest === void 0 || b.newest < newest) newest = b.newest;
684
+ newest = Math.min(newest ?? b.newest, b.newest);
676
685
  }
677
686
  if (complete && newest !== void 0) {
678
687
  bounded.push(pk);
@@ -697,15 +706,19 @@ var OutboxFeed = class {
697
706
  authors: unbounded
698
707
  }];
699
708
  }
700
- #noteEvent(event) {
709
+ #noteEvent(event, relayUrl) {
701
710
  this.#updateBounds(event);
702
711
  const bound = this.#bounds.get(boundKey(event.pubkey, event.kind));
703
- if (bound) this.#storage.setOutboxBound(event.pubkey, event.kind, {
704
- oldest: bound.oldest,
705
- newest: bound.newest
706
- }).catch(() => {});
707
- this.#observe?.(event);
708
- this.#onEvent?.(event);
712
+ if (bound) (async () => {
713
+ try {
714
+ await this.#storage.setOutboxBound(event.pubkey, event.kind, {
715
+ oldest: bound.oldest,
716
+ newest: bound.newest
717
+ });
718
+ } catch {}
719
+ })();
720
+ invokeSafely(() => this.#observe?.(event, relayUrl));
721
+ invokeSafely(() => this.#onEvent?.(event));
709
722
  }
710
723
  async #persistBounds(keys) {
711
724
  const writes = [];
@@ -721,8 +734,8 @@ var OutboxFeed = class {
721
734
  if (writes.length === 0) return;
722
735
  try {
723
736
  await Promise.all(writes);
724
- } catch (err) {
725
- throw toStorageError(err);
737
+ } catch (error) {
738
+ throw toStorageError(error);
726
739
  }
727
740
  }
728
741
  #updateBounds(event) {
@@ -748,20 +761,31 @@ function createOutboxFeed(opts) {
748
761
  /** Build an instance-scoped loader suite (no module globals). */
749
762
  function createLoaders(opts) {
750
763
  const context = new LoaderContext(opts);
751
- const lists = createListLoaders(context);
752
- const profile = createProfileLoader(context);
764
+ const byKind = /* @__PURE__ */ new Map();
765
+ const replaceable = (kind) => {
766
+ let loader = byKind.get(kind);
767
+ if (loader === void 0) {
768
+ loader = createReplaceableLoader(context, kind);
769
+ byKind.set(kind, loader);
770
+ }
771
+ return loader;
772
+ };
773
+ const lists = createListLoaders(replaceable);
774
+ const profile = createProfileLoader(replaceable);
753
775
  const event = createEventLoader(context);
754
776
  return {
755
- context,
756
- follows: (pubkey, o) => lists.follows(pubkey, o),
757
- muteList: (pubkey, o) => lists.muteList(pubkey, o),
758
- relayList: (pubkey, o) => lists.relayList(pubkey, o),
759
- dmRelayList: (pubkey, o) => lists.dmRelayList(pubkey, o),
760
- profile: (pubkey, o) => profile.load(pubkey, o),
761
- event: (ref) => event.load(ref)
777
+ follows: async (pubkey, o) => lists.follows(pubkey, o),
778
+ muteList: async (pubkey, o) => lists.muteList(pubkey, o),
779
+ relayList: async (pubkey, o) => lists.relayList(pubkey, o),
780
+ dmRelayList: async (pubkey, o) => lists.dmRelayList(pubkey, o),
781
+ profile: async (pubkey, o) => profile.load(pubkey, o),
782
+ event: (ref) => event.load(ref),
783
+ replaceable,
784
+ addRelay: (url) => context.addRelay(url),
785
+ removeRelay: (url) => context.removeRelay(url)
762
786
  };
763
787
  }
764
788
  //#endregion
765
- export { groupAuthorsByOutboxRelay as a, createListLoaders as c, LoaderError as d, createEventLoader as f, createOutboxFeed as i, createReplaceableLoader as l, ReplaceableCache as m, OutboxError as n, bareNostrUser as o, LoaderContext as p, OutboxFeed as r, createProfileLoader as s, createLoaders as t, DataLoader as u };
789
+ export { groupAuthorsByOutboxRelay as a, createOutboxFeed as i, OutboxError as n, bareNostrUser as o, OutboxFeed as r, createLoaders as t };
766
790
 
767
- //# sourceMappingURL=loaders-BNQszDAn.mjs.map
791
+ //# sourceMappingURL=loaders-o6YXB_Ri.mjs.map