@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
@@ -0,0 +1,1376 @@
1
+ import { c as isRecord } from "./util-LfQirgDk.mjs";
2
+ import { t as Kind } from "./kind-Cxwhhkx8.mjs";
3
+ import { _ as formatEventAddress, a as isReplaceableWinner, b as parseEventAddress, d as sortEvents, g as eventAddress, m as validateSignedEvent, o as itemCompare, t as compareEventsDesc } from "./event-D8Nfoh4K.mjs";
4
+ import { a as matchFilter } from "./filter-BQl3GlHt.mjs";
5
+ import { n as toStorageError, t as StorageError } from "./error-DrEIM7FO.mjs";
6
+ import { a as planDeletion, i as DeletionState, n as decidePut, r as outboxBoundKey } from "./memory-index-Do1mz0WF.mjs";
7
+ //#region src/storage/idb-types.ts
8
+ const EVENTS = "events";
9
+ const TAG_REFS = "tag_refs";
10
+ const ADDRESSES = "addresses";
11
+ const TOMBSTONES = "tombstones";
12
+ const OUTBOX_BOUNDS = "outbox_bounds";
13
+ const WRITE_STORES = [
14
+ EVENTS,
15
+ TAG_REFS,
16
+ ADDRESSES,
17
+ TOMBSTONES
18
+ ];
19
+ //#endregion
20
+ //#region src/storage/idb-helpers.ts
21
+ async function reqOf(req) {
22
+ return new Promise((resolve, reject) => {
23
+ req.onsuccess = () => resolve(req.result);
24
+ req.onerror = () => reject(req.error ?? new StorageError("IndexedDB request failed"));
25
+ });
26
+ }
27
+ async function txDone(tx) {
28
+ return new Promise((resolve, reject) => {
29
+ tx.oncomplete = () => resolve();
30
+ tx.onerror = () => reject(tx.error ?? new StorageError("IndexedDB transaction failed"));
31
+ });
32
+ }
33
+ async function walkCursor(source, range, direction, visit) {
34
+ return new Promise((resolve, reject) => {
35
+ const req = source.openCursor(range, direction);
36
+ req.onerror = () => reject(req.error ?? new StorageError("IndexedDB cursor failed"));
37
+ req.onsuccess = () => {
38
+ const cursor = req.result ?? void 0;
39
+ if (cursor === void 0) {
40
+ resolve();
41
+ return;
42
+ }
43
+ if (visit(cursor)) {
44
+ resolve();
45
+ return;
46
+ }
47
+ cursor.continue();
48
+ };
49
+ });
50
+ }
51
+ function tagRefKey(name, value, id) {
52
+ return `${name}:${value.toLowerCase()}:${id}`;
53
+ }
54
+ function writeTagRefs(store, event) {
55
+ for (const tag of event.tags) {
56
+ if (tag[0] !== "e" && tag[0] !== "p" || tag[1] === void 0) continue;
57
+ const value = tag[1].toLowerCase();
58
+ const { id } = event;
59
+ store.put({
60
+ key: tagRefKey(tag[0], value, id),
61
+ name: tag[0],
62
+ value,
63
+ id,
64
+ created_at: event.created_at
65
+ });
66
+ }
67
+ }
68
+ function deleteStoredEvent(tx, event, addressRow) {
69
+ const tagRefs = tx.objectStore(TAG_REFS);
70
+ for (const tag of event.tags) if ((tag[0] === "e" || tag[0] === "p") && tag[1] !== void 0) tagRefs.delete(tagRefKey(tag[0], tag[1], event.id));
71
+ if (addressRow?.id === event.id) tx.objectStore(ADDRESSES).delete(addressRow.address);
72
+ tx.objectStore(EVENTS).delete(event.id);
73
+ }
74
+ function persistPlanTombstones(store, plan, coordIds, deletion) {
75
+ for (const id of plan.removeIds) {
76
+ store.put({
77
+ key: `id:${id}`,
78
+ type: "id"
79
+ });
80
+ store.delete(`pending:${id}`);
81
+ }
82
+ for (const id of coordIds) {
83
+ store.put({
84
+ key: `id:${id}`,
85
+ type: "id"
86
+ });
87
+ store.delete(`pending:${id}`);
88
+ }
89
+ for (const p of plan.pendingIds) {
90
+ if (deletion.ids.has(p.id)) continue;
91
+ store.put({
92
+ key: `pending:${p.id}`,
93
+ type: "pending",
94
+ pubkey: p.pubkey
95
+ });
96
+ }
97
+ for (const c of plan.coordinates) {
98
+ const prev = deletion.coordinates.get(c.key) ?? Number.NEGATIVE_INFINITY;
99
+ store.put({
100
+ key: `coord:${c.key}`,
101
+ type: "coord",
102
+ until: Math.max(prev, c.until)
103
+ });
104
+ }
105
+ }
106
+ function tombstonesToPlan(rows) {
107
+ const plan = {
108
+ removeIds: [],
109
+ pendingIds: [],
110
+ coordinates: []
111
+ };
112
+ for (const row of rows) {
113
+ if (!isRecord(row)) continue;
114
+ const r = row;
115
+ if (r["type"] === "id" && typeof r["key"] === "string" && r["key"].startsWith("id:")) {
116
+ plan.removeIds.push(r["key"].slice(3));
117
+ continue;
118
+ }
119
+ if (r["type"] === "pending" && typeof r["key"] === "string" && r["key"].startsWith("pending:") && typeof r["pubkey"] === "string") {
120
+ plan.pendingIds.push({
121
+ id: r["key"].slice(8),
122
+ pubkey: r["pubkey"]
123
+ });
124
+ continue;
125
+ }
126
+ if (r["type"] === "coord" && typeof r["key"] === "string" && r["key"].startsWith("coord:") && typeof r["until"] === "number") plan.coordinates.push({
127
+ key: r["key"].slice(6),
128
+ until: r["until"]
129
+ });
130
+ }
131
+ return plan;
132
+ }
133
+ function applyPutIndexedDb(tx, s, d) {
134
+ const events = tx.objectStore(EVENTS);
135
+ const tagRefs = tx.objectStore(TAG_REFS);
136
+ const addresses = tx.objectStore(ADDRESSES);
137
+ const tombstones = tx.objectStore(TOMBSTONES);
138
+ if (d.action === "skip") return d.result;
139
+ if (d.action === "tombstone") {
140
+ tombstones.put({
141
+ key: `id:${d.event.id}`,
142
+ type: "id"
143
+ });
144
+ tombstones.delete(`pending:${d.event.id}`);
145
+ s.deletion.ids.add(d.event.id);
146
+ s.deletion.pending.delete(d.event.id);
147
+ return "duplicate";
148
+ }
149
+ if (d.action === "delete") {
150
+ s.deletion.pending.delete(d.event.id);
151
+ persistPlanTombstones(tombstones, d.plan, d.coordIds, s.deletion);
152
+ s.deletion.absorb(d.plan);
153
+ for (const id of d.coordIds) s.deletion.ids.add(id);
154
+ events.put(d.event);
155
+ writeTagRefs(tagRefs, d.event);
156
+ return "deleted";
157
+ }
158
+ events.put(d.event);
159
+ writeTagRefs(tagRefs, d.event);
160
+ if (d.address !== void 0) {
161
+ addresses.put({
162
+ address: d.address,
163
+ id: d.event.id,
164
+ created_at: d.event.created_at
165
+ });
166
+ s.replaceable.set(d.address, d.event.id);
167
+ }
168
+ return d.result;
169
+ }
170
+ //#endregion
171
+ //#region src/storage/idb-query.ts
172
+ function prefixRange(prefix, since, until) {
173
+ return idbKeyRange().bound([...prefix, since ?? 0], [...prefix, until ?? Number.MAX_SAFE_INTEGER]);
174
+ }
175
+ function createdAtRange(since, until) {
176
+ return idbKeyRange().bound(since ?? 0, until ?? Number.MAX_SAFE_INTEGER);
177
+ }
178
+ function idbKeyRange() {
179
+ return globalThis.IDBKeyRange;
180
+ }
181
+ function epTagPrefixes(filter) {
182
+ const out = [];
183
+ for (const name of ["e", "p"]) {
184
+ const values = filter[`#${name}`];
185
+ if (values === void 0) continue;
186
+ for (const value of values) out.push({
187
+ name,
188
+ value: value.toLowerCase()
189
+ });
190
+ }
191
+ return out;
192
+ }
193
+ function eventCursor(source, range) {
194
+ return {
195
+ open: () => source.openCursor(range, "prev"),
196
+ read: (cursor, ok) => {
197
+ ok(cursor.value);
198
+ }
199
+ };
200
+ }
201
+ function tagCursor(index, events, range) {
202
+ return {
203
+ open: () => index.openCursor(range, "prev"),
204
+ read: (cursor, ok, err) => {
205
+ const row = cursor.value;
206
+ const req = events.get(row.id);
207
+ req.onerror = () => err(req.error ?? new StorageError("IndexedDB get failed"));
208
+ req.onsuccess = () => ok(req.result);
209
+ }
210
+ };
211
+ }
212
+ async function scanIds(tx, filter, accept, take) {
213
+ const ids = filter.ids ?? [];
214
+ const events = tx.objectStore(EVENTS);
215
+ const reqs = ids.map((id) => events.get(id.toLowerCase()));
216
+ const rows = await Promise.all(reqs.map(async (req) => reqOf(req)));
217
+ const matched = [];
218
+ const seen = /* @__PURE__ */ new Set();
219
+ for (const event of rows) {
220
+ if (event === void 0 || seen.has(event.id) || !accept(event)) continue;
221
+ seen.add(event.id);
222
+ matched.push(event);
223
+ }
224
+ sortEvents(matched);
225
+ const out = filter.limit === void 0 ? matched : matched.slice(0, filter.limit);
226
+ for (const event of out) if (take(event)) break;
227
+ }
228
+ /** IDB auto-commits when onsuccess returns with no outstanding requests. */
229
+ async function kWayMerge(openers, accept, take) {
230
+ return new Promise((resolve, reject) => {
231
+ if (openers.length === 0) {
232
+ resolve();
233
+ return;
234
+ }
235
+ const slots = openers.map(() => ({
236
+ cursor: void 0,
237
+ head: void 0
238
+ }));
239
+ const seen = /* @__PURE__ */ new Set();
240
+ let inflight = 0;
241
+ let phase = "merge";
242
+ let drainT = 0;
243
+ let drainBuf = [];
244
+ const finish = () => {
245
+ if (phase === "done") return;
246
+ phase = "done";
247
+ resolve();
248
+ };
249
+ const fail = (error) => {
250
+ if (phase === "done") return;
251
+ phase = "done";
252
+ reject(error);
253
+ };
254
+ const stepCursor = (i) => {
255
+ const { cursor } = slots.at(i) ?? { cursor: void 0 };
256
+ if (cursor === void 0) return;
257
+ inflight++;
258
+ cursor.continue();
259
+ };
260
+ const emitDrain = () => {
261
+ sortEvents(drainBuf);
262
+ for (const event of drainBuf) {
263
+ if (seen.has(event.id) || !accept(event)) continue;
264
+ seen.add(event.id);
265
+ if (take(event)) {
266
+ finish();
267
+ return;
268
+ }
269
+ }
270
+ drainBuf = [];
271
+ phase = "merge";
272
+ pump();
273
+ };
274
+ const pump = () => {
275
+ if (phase === "done" || inflight > 0) return;
276
+ if (phase === "drain") {
277
+ emitDrain();
278
+ return;
279
+ }
280
+ let best;
281
+ for (const slot of slots) {
282
+ const event = slot.head;
283
+ if (event === void 0) continue;
284
+ if (best === void 0 || compareEventsDesc(event, best) < 0) best = event;
285
+ }
286
+ if (best === void 0) {
287
+ finish();
288
+ return;
289
+ }
290
+ phase = "drain";
291
+ drainT = best.created_at;
292
+ drainBuf = [];
293
+ for (const [i, slot] of slots.entries()) {
294
+ const event = slot.head;
295
+ if (event === void 0 || event.created_at !== drainT) continue;
296
+ drainBuf.push(event);
297
+ slot.head = void 0;
298
+ stepCursor(i);
299
+ }
300
+ if (inflight === 0) pump();
301
+ };
302
+ const onEvent = (i, event) => {
303
+ if (phase === "done") return;
304
+ const slot = slots.at(i);
305
+ if (slot === void 0) return;
306
+ const { cursor } = slot;
307
+ if (event === void 0) {
308
+ if (cursor === void 0) pump();
309
+ else {
310
+ inflight++;
311
+ cursor.continue();
312
+ }
313
+ return;
314
+ }
315
+ if (phase === "drain") {
316
+ if (event.created_at === drainT) {
317
+ drainBuf.push(event);
318
+ if (cursor === void 0) pump();
319
+ else {
320
+ inflight++;
321
+ cursor.continue();
322
+ }
323
+ return;
324
+ }
325
+ slot.head = event;
326
+ pump();
327
+ return;
328
+ }
329
+ slot.head = event;
330
+ pump();
331
+ };
332
+ const openSlot = (i, opener) => {
333
+ const req = opener.open();
334
+ req.onerror = () => fail(req.error ?? new StorageError("IndexedDB cursor failed"));
335
+ inflight++;
336
+ req.onsuccess = () => {
337
+ inflight--;
338
+ if (phase === "done") return;
339
+ const cursor = req.result ?? void 0;
340
+ const slot = slots.at(i);
341
+ if (slot === void 0) return;
342
+ if (cursor === void 0) {
343
+ slot.cursor = void 0;
344
+ slot.head = void 0;
345
+ pump();
346
+ return;
347
+ }
348
+ slot.cursor = cursor;
349
+ inflight++;
350
+ opener.read(cursor, (event) => {
351
+ inflight--;
352
+ onEvent(i, event);
353
+ }, (error) => {
354
+ inflight--;
355
+ fail(error);
356
+ });
357
+ };
358
+ };
359
+ for (const [i, opener] of openers.entries()) openSlot(i, opener);
360
+ });
361
+ }
362
+ async function scanFilter(tx, filter, accept, take) {
363
+ if (filter.limit === 0) return;
364
+ if (filter.since !== void 0 && filter.until !== void 0 && filter.since > filter.until) return;
365
+ if (filter.ids) return scanIds(tx, filter, accept, take);
366
+ const events = tx.objectStore(EVENTS);
367
+ const fullScan = eventCursor(events.index("created_at"), createdAtRange(filter.since, filter.until));
368
+ const openers = [];
369
+ if (filter.authors && filter.kinds) {
370
+ if (filter.authors.length * filter.kinds.length > 64) {
371
+ if (filter.kinds.length <= 64) {
372
+ const index = events.index("kind_created_at");
373
+ for (const kind of filter.kinds) openers.push(eventCursor(index, prefixRange([kind], filter.since, filter.until)));
374
+ } else openers.push(fullScan);
375
+ } else {
376
+ const index = events.index("kind_pubkey_created_at");
377
+ for (const kind of filter.kinds) for (const pk of filter.authors) openers.push(eventCursor(index, prefixRange([kind, pk.toLowerCase()], filter.since, filter.until)));
378
+ }
379
+ } else if (filter.authors) {
380
+ if (filter.authors.length > 64) openers.push(fullScan);
381
+ else {
382
+ const index = events.index("pubkey_created_at");
383
+ for (const pk of filter.authors) openers.push(eventCursor(index, prefixRange([pk.toLowerCase()], filter.since, filter.until)));
384
+ }
385
+ } else if (filter.kinds) {
386
+ if (filter.kinds.length > 64) openers.push(fullScan);
387
+ else {
388
+ const index = events.index("kind_created_at");
389
+ for (const kind of filter.kinds) openers.push(eventCursor(index, prefixRange([kind], filter.since, filter.until)));
390
+ }
391
+ } else {
392
+ const tags = epTagPrefixes(filter);
393
+ if (tags.length > 64) openers.push(fullScan);
394
+ else if (tags.length > 0) {
395
+ const index = tx.objectStore(TAG_REFS).index("name_value_created");
396
+ for (const tag of tags) openers.push(tagCursor(index, events, prefixRange([tag.name, tag.value], filter.since, filter.until)));
397
+ } else openers.push(fullScan);
398
+ }
399
+ return kWayMerge(openers, accept, take);
400
+ }
401
+ async function openDb(dbName) {
402
+ return new Promise((resolve, reject) => {
403
+ const req = globalThis.indexedDB.open(dbName, 4);
404
+ req.onupgradeneeded = (ev) => {
405
+ const db = req.result;
406
+ const { oldVersion } = ev;
407
+ const tx = ev.target.transaction;
408
+ if (oldVersion < 1) db.createObjectStore(EVENTS, { keyPath: "id" });
409
+ if (oldVersion < 2) {
410
+ const events = tx.objectStore(EVENTS);
411
+ events.createIndex("created_at", "created_at");
412
+ events.createIndex("kind_created_at", ["kind", "created_at"]);
413
+ events.createIndex("pubkey_created_at", ["pubkey", "created_at"]);
414
+ events.createIndex("kind_pubkey_created_at", [
415
+ "kind",
416
+ "pubkey",
417
+ "created_at"
418
+ ]);
419
+ db.createObjectStore(TAG_REFS, { keyPath: "key" }).createIndex("name_value_created", [
420
+ "name",
421
+ "value",
422
+ "created_at"
423
+ ]);
424
+ db.createObjectStore(ADDRESSES, { keyPath: "address" });
425
+ db.createObjectStore(TOMBSTONES, { keyPath: "key" });
426
+ if (oldVersion >= 1) {
427
+ const all = events.getAll();
428
+ all.onsuccess = () => {
429
+ migrateV1Events(tx, all.result ?? []);
430
+ compactSupersededReplaceables(tx);
431
+ };
432
+ }
433
+ } else if (oldVersion < 3) compactSupersededReplaceables(tx);
434
+ if (oldVersion < 4) db.createObjectStore(OUTBOX_BOUNDS, { keyPath: "key" });
435
+ };
436
+ req.onsuccess = () => resolve(req.result);
437
+ req.onerror = () => reject(req.error ?? new StorageError("IndexedDB open failed"));
438
+ });
439
+ }
440
+ function migrateV1Events(tx, events) {
441
+ const eventsStore = tx.objectStore(EVENTS);
442
+ const tagRefs = tx.objectStore(TAG_REFS);
443
+ const addresses = tx.objectStore(ADDRESSES);
444
+ const tombstones = tx.objectStore(TOMBSTONES);
445
+ const valid = [];
446
+ for (const e of events) {
447
+ if (validateSignedEvent(e)) {
448
+ valid.push(e);
449
+ continue;
450
+ }
451
+ if (typeof e === "object" && e !== null && "id" in e && typeof e.id === "string") eventsStore.delete(e.id);
452
+ }
453
+ const byId = new Map(valid.map((e) => [e.id, e]));
454
+ const deletion = new DeletionState();
455
+ const dels = valid.filter((e) => e.kind === Kind.EventDeletion).sort(itemCompare);
456
+ for (const del of dels) {
457
+ const plan = planDeletion(del, (id) => byId.get(id));
458
+ deletion.absorb(plan);
459
+ for (const c of plan.coordinates) for (const ev of valid) {
460
+ if (ev.kind === Kind.EventDeletion) continue;
461
+ if (eventAddress(ev) === c.key && ev.created_at <= c.until) deletion.ids.add(ev.id);
462
+ }
463
+ }
464
+ for (const id of deletion.ids) tombstones.put({
465
+ key: `id:${id}`,
466
+ type: "id"
467
+ });
468
+ for (const [id, pubkey] of deletion.pending) tombstones.put({
469
+ key: `pending:${id}`,
470
+ type: "pending",
471
+ pubkey
472
+ });
473
+ for (const [key, until] of deletion.coordinates) tombstones.put({
474
+ key: `coord:${key}`,
475
+ type: "coord",
476
+ until
477
+ });
478
+ const winners = /* @__PURE__ */ new Map();
479
+ for (const event of valid) {
480
+ if (deletion.ids.has(event.id) || deletion.covers(event)) {
481
+ eventsStore.delete(event.id);
482
+ continue;
483
+ }
484
+ writeTagRefs(tagRefs, event);
485
+ const addr = eventAddress(event);
486
+ if (addr !== void 0) {
487
+ const prev = winners.get(addr);
488
+ if (!prev || isReplaceableWinner(event, prev)) winners.set(addr, event);
489
+ }
490
+ }
491
+ for (const [address, event] of winners) addresses.put({
492
+ address,
493
+ id: event.id,
494
+ created_at: event.created_at
495
+ });
496
+ }
497
+ function compactSupersededReplaceables(tx) {
498
+ const eventsStore = tx.objectStore(EVENTS);
499
+ const addressesStore = tx.objectStore(ADDRESSES);
500
+ const evReq = eventsStore.getAll();
501
+ const addrReq = addressesStore.getAll();
502
+ let events;
503
+ let addressRows;
504
+ const run = () => {
505
+ if (events === void 0 || addressRows === void 0) return;
506
+ const byAddr = /* @__PURE__ */ new Map();
507
+ for (const row of addressRows) byAddr.set(row.address, row);
508
+ const kept = [];
509
+ for (const event of events) {
510
+ const addr = eventAddress(event);
511
+ if (addr !== void 0) {
512
+ const row = byAddr.get(addr);
513
+ if (!row || row.id !== event.id) {
514
+ deleteStoredEvent(tx, event, row);
515
+ continue;
516
+ }
517
+ }
518
+ kept.push(event);
519
+ }
520
+ addressesStore.clear();
521
+ for (const event of kept) {
522
+ const addr = eventAddress(event);
523
+ if (addr === void 0) continue;
524
+ addressesStore.put({
525
+ address: addr,
526
+ id: event.id,
527
+ created_at: event.created_at
528
+ });
529
+ }
530
+ };
531
+ evReq.onsuccess = () => {
532
+ events = evReq.result ?? [];
533
+ run();
534
+ };
535
+ addrReq.onsuccess = () => {
536
+ addressRows = addrReq.result ?? [];
537
+ run();
538
+ };
539
+ }
540
+ //#endregion
541
+ //#region src/storage/indexeddb.ts
542
+ /**
543
+ * Browser IndexedDB event store with the same replaceable / deletion semantics as
544
+ * {@link MemoryEventStore}. Requires a DOM IndexedDB implementation.
545
+ *
546
+ * `open` loads every tombstone and address row into memory. Fine at 10^4 events; tens of MB at 10^5
547
+ * addressables.
548
+ */
549
+ var IndexedDbEventStore = class IndexedDbEventStore {
550
+ #dbName;
551
+ #db;
552
+ #deletion = new DeletionState();
553
+ #replaceable = /* @__PURE__ */ new Map();
554
+ /** Serializes writes so abort restore cannot roll back a committed sibling tx. */
555
+ #writeTail = Promise.resolve();
556
+ constructor(opts = {}) {
557
+ this.#dbName = opts.dbName ?? "@qntx/nostr";
558
+ }
559
+ static isAvailable() {
560
+ return globalThis.indexedDB !== void 0;
561
+ }
562
+ async open() {
563
+ if (this.#db) return;
564
+ if (!IndexedDbEventStore.isAvailable()) throw new StorageError("IndexedDB is not available in this environment");
565
+ const db = await openDb(this.#dbName);
566
+ this.#db = db;
567
+ await this.#loadCaches(db);
568
+ }
569
+ async #ensure() {
570
+ await this.open();
571
+ const db = this.#db;
572
+ if (db === void 0) throw new StorageError("IndexedDB is not available in this environment");
573
+ return db;
574
+ }
575
+ /** Loads every tombstone and address row into RAM. Tens of MB at 10^5 addressables. */
576
+ async #loadCaches(db) {
577
+ const tx = db.transaction([TOMBSTONES, ADDRESSES], "readonly");
578
+ const done = txDone(tx);
579
+ this.#deletion.clear();
580
+ this.#replaceable.clear();
581
+ const tombs = await reqOf(tx.objectStore(TOMBSTONES).getAll());
582
+ this.#deletion.absorb(tombstonesToPlan(tombs ?? []));
583
+ await walkCursor(tx.objectStore(ADDRESSES), void 0, "next", (cursor) => {
584
+ const row = cursor.value;
585
+ this.#replaceable.set(row.address, row.id);
586
+ return false;
587
+ });
588
+ await done;
589
+ }
590
+ async #enqueueWrite(op) {
591
+ const prev = this.#writeTail;
592
+ let release;
593
+ this.#writeTail = new Promise((resolve) => {
594
+ release = resolve;
595
+ });
596
+ try {
597
+ await prev;
598
+ return await op();
599
+ } finally {
600
+ release?.();
601
+ }
602
+ }
603
+ async put(event) {
604
+ const [result] = await this.putMany([event]);
605
+ if (result === void 0) throw new StorageError("putMany returned no result");
606
+ return result;
607
+ }
608
+ /** One readwrite transaction; abort rejects the whole batch with no partial persist. */
609
+ async putMany(events) {
610
+ if (events.length === 0) return [];
611
+ return this.#enqueueWrite(async () => this.#putManyLocked(events));
612
+ }
613
+ async #putManyLocked(events) {
614
+ let db;
615
+ try {
616
+ db = await this.#ensure();
617
+ } catch (error) {
618
+ throw toStorageError(error);
619
+ }
620
+ const tx = db.transaction(WRITE_STORES, "readwrite");
621
+ const txSettled = new Promise((resolve, reject) => {
622
+ tx.oncomplete = () => resolve();
623
+ tx.onerror = () => reject(tx.error ?? new StorageError("IndexedDB transaction failed"));
624
+ tx.onabort = () => reject(tx.error ?? new StorageError("IndexedDB transaction aborted"));
625
+ });
626
+ const txOutcome = (async () => {
627
+ try {
628
+ await txSettled;
629
+ return;
630
+ } catch (error) {
631
+ return error;
632
+ }
633
+ })();
634
+ const cache = this.#snapshotCaches();
635
+ try {
636
+ const results = await this.#putAllInTx(tx, events);
637
+ const txErr = await txOutcome;
638
+ if (txErr !== void 0) throw toStorageError(txErr);
639
+ return results;
640
+ } catch (error) {
641
+ this.#restoreCaches(cache);
642
+ try {
643
+ tx.abort();
644
+ } catch {}
645
+ await txOutcome;
646
+ throw toStorageError(error);
647
+ }
648
+ }
649
+ /** Sequential awaits so the tx stays alive between events. */
650
+ async #putAllInTx(tx, batch) {
651
+ const eventsStore = tx.objectStore(EVENTS);
652
+ const addressesStore = tx.objectStore(ADDRESSES);
653
+ const results = [];
654
+ for (const raw of batch) {
655
+ if (!validateSignedEvent(raw)) {
656
+ results.push("invalid");
657
+ continue;
658
+ }
659
+ const event = raw;
660
+ const byId = /* @__PURE__ */ new Map();
661
+ const existing = await reqOf(eventsStore.get(event.id));
662
+ if (existing) byId.set(existing.id, existing);
663
+ if (event.kind === Kind.EventDeletion) for (const tag of event.tags) {
664
+ if (tag[0] !== "e" || tag[1] === void 0) continue;
665
+ const got = await reqOf(eventsStore.get(tag[1].toLowerCase()));
666
+ if (got) byId.set(got.id, got);
667
+ }
668
+ const addrRows = /* @__PURE__ */ new Map();
669
+ const ownAddr = eventAddress(event);
670
+ if (ownAddr !== void 0) {
671
+ const row = await reqOf(addressesStore.get(ownAddr));
672
+ if (row) addrRows.set(ownAddr, {
673
+ id: row.id,
674
+ created_at: row.created_at
675
+ });
676
+ }
677
+ if (event.kind === Kind.EventDeletion) for (const tag of event.tags) {
678
+ if (tag[0] !== "a" || tag[1] === void 0) continue;
679
+ const coord = parseEventAddress(tag[1]);
680
+ if (!coord) continue;
681
+ const key = `${coord.kind}:${coord.pubkey}:${coord.identifier}`;
682
+ if (addrRows.has(key)) continue;
683
+ const row = await reqOf(addressesStore.get(key));
684
+ if (row) addrRows.set(key, {
685
+ id: row.id,
686
+ created_at: row.created_at
687
+ });
688
+ }
689
+ const d = decidePut(event, {
690
+ deletion: this.#deletion,
691
+ getById: (id) => byId.get(id),
692
+ getReplaceable: (address) => addrRows.get(address)
693
+ });
694
+ if (d.action === "delete") {
695
+ const remove = /* @__PURE__ */ new Set([...d.plan.removeIds, ...d.coordIds]);
696
+ for (const id of remove) await this.#deleteEventRows(tx, id);
697
+ } else if (d.action === "insert" && d.replaceId !== void 0) await this.#deleteEventRows(tx, d.replaceId);
698
+ results.push(applyPutIndexedDb(tx, {
699
+ deletion: this.#deletion,
700
+ replaceable: this.#replaceable
701
+ }, d));
702
+ }
703
+ return results;
704
+ }
705
+ #snapshotCaches() {
706
+ return {
707
+ replaceable: new Map(this.#replaceable),
708
+ ids: new Set(this.#deletion.ids),
709
+ pending: new Map(this.#deletion.pending),
710
+ coordinates: new Map(this.#deletion.coordinates)
711
+ };
712
+ }
713
+ #restoreCaches(snap) {
714
+ this.#replaceable = snap.replaceable;
715
+ this.#deletion.ids.clear();
716
+ for (const id of snap.ids) this.#deletion.ids.add(id);
717
+ this.#deletion.pending.clear();
718
+ for (const [k, v] of snap.pending) this.#deletion.pending.set(k, v);
719
+ this.#deletion.coordinates.clear();
720
+ for (const [k, v] of snap.coordinates) this.#deletion.coordinates.set(k, v);
721
+ }
722
+ async get(id) {
723
+ const key = id.toLowerCase();
724
+ if (this.#deletion.ids.has(key)) return;
725
+ const tx = (await this.#ensure()).transaction(EVENTS, "readonly");
726
+ const done = txDone(tx);
727
+ const event = await reqOf(tx.objectStore(EVENTS).get(key));
728
+ await done;
729
+ return event;
730
+ }
731
+ async query(filters) {
732
+ const tx = (await this.#ensure()).transaction([EVENTS, TAG_REFS], "readonly");
733
+ const done = txDone(tx);
734
+ const seen = /* @__PURE__ */ new Set();
735
+ const events = [];
736
+ for (const filter of filters) {
737
+ const matched = await this.#queryOne(tx, filter);
738
+ for (const event of matched) {
739
+ if (seen.has(event.id)) continue;
740
+ seen.add(event.id);
741
+ events.push(event);
742
+ }
743
+ }
744
+ await done;
745
+ sortEvents(events);
746
+ return events;
747
+ }
748
+ async count(filters) {
749
+ const tx = (await this.#ensure()).transaction([EVENTS, TAG_REFS], "readonly");
750
+ const done = txDone(tx);
751
+ const seen = /* @__PURE__ */ new Set();
752
+ for (const filter of filters) {
753
+ const local = /* @__PURE__ */ new Set();
754
+ await this.#scan(tx, filter, (event) => {
755
+ local.add(event.id);
756
+ return filter.limit !== void 0 && local.size >= filter.limit;
757
+ });
758
+ for (const id of local) seen.add(id);
759
+ }
760
+ await done;
761
+ return seen.size;
762
+ }
763
+ async negentropyItems(filter) {
764
+ const tx = (await this.#ensure()).transaction([EVENTS, TAG_REFS], "readonly");
765
+ const done = txDone(tx);
766
+ const items = [];
767
+ await this.#scan(tx, filter, (event) => {
768
+ items.push({
769
+ id: event.id,
770
+ created_at: event.created_at
771
+ });
772
+ return filter.limit !== void 0 && items.length >= filter.limit;
773
+ });
774
+ await done;
775
+ items.sort(itemCompare);
776
+ return items;
777
+ }
778
+ #acceptHit(filter, event) {
779
+ if (this.#deletion.ids.has(event.id)) return false;
780
+ if (this.#deletion.covers(event)) return false;
781
+ return matchFilter(filter, event);
782
+ }
783
+ async #queryOne(tx, filter) {
784
+ const matched = [];
785
+ await this.#scan(tx, filter, (event) => {
786
+ matched.push(event);
787
+ return filter.limit !== void 0 && matched.length >= filter.limit;
788
+ });
789
+ sortEvents(matched);
790
+ return matched;
791
+ }
792
+ async #scan(tx, filter, take) {
793
+ return scanFilter(tx, filter, (event) => this.#acceptHit(filter, event), take);
794
+ }
795
+ async getOutboxBound(pubkey, kind) {
796
+ try {
797
+ const tx = (await this.#ensure()).transaction(OUTBOX_BOUNDS, "readonly");
798
+ const done = txDone(tx);
799
+ const row = await reqOf(tx.objectStore(OUTBOX_BOUNDS).get(outboxBoundKey(pubkey, kind)));
800
+ await done;
801
+ if (row && typeof row.oldest === "number" && typeof row.newest === "number") return {
802
+ oldest: row.oldest,
803
+ newest: row.newest
804
+ };
805
+ return await this.#deriveOutboxBound(pubkey, kind);
806
+ } catch (error) {
807
+ throw toStorageError(error);
808
+ }
809
+ }
810
+ async setOutboxBound(pubkey, kind, bound) {
811
+ return this.#enqueueWrite(async () => this.#setOutboxBoundLocked(pubkey, kind, bound));
812
+ }
813
+ async #setOutboxBoundLocked(pubkey, kind, bound) {
814
+ try {
815
+ const tx = (await this.#ensure()).transaction(OUTBOX_BOUNDS, "readwrite");
816
+ const done = txDone(tx);
817
+ tx.objectStore(OUTBOX_BOUNDS).put({
818
+ key: outboxBoundKey(pubkey, kind),
819
+ oldest: bound.oldest,
820
+ newest: bound.newest
821
+ });
822
+ await done;
823
+ } catch (error) {
824
+ throw toStorageError(error);
825
+ }
826
+ }
827
+ async remove(ids) {
828
+ return this.#enqueueWrite(async () => this.#removeLocked(ids));
829
+ }
830
+ async #removeLocked(ids) {
831
+ const tx = (await this.#ensure()).transaction(WRITE_STORES, "readwrite");
832
+ const tombstones = tx.objectStore(TOMBSTONES);
833
+ const done = txDone(tx);
834
+ let n = 0;
835
+ for (const raw of ids) {
836
+ const id = raw.toLowerCase();
837
+ if (await this.#deleteEventRows(tx, id)) n += 1;
838
+ tombstones.put({
839
+ key: `id:${id}`,
840
+ type: "id"
841
+ });
842
+ tombstones.delete(`pending:${id}`);
843
+ this.#deletion.ids.add(id);
844
+ this.#deletion.pending.delete(id);
845
+ }
846
+ await done;
847
+ return n;
848
+ }
849
+ async clear() {
850
+ return this.#enqueueWrite(async () => this.#clearLocked());
851
+ }
852
+ async #clearLocked() {
853
+ const db = await this.#ensure();
854
+ const stores = [...WRITE_STORES, OUTBOX_BOUNDS];
855
+ const tx = db.transaction(stores, "readwrite");
856
+ const done = txDone(tx);
857
+ for (const name of stores) tx.objectStore(name).clear();
858
+ this.#deletion.clear();
859
+ this.#replaceable.clear();
860
+ await done;
861
+ }
862
+ close() {
863
+ this.#db?.close();
864
+ this.#db = void 0;
865
+ }
866
+ async #deleteEventRows(tx, id) {
867
+ const event = await reqOf(tx.objectStore(EVENTS).get(id));
868
+ if (!event) return false;
869
+ const addr = eventAddress(event);
870
+ let row;
871
+ if (addr !== void 0) {
872
+ row = await reqOf(tx.objectStore(ADDRESSES).get(addr));
873
+ if (this.#replaceable.get(addr) === event.id) this.#replaceable.delete(addr);
874
+ }
875
+ deleteStoredEvent(tx, event, row);
876
+ return true;
877
+ }
878
+ async #deriveOutboxBound(pubkey, kind) {
879
+ const db = await this.#ensure();
880
+ const pk = pubkey.toLowerCase();
881
+ const tx = db.transaction(EVENTS, "readonly");
882
+ const done = txDone(tx);
883
+ const index = tx.objectStore(EVENTS).index("kind_pubkey_created_at");
884
+ const range = prefixRange([kind, pk]);
885
+ const filter = {
886
+ authors: [pk],
887
+ kinds: [kind]
888
+ };
889
+ let oldest;
890
+ let newest;
891
+ const firstLive = async (direction, assign) => walkCursor(index, range, direction, (cursor) => {
892
+ const event = cursor.value;
893
+ if (!this.#acceptHit(filter, event)) return false;
894
+ assign(event.created_at);
895
+ return true;
896
+ });
897
+ await Promise.all([firstLive("next", (createdAt) => {
898
+ oldest = createdAt;
899
+ }), firstLive("prev", (createdAt) => {
900
+ newest = createdAt;
901
+ })]);
902
+ await done;
903
+ if (oldest === void 0 || newest === void 0) return;
904
+ return {
905
+ oldest,
906
+ newest
907
+ };
908
+ }
909
+ };
910
+ //#endregion
911
+ //#region src/storage/sqlite.ts
912
+ /** Bound values per `IN (...)` list; stays below every host's variable limit. */
913
+ const IN_CHUNK = 500;
914
+ const SCHEMA_VERSION = 1;
915
+ const SCHEMA_SQL = `
916
+ CREATE TABLE IF NOT EXISTS events (
917
+ id TEXT PRIMARY KEY,
918
+ pubkey TEXT NOT NULL,
919
+ kind INTEGER NOT NULL,
920
+ created_at INTEGER NOT NULL,
921
+ content TEXT NOT NULL,
922
+ tags TEXT NOT NULL,
923
+ sig TEXT NOT NULL,
924
+ address TEXT
925
+ );
926
+ CREATE UNIQUE INDEX IF NOT EXISTS events_address
927
+ ON events(address) WHERE address IS NOT NULL;
928
+ CREATE INDEX IF NOT EXISTS events_kind_created ON events(kind, created_at DESC, id);
929
+ CREATE INDEX IF NOT EXISTS events_pubkey_kind_created
930
+ ON events(pubkey, kind, created_at DESC, id);
931
+ CREATE INDEX IF NOT EXISTS events_pubkey_created ON events(pubkey, created_at DESC, id);
932
+ CREATE INDEX IF NOT EXISTS events_created ON events(created_at DESC, id);
933
+ CREATE TABLE IF NOT EXISTS tags (
934
+ event_id TEXT NOT NULL REFERENCES events(id) ON DELETE CASCADE,
935
+ name TEXT NOT NULL,
936
+ value TEXT NOT NULL,
937
+ created_at INTEGER NOT NULL
938
+ );
939
+ CREATE INDEX IF NOT EXISTS tags_name_value_created
940
+ ON tags(name, value, created_at DESC);
941
+ CREATE TABLE IF NOT EXISTS tombstones (
942
+ kind TEXT NOT NULL CHECK(kind IN ('id', 'pending', 'coord')),
943
+ key TEXT NOT NULL,
944
+ pubkey TEXT,
945
+ until INTEGER,
946
+ PRIMARY KEY(kind, key)
947
+ );
948
+ CREATE TABLE IF NOT EXISTS outbox_bounds (
949
+ pubkey TEXT NOT NULL,
950
+ kind INTEGER NOT NULL,
951
+ oldest INTEGER NOT NULL,
952
+ newest INTEGER NOT NULL,
953
+ PRIMARY KEY(pubkey, kind)
954
+ );
955
+ `;
956
+ function rowToEvent(row) {
957
+ return {
958
+ kind: row.kind,
959
+ tags: JSON.parse(row.tags),
960
+ content: row.content,
961
+ created_at: row.created_at,
962
+ pubkey: row.pubkey,
963
+ id: row.id,
964
+ sig: row.sig
965
+ };
966
+ }
967
+ function chunkValues(values) {
968
+ const chunks = [];
969
+ for (let i = 0; i < values.length; i += IN_CHUNK) chunks.push(values.slice(i, i + IN_CHUNK));
970
+ return chunks;
971
+ }
972
+ function inClause(column, count) {
973
+ return `${column} IN (${Array.from({ length: count }, () => "?").join(",")})`;
974
+ }
975
+ /**
976
+ * Compile one NIP-01 filter into query variants. `IN` lists are chunked so a variant never binds
977
+ * more than {@link IN_CHUNK} values per list; variants are the cartesian product of chunks across
978
+ * constrained fields. `deferred` means a `#<multi-char>` tag term cannot use the tags index (only
979
+ * single-letter tag names are indexed) and must be checked with `matchFilter` before `limit`.
980
+ * Returns `undefined` when an empty list field makes the filter match nothing.
981
+ */
982
+ function compileFilter(filter) {
983
+ const variants = [];
984
+ const pushIn = (values, clause) => {
985
+ variants.push(chunkValues(values).map((chunk) => ({
986
+ wheres: [clause(chunk.length)],
987
+ params: [...chunk]
988
+ })));
989
+ };
990
+ if (filter.ids) {
991
+ if (filter.ids.length === 0) return;
992
+ pushIn(filter.ids.map((id) => id.toLowerCase()), (n) => inClause("id", n));
993
+ }
994
+ if (filter.authors) {
995
+ if (filter.authors.length === 0) return;
996
+ pushIn(filter.authors.map((pk) => pk.toLowerCase()), (n) => inClause("pubkey", n));
997
+ }
998
+ if (filter.kinds) {
999
+ if (filter.kinds.length === 0) return;
1000
+ pushIn(filter.kinds, (n) => inClause("kind", n));
1001
+ }
1002
+ if (filter.since !== void 0) variants.push([{
1003
+ wheres: ["created_at >= ?"],
1004
+ params: [filter.since]
1005
+ }]);
1006
+ if (filter.until !== void 0) variants.push([{
1007
+ wheres: ["created_at <= ?"],
1008
+ params: [filter.until]
1009
+ }]);
1010
+ let deferred = false;
1011
+ for (const key of Object.keys(filter)) {
1012
+ if (!key.startsWith("#")) continue;
1013
+ const name = key.slice(1);
1014
+ const values = filter[key];
1015
+ if (!values) continue;
1016
+ if (name.length !== 1) {
1017
+ deferred = true;
1018
+ continue;
1019
+ }
1020
+ if (values.length === 0) return;
1021
+ const normalized = name === "e" || name === "p" ? values.map((v) => v.toLowerCase()) : values;
1022
+ variants.push(chunkValues(normalized).map((chunk) => ({
1023
+ wheres: [`EXISTS (SELECT 1 FROM tags t WHERE t.event_id = events.id AND t.name = ? AND ${inClause("t.value", chunk.length)})`],
1024
+ params: [name, ...chunk]
1025
+ })));
1026
+ }
1027
+ const plans = [{
1028
+ wheres: [],
1029
+ params: []
1030
+ }];
1031
+ for (const field of variants) {
1032
+ const next = [];
1033
+ for (const plan of plans) for (const variant of field) next.push({
1034
+ wheres: [...plan.wheres, ...variant.wheres],
1035
+ params: [...plan.params, ...variant.params]
1036
+ });
1037
+ plans.length = 0;
1038
+ plans.push(...next);
1039
+ }
1040
+ return {
1041
+ plans,
1042
+ deferred
1043
+ };
1044
+ }
1045
+ /**
1046
+ * SQLite-backed {@link EventStore} for React Native (`expo-sqlite`, `op-sqlite`) and desktop
1047
+ * runtimes. Same semantics as {@link MemoryEventStore} and {@link IndexedDbEventStore}: identical
1048
+ * `decidePut` insertion policy, NIP-09 tombstones, and filter handling, with the database — not
1049
+ * in-memory caches — as the source of truth.
1050
+ *
1051
+ * Construct via {@link SqliteEventStore.open}; writes are serialized and run inside an exclusive
1052
+ * driver transaction.
1053
+ */
1054
+ var SqliteEventStore = class SqliteEventStore {
1055
+ #driver;
1056
+ #writeTail = Promise.resolve();
1057
+ constructor(driver) {
1058
+ this.#driver = driver;
1059
+ }
1060
+ /** Create the schema on first use and return a ready store. */
1061
+ static async open(driver) {
1062
+ const store = new SqliteEventStore(driver);
1063
+ await store.#migrate();
1064
+ return store;
1065
+ }
1066
+ async #migrate() {
1067
+ try {
1068
+ await this.#driver.exec("PRAGMA foreign_keys = ON");
1069
+ const version = (await this.#driver.all("PRAGMA user_version"))[0]?.user_version ?? 0;
1070
+ if (version === 0) {
1071
+ await this.#driver.exec(SCHEMA_SQL);
1072
+ await this.#driver.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
1073
+ } else if (version !== SCHEMA_VERSION) throw toStorageError(/* @__PURE__ */ new Error(`unsupported sqlite event store schema version ${version}`));
1074
+ } catch (error) {
1075
+ throw toStorageError(error);
1076
+ }
1077
+ }
1078
+ async #enqueueWrite(op) {
1079
+ const tail = this.#writeTail;
1080
+ const result = (async () => {
1081
+ await tail;
1082
+ return op();
1083
+ })();
1084
+ this.#writeTail = (async () => {
1085
+ try {
1086
+ await result;
1087
+ } catch {}
1088
+ })();
1089
+ return result;
1090
+ }
1091
+ async put(event) {
1092
+ const result = (await this.putMany([event])).at(0);
1093
+ if (result === void 0) throw toStorageError(/* @__PURE__ */ new Error("putMany returned no result"));
1094
+ return result;
1095
+ }
1096
+ async putMany(events) {
1097
+ if (events.length === 0) return [];
1098
+ return this.#enqueueWrite(async () => {
1099
+ try {
1100
+ return await this.#driver.transaction(async (tx) => this.#putAllInTx(tx, events));
1101
+ } catch (error) {
1102
+ throw toStorageError(error);
1103
+ }
1104
+ });
1105
+ }
1106
+ async #putAllInTx(tx, batch) {
1107
+ const results = [];
1108
+ for (const event of batch) {
1109
+ const lookup = await this.#buildLookup(tx, event);
1110
+ const decision = decidePut(event, lookup);
1111
+ await this.#applyDecision(tx, decision);
1112
+ results.push(decision.result);
1113
+ }
1114
+ return results;
1115
+ }
1116
+ async #buildLookup(tx, event) {
1117
+ const byId = /* @__PURE__ */ new Map();
1118
+ const idList = [event.id];
1119
+ if (event.kind === Kind.EventDeletion) {
1120
+ for (const tag of event.tags) if (tag[0] === "e" && tag[1] !== void 0 && tag[1] !== "") idList.push(tag[1].toLowerCase());
1121
+ }
1122
+ for (const row of await this.#eventRowsByIds(tx, idList)) byId.set(row.id, row);
1123
+ const byAddress = /* @__PURE__ */ new Map();
1124
+ const addressList = [];
1125
+ const ownAddress = eventAddress(event);
1126
+ if (ownAddress !== void 0) addressList.push(ownAddress);
1127
+ if (event.kind === Kind.EventDeletion) for (const tag of event.tags) {
1128
+ if (tag[0] !== "a" || tag[1] === void 0 || tag[1] === "") continue;
1129
+ const coord = parseEventAddress(tag[1]);
1130
+ if (coord === void 0) continue;
1131
+ const key = formatEventAddress(coord.kind, coord.pubkey, coord.identifier);
1132
+ if (!addressList.includes(key)) addressList.push(key);
1133
+ }
1134
+ const addressRows = [];
1135
+ for (const chunk of chunkValues(addressList)) addressRows.push(...await tx.all(`SELECT address, id, created_at FROM events
1136
+ WHERE ${inClause("address", chunk.length)}`, chunk));
1137
+ for (const row of addressRows.flat()) byAddress.set(row.address, {
1138
+ id: row.id,
1139
+ created_at: row.created_at
1140
+ });
1141
+ const deletion = new DeletionState();
1142
+ const tombstoneRows = await tx.all(ownAddress === void 0 ? `SELECT kind, key, pubkey, until FROM tombstones
1143
+ WHERE kind IN ('id', 'pending') AND key = ?` : `SELECT kind, key, pubkey, until FROM tombstones
1144
+ WHERE (kind IN ('id', 'pending') AND key = ?)
1145
+ OR (kind = 'coord' AND key = ?)`, ownAddress === void 0 ? [event.id] : [event.id, ownAddress]);
1146
+ for (const row of tombstoneRows) if (row.kind === "id") deletion.ids.add(row.key);
1147
+ else if (row.kind === "pending" && row.pubkey !== null) deletion.pending.set(row.key, row.pubkey);
1148
+ else if (row.kind === "coord" && row.until !== null) deletion.coordinates.set(row.key, row.until);
1149
+ return {
1150
+ deletion,
1151
+ getById: (id) => byId.get(id),
1152
+ getReplaceable: (address) => byAddress.get(address)
1153
+ };
1154
+ }
1155
+ async #eventRowsByIds(tx, ids) {
1156
+ const events = [];
1157
+ for (const chunk of chunkValues(ids)) {
1158
+ const rows = await tx.all(`SELECT id, pubkey, kind, created_at, content, tags, sig FROM events
1159
+ WHERE ${inClause("id", chunk.length)}`, chunk);
1160
+ for (const row of rows) events.push(rowToEvent(row));
1161
+ }
1162
+ return events;
1163
+ }
1164
+ async #applyDecision(tx, d) {
1165
+ switch (d.action) {
1166
+ case "skip": return;
1167
+ case "tombstone":
1168
+ await this.#putTombstone(tx, "id", d.event.id);
1169
+ await this.#deleteTombstone(tx, "pending", d.event.id);
1170
+ return;
1171
+ case "delete":
1172
+ await this.#persistPlan(tx, d.event.id, d.plan, d.coordIds);
1173
+ for (const id of [...d.plan.removeIds, ...d.coordIds]) await this.#deleteEvent(tx, id);
1174
+ await this.#insertEvent(tx, d.event);
1175
+ return;
1176
+ case "insert":
1177
+ if (d.replaceId !== void 0) await this.#deleteEvent(tx, d.replaceId);
1178
+ await this.#insertEvent(tx, d.event);
1179
+ }
1180
+ }
1181
+ /** Persist a deletion plan's tombstones, mirroring `DeletionState.absorb`. */
1182
+ async #persistPlan(tx, deletionId, plan, coordIds) {
1183
+ await this.#deleteTombstone(tx, "pending", deletionId);
1184
+ for (const id of [...plan.removeIds, ...coordIds]) {
1185
+ await this.#putTombstone(tx, "id", id);
1186
+ await this.#deleteTombstone(tx, "pending", id);
1187
+ }
1188
+ for (const p of plan.pendingIds) if ((await tx.all(`SELECT 1 AS found FROM tombstones WHERE kind = 'id' AND key = ?`, [p.id])).length === 0) await tx.run(`INSERT OR REPLACE INTO tombstones (kind, key, pubkey) VALUES ('pending', ?, ?)`, [p.id, p.pubkey]);
1189
+ for (const c of plan.coordinates) await tx.run(`INSERT INTO tombstones (kind, key, until) VALUES ('coord', ?, ?)
1190
+ ON CONFLICT(kind, key)
1191
+ DO UPDATE SET until = MAX(tombstones.until, excluded.until)`, [c.key, c.until]);
1192
+ }
1193
+ async #putTombstone(tx, kind, key) {
1194
+ await tx.run(`INSERT OR REPLACE INTO tombstones (kind, key) VALUES (?, ?)`, [kind, key]);
1195
+ }
1196
+ async #deleteTombstone(tx, kind, key) {
1197
+ await tx.run(`DELETE FROM tombstones WHERE kind = ? AND key = ?`, [kind, key]);
1198
+ }
1199
+ async #insertEvent(tx, event) {
1200
+ await tx.run(`INSERT INTO events (id, pubkey, kind, created_at, content, tags, sig, address)
1201
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
1202
+ event.id,
1203
+ event.pubkey,
1204
+ event.kind,
1205
+ event.created_at,
1206
+ event.content,
1207
+ JSON.stringify(event.tags),
1208
+ event.sig,
1209
+ eventAddress(event) ?? null
1210
+ ]);
1211
+ for (const tag of event.tags) {
1212
+ if (tag[0] === void 0 || tag[1] === void 0 || tag[0].length !== 1) continue;
1213
+ await tx.run(`INSERT INTO tags (event_id, name, value, created_at) VALUES (?, ?, ?, ?)`, [
1214
+ event.id,
1215
+ tag[0],
1216
+ tag[0] === "e" || tag[0] === "p" ? tag[1].toLowerCase() : tag[1],
1217
+ event.created_at
1218
+ ]);
1219
+ }
1220
+ }
1221
+ async #deleteEvent(tx, id) {
1222
+ return (await tx.run(`DELETE FROM events WHERE id = ?`, [id])).changes;
1223
+ }
1224
+ async get(id) {
1225
+ try {
1226
+ const row = (await this.#driver.all(`SELECT id, pubkey, kind, created_at, content, tags, sig FROM events
1227
+ WHERE id = ?`, [id.toLowerCase()])).at(0);
1228
+ return row === void 0 ? void 0 : rowToEvent(row);
1229
+ } catch (error) {
1230
+ throw toStorageError(error);
1231
+ }
1232
+ }
1233
+ async query(filters) {
1234
+ try {
1235
+ const perFilter = [];
1236
+ for (const filter of filters) perFilter.push(await this.#filterRows(filter, "*"));
1237
+ const seen = /* @__PURE__ */ new Set();
1238
+ const events = [];
1239
+ for (const rows of perFilter) for (const event of rows) {
1240
+ if (seen.has(event.id)) continue;
1241
+ seen.add(event.id);
1242
+ events.push(event);
1243
+ }
1244
+ return sortEvents(events);
1245
+ } catch (error) {
1246
+ throw toStorageError(error);
1247
+ }
1248
+ }
1249
+ async count(filters) {
1250
+ try {
1251
+ const perFilter = [];
1252
+ for (const filter of filters) perFilter.push(await this.#filterRows(filter, "id, created_at"));
1253
+ const seen = /* @__PURE__ */ new Set();
1254
+ for (const rows of perFilter) for (const row of rows) seen.add(row.id);
1255
+ return seen.size;
1256
+ } catch (error) {
1257
+ throw toStorageError(error);
1258
+ }
1259
+ }
1260
+ async negentropyItems(filter) {
1261
+ try {
1262
+ return [...await this.#filterRows(filter, "id, created_at")].sort(itemCompare);
1263
+ } catch (error) {
1264
+ throw toStorageError(error);
1265
+ }
1266
+ }
1267
+ async #filterRows(filter, select) {
1268
+ if (filter.limit === 0) return [];
1269
+ const compiled = compileFilter(filter);
1270
+ if (compiled === void 0) return [];
1271
+ const { plans, deferred } = compiled;
1272
+ if (deferred) {
1273
+ const matched = (await this.#planRows(plans, "*", void 0)).map(rowToEvent).filter((event) => matchFilter(filter, event));
1274
+ const limited = filter.limit === void 0 ? matched : matched.slice(0, filter.limit);
1275
+ return select === "*" ? limited : limited.map((e) => ({
1276
+ id: e.id,
1277
+ created_at: e.created_at
1278
+ }));
1279
+ }
1280
+ if (select === "*") return (await this.#planRows(plans, "*", filter.limit)).map(rowToEvent);
1281
+ return this.#planRows(plans, "id, created_at", filter.limit);
1282
+ }
1283
+ /** Rows for the compiled plans of one filter, deduped newest-first, `limit` applied. */
1284
+ async #planRows(plans, select, limit) {
1285
+ const tail = ` ORDER BY created_at DESC, id ASC${limit === void 0 ? "" : " LIMIT ?"}`;
1286
+ const perPlan = [];
1287
+ for (const plan of plans) {
1288
+ const where = plan.wheres.length > 0 ? ` WHERE ${plan.wheres.join(" AND ")}` : "";
1289
+ const params = limit === void 0 ? plan.params : [...plan.params, limit];
1290
+ perPlan.push(await this.#driver.all(`SELECT ${select} FROM events${where}${tail}`, params));
1291
+ }
1292
+ const sorted = perPlan.flat().sort(compareEventsDesc);
1293
+ const seen = /* @__PURE__ */ new Set();
1294
+ const merged = [];
1295
+ for (const row of sorted) {
1296
+ if (seen.has(row.id)) continue;
1297
+ seen.add(row.id);
1298
+ merged.push(row);
1299
+ }
1300
+ return limit === void 0 ? merged : merged.slice(0, limit);
1301
+ }
1302
+ async getOutboxBound(pubkey, kind) {
1303
+ const pk = pubkey.toLowerCase();
1304
+ try {
1305
+ const row = (await this.#driver.all(`SELECT oldest, newest FROM outbox_bounds
1306
+ WHERE pubkey = ? AND kind = ?`, [pk, kind])).at(0);
1307
+ if (row !== void 0) return {
1308
+ oldest: row.oldest,
1309
+ newest: row.newest
1310
+ };
1311
+ const d = (await this.#driver.all(`SELECT MIN(created_at) AS oldest, MAX(created_at) AS newest
1312
+ FROM events WHERE pubkey = ? AND kind = ?`, [pk, kind])).at(0);
1313
+ if (d === void 0 || d.oldest === null || d.newest === null) return;
1314
+ return {
1315
+ oldest: d.oldest,
1316
+ newest: d.newest
1317
+ };
1318
+ } catch (error) {
1319
+ throw toStorageError(error);
1320
+ }
1321
+ }
1322
+ async setOutboxBound(pubkey, kind, bound) {
1323
+ const pk = pubkey.toLowerCase();
1324
+ await this.#enqueueWrite(async () => {
1325
+ try {
1326
+ await this.#driver.transaction(async (tx) => {
1327
+ await tx.run(`INSERT OR REPLACE INTO outbox_bounds (pubkey, kind, oldest, newest)
1328
+ VALUES (?, ?, ?, ?)`, [
1329
+ pk,
1330
+ kind,
1331
+ bound.oldest,
1332
+ bound.newest
1333
+ ]);
1334
+ });
1335
+ } catch (error) {
1336
+ throw toStorageError(error);
1337
+ }
1338
+ });
1339
+ }
1340
+ async remove(ids) {
1341
+ const lowered = ids.map((id) => id.toLowerCase());
1342
+ return this.#enqueueWrite(async () => {
1343
+ try {
1344
+ return await this.#driver.transaction(async (tx) => {
1345
+ let removed = 0;
1346
+ for (const id of lowered) {
1347
+ removed += await this.#deleteEvent(tx, id);
1348
+ await this.#putTombstone(tx, "id", id);
1349
+ await this.#deleteTombstone(tx, "pending", id);
1350
+ }
1351
+ return removed;
1352
+ });
1353
+ } catch (error) {
1354
+ throw toStorageError(error);
1355
+ }
1356
+ });
1357
+ }
1358
+ async clear() {
1359
+ await this.#enqueueWrite(async () => {
1360
+ try {
1361
+ await this.#driver.transaction(async (tx) => {
1362
+ await tx.run("DELETE FROM tags");
1363
+ await tx.run("DELETE FROM events");
1364
+ await tx.run("DELETE FROM tombstones");
1365
+ await tx.run("DELETE FROM outbox_bounds");
1366
+ });
1367
+ } catch (error) {
1368
+ throw toStorageError(error);
1369
+ }
1370
+ });
1371
+ }
1372
+ };
1373
+ //#endregion
1374
+ export { IndexedDbEventStore as n, SqliteEventStore as t };
1375
+
1376
+ //# sourceMappingURL=storage-xXxLGCVb.mjs.map