@welshman/signer 0.14.0-pre3 → 0.14.0-pre5

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 (104) hide show
  1. package/dist/net/src/adapter.d.ts +3 -2
  2. package/dist/net/src/adapter.d.ts.map +1 -1
  3. package/dist/net/src/adapter.js +29 -16
  4. package/dist/net/src/adapter.js.map +1 -1
  5. package/dist/net/src/context.d.ts +1 -1
  6. package/dist/net/src/context.d.ts.map +1 -1
  7. package/dist/net/src/index.d.ts +0 -2
  8. package/dist/net/src/index.d.ts.map +1 -1
  9. package/dist/net/src/index.js +0 -2
  10. package/dist/net/src/index.js.map +1 -1
  11. package/dist/net/src/message.d.ts +10 -0
  12. package/dist/net/src/message.d.ts.map +1 -1
  13. package/dist/net/src/message.js +22 -0
  14. package/dist/net/src/message.js.map +1 -1
  15. package/dist/net/src/multiplex.d.ts +2 -1
  16. package/dist/net/src/multiplex.d.ts.map +1 -1
  17. package/dist/net/src/multiplex.js +24 -15
  18. package/dist/net/src/multiplex.js.map +1 -1
  19. package/dist/net/src/policy.d.ts.map +1 -1
  20. package/dist/net/src/policy.js +3 -3
  21. package/dist/net/src/policy.js.map +1 -1
  22. package/dist/net/src/request.d.ts +2 -1
  23. package/dist/net/src/request.d.ts.map +1 -1
  24. package/dist/net/src/request.js +23 -9
  25. package/dist/net/src/request.js.map +1 -1
  26. package/dist/net/src/wrapManager.d.ts +3 -4
  27. package/dist/net/src/wrapManager.d.ts.map +1 -1
  28. package/dist/net/src/wrapManager.js +8 -6
  29. package/dist/net/src/wrapManager.js.map +1 -1
  30. package/dist/store/src/derive.d.ts +86 -0
  31. package/dist/store/src/derive.d.ts.map +1 -0
  32. package/dist/store/src/derive.js +706 -0
  33. package/dist/store/src/derive.js.map +1 -0
  34. package/dist/store/src/index.d.ts +12 -0
  35. package/dist/store/src/index.d.ts.map +1 -0
  36. package/dist/store/src/index.js +12 -0
  37. package/dist/store/src/index.js.map +1 -0
  38. package/dist/store/src/misc.d.ts +41 -0
  39. package/dist/store/src/misc.d.ts.map +1 -0
  40. package/dist/store/src/misc.js +88 -0
  41. package/dist/store/src/misc.js.map +1 -0
  42. package/dist/{net/src/repository.d.ts → store/src/repository/adapters/memory.d.ts} +22 -30
  43. package/dist/store/src/repository/adapters/memory.d.ts.map +1 -0
  44. package/dist/{net/src/repository.js → store/src/repository/adapters/memory.js} +96 -106
  45. package/dist/store/src/repository/adapters/memory.js.map +1 -0
  46. package/dist/store/src/repository/adapters/sqlite/engine.d.ts +65 -0
  47. package/dist/store/src/repository/adapters/sqlite/engine.d.ts.map +1 -0
  48. package/dist/store/src/repository/adapters/sqlite/engine.js +420 -0
  49. package/dist/store/src/repository/adapters/sqlite/engine.js.map +1 -0
  50. package/dist/store/src/repository/adapters/sqlite/leader.d.ts +8 -0
  51. package/dist/store/src/repository/adapters/sqlite/leader.d.ts.map +1 -0
  52. package/dist/store/src/repository/adapters/sqlite/leader.js +222 -0
  53. package/dist/store/src/repository/adapters/sqlite/leader.js.map +1 -0
  54. package/dist/store/src/repository/adapters/sqlite/protocol.d.ts +52 -0
  55. package/dist/store/src/repository/adapters/sqlite/protocol.d.ts.map +1 -0
  56. package/dist/store/src/repository/adapters/sqlite/protocol.js +52 -0
  57. package/dist/store/src/repository/adapters/sqlite/protocol.js.map +1 -0
  58. package/dist/store/src/repository/adapters/sqlite/repository.d.ts +54 -0
  59. package/dist/store/src/repository/adapters/sqlite/repository.d.ts.map +1 -0
  60. package/dist/store/src/repository/adapters/sqlite/repository.js +238 -0
  61. package/dist/store/src/repository/adapters/sqlite/repository.js.map +1 -0
  62. package/dist/store/src/repository/core.d.ts +68 -0
  63. package/dist/store/src/repository/core.d.ts.map +1 -0
  64. package/dist/store/src/repository/core.js +123 -0
  65. package/dist/store/src/repository/core.js.map +1 -0
  66. package/dist/store/src/svelte.d.ts +22 -0
  67. package/dist/store/src/svelte.d.ts.map +1 -0
  68. package/dist/store/src/svelte.js +100 -0
  69. package/dist/store/src/svelte.js.map +1 -0
  70. package/dist/store/src/synced.d.ts +22 -0
  71. package/dist/store/src/synced.d.ts.map +1 -0
  72. package/dist/store/src/synced.js +21 -0
  73. package/dist/store/src/synced.js.map +1 -0
  74. package/dist/{net → store}/src/tracker.d.ts +2 -0
  75. package/dist/store/src/tracker.d.ts.map +1 -0
  76. package/dist/{net → store}/src/tracker.js +8 -4
  77. package/dist/store/src/tracker.js.map +1 -0
  78. package/dist/util/src/Command.d.ts +1 -1
  79. package/dist/util/src/Command.d.ts.map +1 -1
  80. package/dist/util/src/Command.js +12 -5
  81. package/dist/util/src/Command.js.map +1 -1
  82. package/dist/util/src/Events.d.ts +3 -2
  83. package/dist/util/src/Events.d.ts.map +1 -1
  84. package/dist/util/src/Events.js.map +1 -1
  85. package/dist/util/src/Kinds.d.ts +1 -0
  86. package/dist/util/src/Kinds.d.ts.map +1 -1
  87. package/dist/util/src/Kinds.js +1 -0
  88. package/dist/util/src/Kinds.js.map +1 -1
  89. package/dist/util/src/RelaySelection.d.ts +5 -0
  90. package/dist/util/src/RelaySelection.d.ts.map +1 -1
  91. package/dist/util/src/RelaySelection.js.map +1 -1
  92. package/dist/util/src/Tags.d.ts +5 -0
  93. package/dist/util/src/Tags.d.ts.map +1 -1
  94. package/dist/util/src/Tags.js +3 -0
  95. package/dist/util/src/Tags.js.map +1 -1
  96. package/dist/util/src/index.d.ts +1 -0
  97. package/dist/util/src/index.d.ts.map +1 -1
  98. package/dist/util/src/index.js +1 -0
  99. package/dist/util/src/index.js.map +1 -1
  100. package/package.json +7 -7
  101. package/dist/net/src/repository.d.ts.map +0 -1
  102. package/dist/net/src/repository.js.map +0 -1
  103. package/dist/net/src/tracker.d.ts.map +0 -1
  104. package/dist/net/src/tracker.js.map +0 -1
