libp2r2p 0.5.0 → 0.7.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.
package/README.md CHANGED
@@ -20,10 +20,12 @@ import { createPrivateMessenger } from 'libp2r2p/private-messenger'
20
20
  const messenger = await createPrivateMessenger({
21
21
  userSigner,
22
22
  contentKeySigner,
23
+ offlineRecoverySeconds: 7 * 24 * 60 * 60,
23
24
  channels: [{
24
25
  signer: privateChannelSigner,
25
26
  relays: ['wss://relay.example'],
26
- mode: 'leecher'
27
+ mode: 'leecher',
28
+ offlineRecoverySeconds: 30 * 24 * 60 * 60
27
29
  }],
28
30
  onError: err => console.warn('private messenger failed', err)
29
31
  })
@@ -126,27 +128,71 @@ one `['k', '3560']` tag, explicit matching `e` targets, and no `a` tags. A
126
128
  regular NIP-09 kind `5` request signed by the outer event's private-channel key
127
129
  must not delete a kind `3560` event, whether or not that event has an `s` tag.
128
130
 
129
- ### Temporary Send Storage
131
+ ### Storage Maintenance
130
132
 
131
133
  While an outgoing private message is being assembled, the messenger keeps
132
134
  encrypted envelope rows and router chunks in `sessionStorage`. They are
133
135
  removed when the send finishes, but an interrupted browser operation can leave
134
136
  them behind until cleanup runs.
135
137
 
136
- `PrivateMessenger.init()` performs cleanup automatically. Call
137
- `PrivateMessenger.cleanupTemporaryStorage()` once during app startup when
138
- messenger initialization may be delayed, such as while an account is locked:
138
+ `PrivateMessenger.init()` awaits storage maintenance automatically. Call
139
+ `PrivateMessenger.maintainStorage()` during app startup when messenger
140
+ initialization may be delayed, such as while an account is locked:
139
141
 
140
142
  ```js
141
143
  import { PrivateMessenger } from 'libp2r2p/private-messenger'
142
144
 
143
- PrivateMessenger.cleanupTemporaryStorage()
145
+ PrivateMessenger.maintainStorage().catch(console.warn)
144
146
  ```
145
147
 
146
- Call it before any private-message send using that storage area starts. It
147
- does not clear persisted messages, recovery material, or channel state. Pass
148
- `temporaryStorageArea: localStorage` when constructing a messenger to opt into
149
- a different Storage area.
148
+ Maintenance removes interrupted-send staging, expired receive chunks, and
149
+ storage belonging to inactive principal identities. It also
150
+ resumes any interrupted database-set deletion. An application does not need to
151
+ know database names or enumerate IndexedDB. Pass `temporaryStorageArea` only
152
+ when the messenger was configured to use a Storage area other than the default
153
+ `sessionStorage`.
154
+
155
+ Each principal signer owns internal message, recovery-seed, and channel-state
156
+ databases. The messenger updates their activity lease while it is open and
157
+ closes all handles in `await messenger.close()`. The complete set is removed
158
+ after 60 days without use, including messages that were never consumed.
159
+ Maintenance runs on every initialization and every six hours while a messenger
160
+ is active; failed deletions remain journaled and retry automatically.
161
+
162
+ Channel recovery state inside an otherwise active identity retains its separate
163
+ 45-day stale-channel cleanup policy. Offline recovery defaults to seven days.
164
+ Set `offlineRecoverySeconds` on an individual channel to override the
165
+ messenger default for its recovery seeds, offline ranges, new outer-event
166
+ expiration, and new incomplete receive groups. Updating a channel applies a
167
+ shorter window immediately to its stored seeds and ranges; increasing it does
168
+ not recreate data already removed.
169
+
170
+ Set a channel's `offlineRecoverySeconds` to `0` to disable durable recovery for
171
+ that channel. The messenger then stores no recovery seeds, tracks no offline
172
+ ranges, contacts no seeders, publishes no seeder presence, and uses no recovery
173
+ mirror relays. Live delivery remains usable: new outer events retain the
174
+ private-channel two-day technical expiration and incomplete receive groups use
175
+ the one-hour technical TTL. Existing signed events and receive groups retain
176
+ the deadlines chosen when they were created.
177
+
178
+ Recovery metadata is separate from that temporary send staging. Per-channel
179
+ `lastSeenAt`, offline ranges, and related state are stored in IndexedDB. Raw
180
+ incomplete receive chunks are also stored in IndexedDB and share a 16 MiB
181
+ logical budget. Direct private-channel calls give each new group a one-hour
182
+ TTL; PrivateMessenger groups use the effective recovery window of their
183
+ channel, or one hour when durable recovery is disabled. The TTL is persisted
184
+ per group, so another caller opening the shared database or a later channel
185
+ configuration update cannot change it. Capacity
186
+ eviction removes whole
187
+ least-recently-used message groups so a partial group is never mistaken for a
188
+ complete one. `receivedChunkTtlMs`, `receivedChunkMaxBytes`, and
189
+ `receivedChunkIndexedDB` may be supplied to the private-channel APIs when an
190
+ embedding environment needs different limits or an injected IDB factory.
191
+ Legacy Web Storage recovery records are neither read nor migrated.
192
+
193
+ The recovery-seed queue has a shared 64 MiB logical budget by default and uses
194
+ FIFO eviction. A channel recovery duration is therefore a maximum retention
195
+ window, not a guarantee that every seed remains available until its deadline.
150
196
 