@@ -0,0 +1,706 @@
1
+ import { on, gt, now, noop, defer, mapPop, LRUCache, call, sortBy, first, batch, indexBy, omit, } from "@welshman/lib";
2
+ import { getIdFilters, sortEventsAsc, sortEventsDesc, getIdOrAddress } from "@welshman/util";
3
+ import { derived, readable } from "./svelte.js";
4
+ import { deriveDeduplicated, projection, projectFrom } from "./misc.js";
5
+ /** Build an `AsyncProjection` derived from another, whose `load` reads `src` fresh. */
6
+ export const projectAsync = (src, read) => ({ ...projectFrom(src, read), load: async () => read(await src.load()) });
7
+ // The filters narrowed to `ids` and unlimited, for checking which of them match
8
+ const narrowFilters = (filters, ids) => filters.map(filter => ({
9
+ ...omit(["limit"], filter),
10
+ ids: filter.ids ? filter.ids.filter(id => ids.includes(id)) : ids,
11
+ }));
12
+ export const getEventsById = async ({ filters, repository, includeDeleted }) => {
13
+ const events = await repository.query(filters, { includeDeleted });
14
+ return indexBy(event => event.id, events);
15
+ };
16
+ // Keeps `eventsById` in step with the repository, refilling it when the read answers
17
+ const syncEventsById = (eventsById, set) => ({
18
+ onLoad: events => {
19
+ eventsById.clear();
20
+ for (const event of events) {
21
+ eventsById.set(event.id, event);
22
+ }
23
+ set(eventsById);
24
+ },
25
+ onUpdate: ({ added, removed }) => {
26
+ let dirty = false;
27
+ for (const event of added) {
28
+ eventsById.set(event.id, event);
29
+ dirty = true;
30
+ }
31
+ for (const id of removed) {
32
+ dirty = eventsById.delete(id) || dirty;
33
+ }
34
+ if (dirty) {
35
+ set(eventsById);
36
+ }
37
+ },
38
+ });
39
+ /** The events matching `filters`, kept current. It holds its last value until the read answers. */
40
+ export const deriveEventsById = (options) => {
41
+ const { filters, repository, includeDeleted } = options;
42
+ const eventsById = new Map();
43
+ const $ = readable(eventsById, set => repository.observe({ filters, includeDeleted, ...syncEventsById(eventsById, set) }));
44
+ return { ...projection($, () => eventsById), load: () => getEventsById(options) };
45
+ };
46
+ /** Calls `onEvents` with the matches once the read answers, then on each change, until stopped. */
47
+ export const observeEventsById = ({ filters, repository, includeDeleted, onEvents, }) => {
48
+ const { onLoad, onUpdate } = syncEventsById(new Map(), onEvents);
49
+ let loaded = false;
50
+ return repository.observe({
51
+ filters,
52
+ includeDeleted,
53
+ onLoad: events => {
54
+ loaded = true;
55
+ onLoad(events);
56
+ },
57
+ // What changes before the read answers is in what onLoad receives
58
+ onUpdate: update => {
59
+ if (loaded) {
60
+ onUpdate(update);
61
+ }
62
+ },
63
+ });
64
+ };
65
+ const toArray = (itemsById) => Array.from(itemsById.values());
66
+ export const deriveArray = (itemsById) => projectFrom(itemsById, toArray);
67
+ export const deriveEventsArray = (eventsById) => projectAsync(eventsById, toArray);
68
+ export const deriveEventsAsc = (eventsById) => projectAsync(eventsById, $eventsById => sortEventsAsc($eventsById.values()));
69
+ export const deriveEventsDesc = (eventsById) => projectAsync(eventsById, $eventsById => sortEventsDesc($eventsById.values()));
70
+ export const deriveEvents = (options) => deriveEventsArray(deriveEventsById(options));
71
+ export const makeDeriveEvent = ({ repository, includeDeleted = false, onDerive }) => {
72
+ return (idOrAddress, ...args) => {
73
+ const filters = getIdFilters([idOrAddress]);
74
+ const load = async () => {
75
+ const events = await repository.query(filters, { includeDeleted });
76
+ return first(events);
77
+ };
78
+ let event;
79
+ onDerive?.(filters, ...args);
80
+ const $ = readable(event, set => repository.observe({
81
+ filters,
82
+ includeDeleted,
83
+ onLoad: events => {
84
+ event = first(events);
85
+ set(event);
86
+ },
87
+ onUpdate: ({ added, removed }) => {
88
+ if (event && removed.has(event.id)) {
89
+ event = undefined;
90
+ set(event);
91
+ }
92
+ for (const newEvent of added) {
93
+ event = newEvent;
94
+ set(event);
95
+ }
96
+ },
97
+ }));
98
+ return { ...projection($, () => event), load };
99
+ };
100
+ };
101
+ export const getEventsByIdByUrl = async ({ filters, repository, includeDeleted, }) => {
102
+ const eventsByIdByUrl = new Map();
103
+ const events = await repository.query(filters, { includeDeleted });
104
+ for (const event of events) {
105
+ for (const url of repository.tracker.getRelays(event.id)) {
106
+ let eventsById = eventsByIdByUrl.get(url);
107
+ if (!eventsById) {
108
+ eventsById = new Map();
109
+ eventsByIdByUrl.set(url, eventsById);
110
+ }
111
+ eventsById.set(event.id, event);
112
+ }
113
+ }
114
+ return eventsByIdByUrl;
115
+ };
116
+ /** The events matching `filters`, grouped by each relay the tracker has seen them on. */
117
+ export const deriveEventsByIdByUrl = (options) => {
118
+ const { filters, repository, includeDeleted } = options;
119
+ const { tracker } = repository;
120
+ let eventsByIdByUrl = new Map();
121
+ const $ = readable(eventsByIdByUrl, set => {
122
+ // Every match, keyed or not, so a relay seen later can key it
123
+ const matched = new Map();
124
+ const addEvent = (url, event) => {
125
+ const eventsById = eventsByIdByUrl.get(url);
126
+ // The repository swaps a changed copy of a held event in under the same id.
127
+ if (eventsById?.get(event.id) === event)
128
+ return false;
129
+ // Create a new map so we can detect which key changed
130
+ const newEventsById = new Map(eventsById);
131
+ newEventsById.set(event.id, event);
132
+ eventsByIdByUrl.set(url, newEventsById);
133
+ return true;
134
+ };
135
+ const removeEvent = (url, id) => {
136
+ const eventsById = eventsByIdByUrl.get(url);
137
+ if (eventsById?.has(id)) {
138
+ eventsById.delete(id);
139
+ if (eventsById.size === 0) {
140
+ eventsByIdByUrl.delete(url);
141
+ }
142
+ else {
143
+ // Create a new map so we can detect which key changed
144
+ eventsByIdByUrl.set(url, new Map(eventsById));
145
+ }
146
+ return true;
147
+ }
148
+ return false;
149
+ };
150
+ const observe = () => repository.observe({
151
+ filters,
152
+ includeDeleted,
153
+ onLoad: events => {
154
+ matched.clear();
155
+ eventsByIdByUrl = new Map();
156
+ for (const event of events) {
157
+ matched.set(event.id, event);
158
+ for (const url of tracker.getRelays(event.id)) {
159
+ addEvent(url, event);
160
+ }
161
+ }
162
+ set(eventsByIdByUrl);
163
+ },
164
+ onUpdate: ({ added, removed }) => {
165
+ let dirty = false;
166
+ for (const event of added) {
167
+ matched.set(event.id, event);
168
+ for (const url of tracker.getRelays(event.id)) {
169
+ dirty = addEvent(url, event) || dirty;
170
+ }
171
+ }
172
+ for (const id of removed) {
173
+ matched.delete(id);
174
+ for (const url of tracker.getRelays(id)) {
175
+ dirty = removeEvent(url, id) || dirty;
176
+ }
177
+ }
178
+ if (dirty) {
179
+ set(eventsByIdByUrl);
180
+ }
181
+ },
182
+ });
183
+ let stop = observe();
184
+ const unsubscribers = [
185
+ on(tracker, "add", (id, url) => {
186
+ const event = matched.get(id);
187
+ if (event && addEvent(url, event)) {
188
+ set(eventsByIdByUrl);
189
+ }
190
+ }),
191
+ on(tracker, "remove", (id, url) => {
192
+ if (removeEvent(url, id)) {
193
+ set(eventsByIdByUrl);
194
+ }
195
+ }),
196
+ on(tracker, "load", () => {
197
+ stop();
198
+ stop = observe();
199
+ }),
200
+ on(tracker, "clear", () => {
201
+ eventsByIdByUrl = new Map();
202
+ set(eventsByIdByUrl);
203
+ }),
204
+ ];
205
+ return () => {
206
+ stop();
207
+ unsubscribers.forEach(call);
208
+ };
209
+ });
210
+ return { ...projection($, () => eventsByIdByUrl), load: () => getEventsByIdByUrl(options) };
211
+ };
212
+ /** The events one relay has been seen to hold, with any limit counting only those events. */
213
+ export const getEventsByIdForUrl = async ({ url, filters, repository, includeDeleted, }) => {
214
+ const events = await repository.query(filters, { includeDeleted, relay: url });
215
+ return indexBy(event => event.id, events);
216
+ };
217
+ const onLookUpError = (error) => console.error("Failed to look up events seen on a relay", error);
218
+ export const deriveEventsByIdForUrl = (options) => {
219
+ const { url, filters, repository, includeDeleted } = options;
220
+ const { tracker } = repository;
221
+ const eventsById = new Map();
222
+ const $ = readable(eventsById, set => {
223
+ const sync = syncEventsById(eventsById, set);
224
+ // Ids seen on the relay while the read is in flight, which the read may well answer itself
225
+ let seenWhileReading;
226
+ let stopped = false;
227
+ const observe = () => {
228
+ seenWhileReading = new Set();
229
+ return repository.observe({
230
+ filters,
231
+ includeDeleted,
232
+ relay: url,
233
+ onUpdate: sync.onUpdate,
234
+ onLoad: events => {
235
+ const seen = seenWhileReading ?? [];
236
+ seenWhileReading = undefined;
237
+ sync.onLoad(events);
238
+ for (const id of seen) {
239
+ if (!eventsById.has(id)) {
240
+ check(id);
241
+ }
242
+ }
243
+ },
244
+ });
245
+ };
246
+ // Look up stored events seen on the relay later, observing so a removal meanwhile stands
247
+ const lookUp = (ids) => {
248
+ const stopLookUp = repository.observe({
249
+ filters: narrowFilters(filters, ids),
250
+ includeDeleted,
251
+ onUpdate: noop,
252
+ onError: error => {
253
+ stopLookUp();
254
+ onLookUpError(error);
255
+ },
256
+ onLoad: events => {
257
+ let dirty = false;
258
+ stopLookUp();
259
+ for (const event of events) {
260
+ if (!stopped && tracker.hasRelay(event.id, url) && !eventsById.has(event.id)) {
261
+ eventsById.set(event.id, event);
262
+ dirty = true;
263
+ }
264
+ }
265
+ if (dirty) {
266
+ set(eventsById);
267
+ }
268
+ },
269
+ });
270
+ };
271
+ const check = batch(100, (ids) => {
272
+ if (!stopped)
273
+ lookUp(ids);
274
+ });
275
+ const reset = () => {
276
+ stop();
277
+ stop = observe();
278
+ };
279
+ let stop = observe();
280
+ const unsubscribers = [
281
+ on(tracker, "add", (id, trackedUrl) => {
282
+ if (trackedUrl === url && !eventsById.has(id)) {
283
+ if (seenWhileReading) {
284
+ seenWhileReading.add(id);
285
+ }
286
+ else {
287
+ check(id);
288
+ }
289
+ }
290
+ }),
291
+ on(tracker, "remove", (id, trackedUrl) => {
292
+ if (trackedUrl === url && eventsById.delete(id)) {
293
+ set(eventsById);
294
+ }
295
+ }),
296
+ on(tracker, "load", reset),
297
+ on(tracker, "clear", reset),
298
+ ];
299
+ return () => {
300
+ stopped = true;
301
+ stop();
302
+ unsubscribers.forEach(call);
303
+ };
304
+ });
305
+ return { ...projection($, () => eventsById), load: () => getEventsByIdForUrl(options) };
306
+ };
307
+ const asCollection = ($, itemsByKey, pending, repository) => {
308
+ const self = projection($, () => itemsByKey);
309
+ // While subscribed, wait out pending writes, then the query and the parses outstanding after it
310
+ const read = (key) => {
311
+ const writes = repository.pendingWrites();
312
+ return ((writes ? undefined : itemsByKey.get(key)) ??
313
+ call(async () => {
314
+ const unsubscribe = $.subscribe(noop);
315
+ try {
316
+ await writes;
317
+ if (!itemsByKey.has(key)) {
318
+ await pending.get(QUERY);
319
+ await Promise.all(pending.values());
320
+ }
321
+ return itemsByKey.get(key);
322
+ }
323
+ finally {
324
+ unsubscribe();
325
+ }
326
+ }));
327
+ };
328
+ return { ...self, read };
329
+ };
330
+ const onParseError = (error) => console.warn("Failed to build an item from an event", error);
331
+ // A sync parse's item, or undefined while an async one waits in `pending` to reach `later`
332
+ const buildItem = (event, eventToItem, pending, later) => {
333
+ let item;
334
+ try {
335
+ const itemOrPromise = eventToItem(event);
336
+ if (itemOrPromise instanceof Promise) {
337
+ const isPending = () => pending.get(event.id) === parse;
338
+ const parse = itemOrPromise
339
+ .then(parsed => {
340
+ if (isPending())
341
+ later(parsed);
342
+ })
343
+ .catch(onParseError)
344
+ .finally(() => {
345
+ if (isPending())
346
+ pending.delete(event.id);
347
+ });
348
+ pending.set(event.id, parse);
349
+ }
350
+ else {
351
+ item = itemOrPromise;
352
+ }
353
+ }
354
+ catch (error) {
355
+ onParseError(error);
356
+ }
357
+ return item;
358
+ };
359
+ // The read a subscription starts is tracked with the parses, so `read` waits for both
360
+ const QUERY = Symbol("query");
361
+ const onCollectionError = (error) => console.error("Failed to read a collection", error);
362
+ // Observe the repository, recording the read in `pending` until it answers or the observer stops
363
+ const observePending = (repository, pending, options) => {
364
+ const loaded = defer();
365
+ const done = () => {
366
+ if (pending.get(QUERY) === loaded)
367
+ pending.delete(QUERY);
368
+ };
369
+ pending.set(QUERY, loaded);
370
+ const stop = repository.observe({
371
+ ...options,
372
+ onLoad: events => {
373
+ options.onLoad?.(events);
374
+ done();
375
+ loaded.resolve();
376
+ },
377
+ // A failed read leaves the index as it was, which is what a waiting `read` answers from
378
+ onError: error => {
379
+ onCollectionError(error);
380
+ done();
381
+ loaded.resolve();
382
+ },
383
+ });
384
+ return () => {
385
+ stop();
386
+ done();
387
+ loaded.resolve();
388
+ };
389
+ };
390
+ export const deriveItemsByKey = ({ getKey, filters, repository, eventToItem, includeDeleted, }) => {
391
+ const pending = new Map();
392
+ const itemsByKey = new Map();
393
+ // The event owning each key, so removing a superseded event leaves its successor's entry
394
+ const ownerByKey = new Map();
395
+ // The key each event contributed, for teardown.
396
+ const keyById = new Map();
397
+ const $ = readable(itemsByKey, set => {
398
+ const commit = (id, item) => {
399
+ if (item) {
400
+ const key = getKey(item);
401
+ itemsByKey.set(key, item);
402
+ ownerByKey.set(key, id);
403
+ keyById.set(id, key);
404
+ }
405
+ return Boolean(item);
406
+ };
407
+ const addEvent = (event) => {
408
+ let added = false;
409
+ if (!pending.has(event.id) && !keyById.has(event.id)) {
410
+ const item = buildItem(event, eventToItem, pending, parsed => {
411
+ if (commit(event.id, parsed))
412
+ set(itemsByKey);
413
+ });
414
+ added = commit(event.id, item);
415
+ }
416
+ return added;
417
+ };
418
+ const removeEvent = (id) => {
419
+ const key = mapPop(id, keyById);
420
+ const owns = key !== undefined && ownerByKey.get(key) === id;
421
+ pending.delete(id);
422
+ if (owns) {
423
+ ownerByKey.delete(key);
424
+ itemsByKey.delete(key);
425
+ }
426
+ return owns;
427
+ };
428
+ return observePending(repository, pending, {
429
+ filters,
430
+ includeDeleted,
431
+ // The index outlives a subscription, so a subscribe drops what left the repository meanwhile
432
+ onLoad: events => {
433
+ const ids = new Set(events.map(event => event.id));
434
+ let dirty = false;
435
+ for (const id of Array.from(keyById.keys())) {
436
+ if (!ids.has(id)) {
437
+ dirty = removeEvent(id) || dirty;
438
+ }
439
+ }
440
+ for (const event of events) {
441
+ dirty = addEvent(event) || dirty;
442
+ }
443
+ if (dirty)
444
+ set(itemsByKey);
445
+ },
446
+ onUpdate: ({ added, removed }) => {
447
+ let dirty = false;
448
+ for (const event of added) {
449
+ dirty = addEvent(event) || dirty;
450
+ }
451
+ for (const id of removed) {
452
+ dirty = removeEvent(id) || dirty;
453
+ }
454
+ if (dirty)
455
+ set(itemsByKey);
456
+ },
457
+ });
458
+ });
459
+ return asCollection($, itemsByKey, pending, repository);
460
+ };
461
+ /** Like `deriveItemsByKey`, but keying each event once per relay the tracker has seen it on. */
462
+ export const deriveItemsByKeyByUrl = ({ getKey, filters, repository, eventToItem, includeDeleted, revalidateOn, }) => {
463
+ const { tracker } = repository;
464
+ const pending = new Map();
465
+ const itemsByKey = new Map();
466
+ // Which event currently owns each key, as in `deriveItemsByKey`.
467
+ const ownerByKey = new Map();
468
+ // The key each event contributed on each relay url, for teardown.
469
+ const keysById = new Map();
470
+ // What each event parsed to, so re-keying it never parses it again
471
+ const itemsById = new Map();
472
+ const $ = readable(itemsByKey, set => {
473
+ const setKey = (key, id, url, item) => {
474
+ itemsByKey.set(key, item);
475
+ ownerByKey.set(key, id);
476
+ let byUrl = keysById.get(id);
477
+ if (!byUrl) {
478
+ byUrl = new Map();
479
+ keysById.set(id, byUrl);
480
+ }
481
+ byUrl.set(url, key);
482
+ };
483
+ const dropKey = (key, id) => {
484
+ // Only remove if this id still owns the key (guards against a replaceable
485
+ // event having already taken it over).
486
+ if (ownerByKey.get(key) === id) {
487
+ itemsByKey.delete(key);
488
+ ownerByKey.delete(key);
489
+ return true;
490
+ }
491
+ return false;
492
+ };
493
+ // Keys the item on each tracked relay it lacks a key for, including relays seen mid-parse
494
+ const commit = (id, item) => {
495
+ let keyed = false;
496
+ if (item) {
497
+ itemsById.set(id, item);
498
+ for (const url of tracker.getRelays(id)) {
499
+ const key = keysById.get(id)?.has(url) ? undefined : getKey(item, url);
500
+ if (key !== undefined) {
501
+ setKey(key, id, url, item);
502
+ keyed = true;
503
+ }
504
+ }
505
+ }
506
+ return keyed;
507
+ };
508
+ // Commit an item that arrives on its own, as a parse or a relay seen later
509
+ const commitAlone = (id, item) => {
510
+ if (commit(id, item))
511
+ set(itemsByKey);
512
+ };
513
+ const addEvent = (event) => {
514
+ const parsed = itemsById.get(event.id);
515
+ let keyed = false;
516
+ if (parsed) {
517
+ keyed = commit(event.id, parsed);
518
+ }
519
+ else if (!pending.has(event.id)) {
520
+ const item = buildItem(event, eventToItem, pending, later => commitAlone(event.id, later));
521
+ keyed = commit(event.id, item);
522
+ }
523
+ return keyed;
524
+ };
525
+ const removeEvent = (id) => {
526
+ const byUrl = mapPop(id, keysById);
527
+ let dirty = false;
528
+ pending.delete(id);
529
+ itemsById.delete(id);
530
+ for (const key of byUrl?.values() ?? []) {
531
+ dirty = dropKey(key, id) || dirty;
532
+ }
533
+ return dirty;
534
+ };
535
+ const removeEventUrl = (id, url) => {
536
+ const byUrl = keysById.get(id);
537
+ const key = byUrl?.get(url);
538
+ if (!byUrl || key === undefined)
539
+ return false;
540
+ byUrl.delete(url);
541
+ if (byUrl.size === 0) {
542
+ keysById.delete(id);
543
+ }
544
+ return dropKey(key, id);
545
+ };
546
+ const clear = () => {
547
+ const dirty = itemsByKey.size > 0;
548
+ itemsByKey.clear();
549
+ ownerByKey.clear();
550
+ keysById.clear();
551
+ return dirty;
552
+ };
553
+ const stop = observePending(repository, pending, {
554
+ filters,
555
+ includeDeleted,
556
+ onLoad: events => {
557
+ const ids = new Set(events.map(event => event.id));
558
+ let dirty = false;
559
+ for (const id of Array.from(itemsById.keys())) {
560
+ if (!ids.has(id)) {
561
+ dirty = removeEvent(id) || dirty;
562
+ }
563
+ }
564
+ for (const event of events) {
565
+ dirty = addEvent(event) || dirty;
566
+ }
567
+ if (dirty)
568
+ set(itemsByKey);
569
+ },
570
+ onUpdate: ({ added, removed }) => {
571
+ let dirty = false;
572
+ for (const event of added) {
573
+ dirty = addEvent(event) || dirty;
574
+ }
575
+ for (const id of removed) {
576
+ dirty = removeEvent(id) || dirty;
577
+ }
578
+ if (dirty)
579
+ set(itemsByKey);
580
+ },
581
+ });
582
+ // Re-key every parsed item, since the tracker's relays or `getKey`'s answers have changed
583
+ const rebuild = () => {
584
+ let dirty = clear();
585
+ for (const [id, item] of itemsById) {
586
+ dirty = commit(id, item) || dirty;
587
+ }
588
+ if (dirty)
589
+ set(itemsByKey);
590
+ };
591
+ const unsubscribers = [
592
+ on(tracker, "add", (id) => commitAlone(id, itemsById.get(id))),
593
+ on(tracker, "remove", (id, url) => {
594
+ if (removeEventUrl(id, url)) {
595
+ set(itemsByKey);
596
+ }
597
+ }),
598
+ on(tracker, "load", rebuild),
599
+ on(tracker, "clear", () => {
600
+ if (clear())
601
+ set(itemsByKey);
602
+ }),
603
+ ];
604
+ // Re-evaluate everything when external key state (e.g. relay self pubkeys)
605
+ // changes. The first emission fires synchronously on subscribe and is the
606
+ // initial state we already built, so skip it.
607
+ if (revalidateOn) {
608
+ let initialized = false;
609
+ unsubscribers.push(revalidateOn.subscribe(() => {
610
+ if (initialized) {
611
+ rebuild();
612
+ }
613
+ else {
614
+ initialized = true;
615
+ }
616
+ }));
617
+ }
618
+ return () => {
619
+ stop();
620
+ unsubscribers.forEach(call);
621
+ };
622
+ });
623
+ return asCollection($, itemsByKey, pending, repository);
624
+ };
625
+ export const deriveItems = (itemsByKeyStore) => derived(itemsByKeyStore, itemsByKey => Array.from(itemsByKey.values()));
626
+ export const deriveItemsSorted = (sortFn, itemsStore) => derived(itemsStore, items => sortBy(sortFn, items));
627
+ export const makeDeriveItem = (itemsByKeyStore, onDerive) => {
628
+ return (key, ...args) => {
629
+ if (!key)
630
+ return readable(undefined);
631
+ // Nothing awaits this, so a failed load has nowhere to go
632
+ Promise.resolve(onDerive?.(key, ...args)).catch(noop);
633
+ return deriveDeduplicated(itemsByKeyStore, itemsByKey => itemsByKey.get(key));
634
+ };
635
+ };
636
+ export const makeForceLoadItem = (loadItem, getItem) => {
637
+ return async (key, ...args) => {
638
+ await loadItem(key, ...args);
639
+ return getItem(key);
640
+ };
641
+ };
642
+ export const makeLoadItem = (loadItem, getItem, options = {}) => {
643
+ const timeout = options.timeout || 3600;
644
+ const maxSize = options.maxSize || 10_000;
645
+ const fetched = new LRUCache(maxSize);
646
+ const getFetched = options.getFetched || ((key) => fetched.get(key) || 0);
647
+ const setFetched = options.setFetched || ((key, ts) => fetched.set(key, ts));
648
+ const getSource = options.getSource || ((...args) => JSON.stringify(args));
649
+ const sourceFetched = new LRUCache(maxSize);
650
+ const attempts = new LRUCache(maxSize);
651
+ const pending = new Map();
652
+ return async (key, ...args) => {
653
+ const stale = await getItem(key);
654
+ // If we have an item, reload if it's relatively recent
655
+ if (stale && getFetched(key) > now() - timeout) {
656
+ return stale;
657
+ }
658
+ const source = `${key}:${getSource(...args)}`;
659
+ const pendingItem = pending.get(source);
660
+ // If we already are loading from this source, await and return
661
+ if (pendingItem) {
662
+ return pendingItem;
663
+ }
664
+ const attempt = attempts.get(source) || 0;
665
+ // Use exponential backoff to throttle repeat attempts against the same source
666
+ if (gt(sourceFetched.get(source), now() - Math.pow(2, attempt))) {
667
+ return stale;
668
+ }
669
+ attempts.set(source, attempt + 1);
670
+ sourceFetched.set(source, now());
671
+ setFetched(key, now());
672
+ const promise = loadItem(key, ...args).then(() => getItem(key));
673
+ pending.set(source, promise);
674
+ let item;
675
+ try {
676
+ item = await promise;
677
+ }
678
+ finally {
679
+ pending.delete(source);
680
+ }
681
+ if (item) {
682
+ attempts.pop(source);
683
+ }
684
+ return item;
685
+ };
686
+ };
687
+ // Miscellaneous other stuff
688
+ export const deriveIsDeleted = (repository, event) => projection(readable(false, set => {
689
+ const idOrAddress = getIdOrAddress(event);
690
+ set(repository.isDeleted(event));
691
+ const unsubscribers = [
692
+ on(repository, "update", ({ removed, added }) => {
693
+ if (removed.has(event.id)) {
694
+ set(true);
695
+ }
696
+ for (const event of added) {
697
+ if (getIdOrAddress(event) === idOrAddress) {
698
+ set(false);
699
+ }
700
+ }
701
+ }),
702
+ on(repository, "clear", () => set(repository.isDeleted(event))),
703
+ ];
704
+ return () => unsubscribers.forEach(call);
705
+ }), () => repository.isDeleted(event));
706
+ //# sourceMappingURL=derive.js.map