151
197
  Signers are expected to expose the Nostr-style methods used by the messenger,
152
198
  including `getPublicKey()`, `signEvent(event)`, and the NIP-44 v3 methods
@@ -154,7 +200,8 @@ needed by private channels. For double-DH content-key use, pass a
154
200
  `contentKeySigner` or a signer implementation that handles content keys
155
201
  internally.
156
202
 
157
- Messages are stored in a bounded, durable IndexedDB queue until consumed:
203
+ Messages are stored in a bounded, durable IndexedDB queue until consumed or
204
+ until the principal identity has been inactive for 60 days:
158
205
 
159
206
  ```js
160
207
  async function handleMessages () {
@@ -7,6 +7,29 @@ const STATE_KEY = 'queue'
7
7
  const DEFAULT_EVICTION_HEADROOM_RATIO = 0.1
8
8
  const MAX_EVICTION_HEADROOM_BYTES = 64 * 1024 // 64 KiB
9
9
 
10
+ /*
11
+ IndexedDB schema, scoped per queue prefix:
12
+
13
+ database `${prefix}:idb-queue`, version managed additively
14
+
15
+ items, keyPath "position"
16
+ position integer ordering key between the queue head and tail
17
+ byteSize logical serialized size used by the queue capacity limit
18
+ item caller-provided queued value
19
+
20
+ items indexes
21
+ Declared by each createQueue() caller. A requested keyPath such as "status"
22
+ is stored as "item.status"; compound paths receive the same "item." prefix.
23
+ The database version is incremented when declared indexes need to be added.
24
+
25
+ state, keyPath "key"
26
+ key state record name, currently "queue"
27
+ head/tail half-open range containing allocated item positions
28
+ usedBytes sum of the queued items' logical byte sizes
29
+
30
+ The queue state record is absent while the queue is empty.
31
+ */
32
+
10
33
  function deferred () {
11
34
  let resolve
12
35
  let reject
@@ -251,6 +274,10 @@ export async function createQueue ({
251
274
  const waiters = new Set()
252
275
  let sessionMaxBytes = configuredMaxBytes
253
276
  let revision = 0
277
+ let closed = false
278
+ let activeTransactions = 0
279
+ let closePromise = null
280
+ const closeWaiters = new Set()
254
281
 
255
282
  function hasByteLimit () {
256
283
  return Number.isFinite(sessionMaxBytes)
@@ -294,17 +321,27 @@ export async function createQueue ({
294
321
  }
295
322
 
296
323
  async function transaction (mode, work) {
297
- const tx = db.transaction([ITEMS_STORE, STATE_STORE], mode)
298
- const done = transactionDone(tx)
324
+ if (closed) throw new Error('QUEUE_CLOSED')
325
+ activeTransactions++
299
326
  try {
300
- // Keep work limited to IndexedDB requests so the transaction stays active.
301
- const value = await work(tx)
302
- await done
303
- return value
304
- } catch (err) {
305
- try { tx.abort() } catch {}
306
- try { await done } catch {}
307
- throw err
327
+ const tx = db.transaction([ITEMS_STORE, STATE_STORE], mode)
328
+ const done = transactionDone(tx)
329
+ try {
330
+ // Keep work limited to IndexedDB requests so the transaction stays active.
331
+ const value = await work(tx)
332
+ await done
333
+ return value
334
+ } catch (err) {
335
+ try { tx.abort() } catch {}
336
+ try { await done } catch {}
337
+ throw err
338
+ }
339
+ } finally {
340
+ activeTransactions--
341
+ if (!activeTransactions) {
342
+ for (const resolve of closeWaiters) resolve()
343
+ closeWaiters.clear()
344
+ }
308
345
  }
309
346
  }
310
347
 
@@ -748,6 +785,17 @@ export async function createQueue ({
748
785
  for (const record of (await snapshotStoredItems()).reverse()) yield record.item
749
786
  }
750
787
 
788
+ function close () {
789
+ if (closePromise) return closePromise
790
+ closed = true
791
+ wake()
792
+ closePromise = (activeTransactions
793
+ ? new Promise(resolve => closeWaiters.add(resolve))
794
+ : Promise.resolve())
795
+ .then(() => { db.close() })
796
+ return closePromise
797
+ }
798
+
751
799
  async function * storedItemsBy (indexName, query, { direction = 'next' } = {}) {
752
800
  direction = validDirection(direction)
753
801
  const records = await snapshot(async tx => {
@@ -788,6 +836,7 @@ export async function createQueue ({
788
836
  getBy,
789
837
  someBy,
790
838
  removeBy,
791
- storedItemsBy
839
+ storedItemsBy,
840
+ close
792
841
  }
793
842
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libp2r2p",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Peer-to-relay-to-peer",
5
5
  "keywords": [
6
6
  "p2r2p",
@@ -566,8 +566,8 @@ async function decryptRouter ({ content, channelPubkey, channelSigner, channelRe
566
566
 
567
567
  function readValueFromMap (map, key) {
568
568
  if (!map || !key) return null
569
- if (map instanceof Map) return map.get(key) || null
570
- return map[key] || null
569
+ if (map instanceof Map) return map.get(key) ?? null
570
+ return map[key] ?? null
571
571
  }
572
572
 
573
573
  function privateChannelPubkeyList ({ privateChannelPubkey, privateChannelPubkeys }) {
@@ -669,22 +669,26 @@ function createProcessor ({
669
669
  onContentKeyUsage,
670
670
  onError,
671
671
  receivedChunkTtlMs = DEFAULT_RECEIVED_CHUNK_TTL_MS,
672
+ receivedChunkTtlMsByPubkey,
672
673
  receivedChunkMaxBytes = DEFAULT_RECEIVED_CHUNK_MAX_BYTES,
673
- receivedChunkStorageArea,
674
+ receivedChunkIndexedDB = globalThis.indexedDB,
674
675
  ignoredGroupTtlMs = DEFAULT_IGNORED_GROUP_TTL_MS,
675
676
  ignoredGroupMaxEntries = DEFAULT_IGNORED_GROUP_MAX_ENTRIES
676
677
  }) {
677
678
  const receivedChunks = createReceivedChunkStore({
678
679
  ttlMs: receivedChunkTtlMs,
679
680
  maxBytes: receivedChunkMaxBytes,
680
- storageArea: receivedChunkStorageArea
681
+ indexedDB: receivedChunkIndexedDB
681
682
  })
682
683
  const ignoredGroups = createTtlSet({
683
684
  ttlMs: ignoredGroupTtlMs,
684
685
  maxEntries: ignoredGroupMaxEntries
685
686
  })
687
+ const storeReady = receivedChunks.ready()
688
+ storeReady.catch(() => {})
686
689
 
687
- return async function processOuterEvent (outer) {
690
+ async function processOuterEvent (outer) {
691
+ await storeReady
688
692
  let groupKey = ''
689
693
  try {
690
694
  const channelPubkey = outer.pubkey || await privateChannelSigner?.getPublicKey?.()
@@ -692,6 +696,7 @@ function createProcessor ({
692
696
  const channelReaderSigner = readSignerFromMap(privateChannelReaderSignersByPubkey, channelPubkey) || privateChannelReaderSigner || channelSigner
693
697
  const channelReaderPubkey = readValueFromMap(privateChannelReaderPubkeysByPubkey, channelPubkey) || privateChannelReaderPubkey || channelPubkey
694
698
  const channelMode = readValueFromMap(modeByPubkey, channelPubkey) || mode
699
+ const channelReceivedChunkTtlMs = readValueFromMap(receivedChunkTtlMsByPubkey, channelPubkey) ?? receivedChunkTtlMs
695
700
  if (!channelPubkey) throw new Error('PRIVATE_CHANNEL_PUBKEY_REQUIRED')
696
701
 
697
702
  const decrypted = await decryptRouter({
@@ -707,14 +712,15 @@ function createProcessor ({
707
712
  groupKey = receivedChunks.groupKeyFor(channelPubkey, nymCarrierGroupId(carrier))
708
713
  if (ignoredGroups.has(groupKey)) return
709
714
 
710
- const meta = receivedChunks.put({
715
+ const meta = await receivedChunks.put({
711
716
  channelPubkey,
712
717
  routerPubkey: nymCarrierGroupId(carrier),
713
718
  index,
714
719
  total,
715
- content: JSON.stringify(carrier)
720
+ contentBytes: encoder.encode(JSON.stringify(carrier)),
721
+ ttlMs: channelReceivedChunkTtlMs
716
722
  })
717
- const status = receivedChunks.status(meta)
723
+ const status = await receivedChunks.status(meta)
718
724
  onChunk?.({
719
725
  outer,
720
726
  nymCarrier: carrier,
@@ -726,7 +732,7 @@ function createProcessor ({
726
732
  })
727
733
  if (status.received < total) return
728
734
 
729
- const carriers = receivedChunks.readChunkContents(groupKey).map(raw => JSON.parse(raw))
735
+ const carriers = (await receivedChunks.readChunkContents(groupKey)).map(raw => JSON.parse(raw))
730
736
  const event = eventFromNymCarriers(carriers)
731
737
  const shouldSeed = storesRecoverySeeds(channelMode)
732
738
  if (shouldSeed) {
@@ -744,7 +750,7 @@ function createProcessor ({
744
750
  carriers,
745
751
  channelPubkey
746
752
  })
747
- receivedChunks.removeGroup(groupKey)
753
+ await receivedChunks.removeGroup(groupKey)
748
754
  return
749
755
  }
750
756
  const router = decrypted
@@ -757,14 +763,15 @@ function createProcessor ({
757
763
 
758
764
  const imkcPubkey = readImkcTag(router)
759
765
  assertValidRouterImkcProof({ router, senderPubkey, imkcPubkey, imkcProof: readImkcProof(router) })
760
- const meta = receivedChunks.put({
766
+ const meta = await receivedChunks.put({
761
767
  channelPubkey,
762
768
  routerPubkey: router.pubkey,
763
769
  index,
764
770
  total,
765
- content: router.content
771
+ contentBytes: base64ToBytes(router.content),
772
+ ttlMs: channelReceivedChunkTtlMs
766
773
  })
767
- const status = receivedChunks.status(meta)
774
+ const status = await receivedChunks.status(meta)
768
775
  onChunk?.({
769
776
  outer,
770
777
  router,
@@ -826,15 +833,15 @@ function createProcessor ({
826
833
  if (event && !mustScanWholeBundle) {
827
834
  await onEvent?.(event, outer, { router: joinedRouter(router), channelPubkey })
828
835
  ignoredGroups.add(groupKey)
829
- receivedChunks.removeGroup(groupKey)
836
+ await receivedChunks.removeGroup(groupKey)
830
837
  return
831
838
  }
832
839
 
833
840
  if (!drained.complete) return
834
841
 
835
842
  const receiverPubkeys = drained.meta?.receiverPubkeys || []
836
- const content = shouldSeed ? receivedChunks.readEnvelopeBundleContent(groupKey) : ''
837
- const jsonl = shouldSeed ? receivedChunks.readEnvelopeBundleText(groupKey) : ''
843
+ const content = shouldSeed ? await receivedChunks.readEnvelopeBundleContent(groupKey) : ''
844
+ const jsonl = shouldSeed ? await receivedChunks.readEnvelopeBundleText(groupKey) : ''
838
845
  const completeRouter = joinedRouter(router, content)
839
846
 
840
847
  emitSentContentKeyUsage({
@@ -848,15 +855,18 @@ function createProcessor ({
848
855
  if (shouldSeed) await onSeedEvent?.({ recordType: 'routerRow_v1', outer, router: completeRouter, channelPubkey, jsonl, innerEventIdsByRowIndex })
849
856
  if (event) await onEvent?.(event, outer, { router: completeRouter, channelPubkey, jsonl })
850
857
 
851
- receivedChunks.removeGroup(groupKey)
858
+ await receivedChunks.removeGroup(groupKey)
852
859
  } catch (err) {
853
860
  if (shouldIgnoreGroupError(err) && groupKey) {
854
861
  ignoredGroups.add(groupKey)
855
- receivedChunks.removeGroup(groupKey)
862
+ await receivedChunks.removeGroup(groupKey).catch(() => {})
856
863
  }
857
864
  onError?.(err)
858
865
  }
859
866
  }
867
+
868
+ processOuterEvent.close = () => receivedChunks.close()
869
+ return processOuterEvent
860
870
  }
861
871
 
862
872
  function shouldIgnoreGroupError (err) {
@@ -875,13 +885,14 @@ function shouldIgnoreGroupError (err) {
875
885
  'INVALID_RECIPIENT_ENVELOPE',
876
886
  'INVALID_SIGNED_INNER_EVENT',
877
887
  'MISSING_PAYLOAD_ENVELOPE',
888
+ 'RECEIVED_CHUNK_GROUP_TOO_LARGE',
878
889
  'MISMATCHED_NYM_CARRIER_CHUNKS',
879
890
  'MISSING_NYM_CARRIER_CHUNK',
880
891
  'MISSING_NYM_CARRIER_ID'
881
892
  ].includes(err?.message)
882
893
  }
883
894
 
884
- export async function fetch ({ receiverSigner, iykcSigner, privateChannelSigner = receiverSigner, privateChannelSignersByPubkey, privateChannelReaderSigner = privateChannelSigner, privateChannelReaderSignersByPubkey, privateChannelReaderPubkey, privateChannelReaderPubkeysByPubkey, privateChannelPubkey, privateChannelPubkeys, receiverPubkey, relays, onChunk, onEvent, onNymEvent, onSeedEvent, onContentKeyUsage, onError, since, until, limit, mode = 'leecher', modeByPubkey, receivedChunkTtlMs = DEFAULT_RECEIVED_CHUNK_TTL_MS, receivedChunkMaxBytes = DEFAULT_RECEIVED_CHUNK_MAX_BYTES, receivedChunkStorageArea, ignoredGroupTtlMs = DEFAULT_IGNORED_GROUP_TTL_MS, ignoredGroupMaxEntries = DEFAULT_IGNORED_GROUP_MAX_ENTRIES, _getEvents = getEvents }) {
895
+ export async function fetch ({ receiverSigner, iykcSigner, privateChannelSigner = receiverSigner, privateChannelSignersByPubkey, privateChannelReaderSigner = privateChannelSigner, privateChannelReaderSignersByPubkey, privateChannelReaderPubkey, privateChannelReaderPubkeysByPubkey, privateChannelPubkey, privateChannelPubkeys, receiverPubkey, relays, onChunk, onEvent, onNymEvent, onSeedEvent, onContentKeyUsage, onError, since, until, limit, mode = 'leecher', modeByPubkey, receivedChunkTtlMs = DEFAULT_RECEIVED_CHUNK_TTL_MS, receivedChunkTtlMsByPubkey, receivedChunkMaxBytes = DEFAULT_RECEIVED_CHUNK_MAX_BYTES, receivedChunkIndexedDB = globalThis.indexedDB, ignoredGroupTtlMs = DEFAULT_IGNORED_GROUP_TTL_MS, ignoredGroupMaxEntries = DEFAULT_IGNORED_GROUP_MAX_ENTRIES, _getEvents = getEvents }) {
885
896
  if (!relays?.length) throw new Error('NO_RELAYS')
886
897
  const authors = privateChannelPubkeyList({ privateChannelPubkey, privateChannelPubkeys })
887
898
  const filter = { kinds: [PRIVATE_BROADCAST_KIND] }
@@ -895,12 +906,16 @@ export async function fetch ({ receiverSigner, iykcSigner, privateChannelSigner
895
906
  timeoutAfterFirstEose: null
896
907
  })
897
908
  events.sort((a, b) => a.created_at - b.created_at)
898
- const processOuterEvent = createProcessor({ receiverSigner, iykcSigner, privateChannelSigner, privateChannelSignersByPubkey, privateChannelReaderSigner, privateChannelReaderSignersByPubkey, privateChannelReaderPubkey, privateChannelReaderPubkeysByPubkey, receiverPubkey, mode, modeByPubkey, onChunk, onEvent, onNymEvent, onSeedEvent, onContentKeyUsage, onError, receivedChunkTtlMs, receivedChunkMaxBytes, receivedChunkStorageArea, ignoredGroupTtlMs, ignoredGroupMaxEntries })
899
- for (const event of events) await processOuterEvent(event)
900
- return events
909
+ const processOuterEvent = createProcessor({ receiverSigner, iykcSigner, privateChannelSigner, privateChannelSignersByPubkey, privateChannelReaderSigner, privateChannelReaderSignersByPubkey, privateChannelReaderPubkey, privateChannelReaderPubkeysByPubkey, receiverPubkey, mode, modeByPubkey, onChunk, onEvent, onNymEvent, onSeedEvent, onContentKeyUsage, onError, receivedChunkTtlMs, receivedChunkTtlMsByPubkey, receivedChunkMaxBytes, receivedChunkIndexedDB, ignoredGroupTtlMs, ignoredGroupMaxEntries })
910
+ try {
911
+ for (const event of events) await processOuterEvent(event)
912
+ return events
913
+ } finally {
914
+ processOuterEvent.close()
915
+ }
901
916
  }
902
917
 
903
- export function subscribe ({ receiverSigner, iykcSigner, privateChannelSigner = receiverSigner, privateChannelSignersByPubkey, privateChannelReaderSigner = privateChannelSigner, privateChannelReaderSignersByPubkey, privateChannelReaderPubkey, privateChannelReaderPubkeysByPubkey, privateChannelPubkey, privateChannelPubkeys, receiverPubkey, relays, onChunk, onEvent, onNymEvent, onSeedEvent, onContentKeyUsage, onError, since = nowSeconds() - 5, limit, liveOnly = false, mode = 'leecher', modeByPubkey, receivedChunkTtlMs = DEFAULT_RECEIVED_CHUNK_TTL_MS, receivedChunkMaxBytes = DEFAULT_RECEIVED_CHUNK_MAX_BYTES, receivedChunkStorageArea, ignoredGroupTtlMs = DEFAULT_IGNORED_GROUP_TTL_MS, ignoredGroupMaxEntries = DEFAULT_IGNORED_GROUP_MAX_ENTRIES, _liveEventsGenerator = getLiveEventsGenerator, _eventsFeedGenerator = getEventsFeedGenerator }) {
918
+ export function subscribe ({ receiverSigner, iykcSigner, privateChannelSigner = receiverSigner, privateChannelSignersByPubkey, privateChannelReaderSigner = privateChannelSigner, privateChannelReaderSignersByPubkey, privateChannelReaderPubkey, privateChannelReaderPubkeysByPubkey, privateChannelPubkey, privateChannelPubkeys, receiverPubkey, relays, onChunk, onEvent, onNymEvent, onSeedEvent, onContentKeyUsage, onError, since = nowSeconds() - 5, limit, liveOnly = false, mode = 'leecher', modeByPubkey, receivedChunkTtlMs = DEFAULT_RECEIVED_CHUNK_TTL_MS, receivedChunkTtlMsByPubkey, receivedChunkMaxBytes = DEFAULT_RECEIVED_CHUNK_MAX_BYTES, receivedChunkIndexedDB = globalThis.indexedDB, ignoredGroupTtlMs = DEFAULT_IGNORED_GROUP_TTL_MS, ignoredGroupMaxEntries = DEFAULT_IGNORED_GROUP_MAX_ENTRIES, _liveEventsGenerator = getLiveEventsGenerator, _eventsFeedGenerator = getEventsFeedGenerator }) {
904
919
  if (!relays?.length) throw new Error('NO_RELAYS')
905
920
  if (receiverSigner && !receiverSigner?.nip44DecryptDoubleDH && !receiverSigner?.nip44v3Decrypt) throw new Error('RECEIVER_SIGNER_NIP44V3_DECRYPT_UNSUPPORTED')
906
921
  if (!privateChannelReaderSigner && !privateChannelReaderSignersByPubkey && !privateChannelSigner && !privateChannelSignersByPubkey) throw new Error('PRIVATE_CHANNEL_READER_REQUIRED')
@@ -909,17 +924,17 @@ export function subscribe ({ receiverSigner, iykcSigner, privateChannelSigner =
909
924
  const filter = { kinds: [PRIVATE_BROADCAST_KIND], since }
910
925
  if (authors.length) filter.authors = authors
911
926
  if (limit != null) filter.limit = limit
912
- const processOuterEvent = createProcessor({ receiverSigner, iykcSigner, privateChannelSigner, privateChannelSignersByPubkey, privateChannelReaderSigner, privateChannelReaderSignersByPubkey, privateChannelReaderPubkey, privateChannelReaderPubkeysByPubkey, receiverPubkey, mode, modeByPubkey, onChunk, onEvent, onNymEvent, onSeedEvent, onContentKeyUsage, onError, receivedChunkTtlMs, receivedChunkMaxBytes, receivedChunkStorageArea, ignoredGroupTtlMs, ignoredGroupMaxEntries })
927
+ const processOuterEvent = createProcessor({ receiverSigner, iykcSigner, privateChannelSigner, privateChannelSignersByPubkey, privateChannelReaderSigner, privateChannelReaderSignersByPubkey, privateChannelReaderPubkey, privateChannelReaderPubkeysByPubkey, receiverPubkey, mode, modeByPubkey, onChunk, onEvent, onNymEvent, onSeedEvent, onContentKeyUsage, onError, receivedChunkTtlMs, receivedChunkTtlMsByPubkey, receivedChunkMaxBytes, receivedChunkIndexedDB, ignoredGroupTtlMs, ignoredGroupMaxEntries })
913
928
  const controller = new AbortController()
914
929
  const events = liveOnly
915
930
  ? _liveEventsGenerator(filter, relays, {
916
- signal: controller.signal
917
- })
931
+ signal: controller.signal
932
+ })
918
933
  : _eventsFeedGenerator(filter, relays, {
919
- signal: controller.signal,
920
- timeout: 5000,
921
- timeoutAfterFirstEose: null
922
- })
934
+ signal: controller.signal,
935
+ timeout: 5000,
936
+ timeoutAfterFirstEose: null
937
+ })
923
938
 
924
939
  async function consumeEvents () {
925
940
  try {
@@ -929,14 +944,17 @@ export function subscribe ({ receiverSigner, iykcSigner, privateChannelSigner =
929
944
  }
930
945
  } catch (error) {
931
946
  if (!controller.signal.aborted && error?.message !== 'Aborted') onError?.(error)
947
+ } finally {
948
+ processOuterEvent.close()
932
949
  }
933
950
  }
934
951
 
935
- consumeEvents()
952
+ const consumePromise = consumeEvents()
936
953
 
937
954
  return {
938
955
  close () {
939
956
  controller.abort()
957
+ return consumePromise
940
958
  }
941
959
  }
942
960
  }