libp2r2p 0.6.0 → 0.8.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.
@@ -5,7 +5,7 @@ export const DEFAULT_RECEIVED_CHUNK_TTL_MS = 60 * 60 * 1000 // 1 hour
5
5
  export const DEFAULT_RECEIVED_CHUNK_MAX_BYTES = 16 * 1024 * 1024 // 16 MiB
6
6
 
7
7
  const DEFAULT_PREFIX = 'libp2r2p:private-channel:received'
8
- const DATABASE_VERSION = 1
8
+ const DATABASE_VERSION = 2
9
9
  const GROUPS_STORE = 'groups'
10
10
  const CHUNKS_STORE = 'chunks'
11
11
  const STATE_STORE = 'state'
@@ -16,7 +16,7 @@ const decoder = new TextDecoder()
16
16
  /*
17
17
  IndexedDB schema, scoped per received-chunk prefix:
18
18
 
19
- database `${prefix}:idb`, version 1
19
+ database `${prefix}:idb`, version 2
20
20
 
21
21
  groups, keyPath "groupKey"
22
22
  groupKey `${channelPubkey}:${routerPubkey}`
@@ -28,10 +28,12 @@ groups, keyPath "groupKey"
28
28
  receiverPubkeys deduplicated intended receivers
29
29
  byteSize logical bytes charged to this group
30
30
  createdAt/updatedAt lifecycle timestamps in milliseconds
31
+ ttlMs/expiresAt group-specific retention and absolute expiry
31
32
  drainToken/drainUntil optional drain lease owner and expiry
32
33
 
33
34
  groups indexes
34
35
  byUpdatedAt updatedAt
36
+ byExpiresAt expiresAt
35
37
 
36
38
  chunks, keyPath ["groupKey", "index"]
37
39
  groupKey owning group coordinate
@@ -80,6 +82,7 @@ function openDatabase (indexedDB, name) {
80
82
  groups = tx.objectStore(GROUPS_STORE)
81
83
  }
82
84
  if (!groups.indexNames.contains('byUpdatedAt')) groups.createIndex('byUpdatedAt', 'updatedAt')
85
+ if (!groups.indexNames.contains('byExpiresAt')) groups.createIndex('byExpiresAt', 'expiresAt')
83
86
 
84
87
  let chunks
85
88
  if (!db.objectStoreNames.contains(CHUNKS_STORE)) {
@@ -119,12 +122,14 @@ function normalizeReceived (received) {
119
122
  )
120
123
  }
121
124
 
122
- function normalizeMeta (meta) {
125
+ function normalizeMeta (meta, fallbackTtlMs = DEFAULT_RECEIVED_CHUNK_TTL_MS) {
123
126
  if (!meta || typeof meta !== 'object') return null
124
127
  const total = Number(meta.total)
125
128
  const nextIndex = Number(meta.nextIndex)
126
129
  const rowIndex = Number(meta.rowIndex)
127
130
  if (!meta.groupKey || !Number.isSafeInteger(total) || total < 1) return null
131
+ const updatedAt = Number(meta.updatedAt) || Date.now()
132
+ const ttlMs = Number.isFinite(meta.ttlMs) && meta.ttlMs >= 0 ? meta.ttlMs : fallbackTtlMs
128
133
  return {
129
134
  groupKey: String(meta.groupKey),
130
135
  channelPubkey: String(meta.channelPubkey || ''),
@@ -139,7 +144,9 @@ function normalizeMeta (meta) {
139
144
  receiverPubkeys: uniq(meta.receiverPubkeys),
140
145
  byteSize: Math.max(0, Number(meta.byteSize) || 0),
141
146
  createdAt: Number(meta.createdAt) || Date.now(),
142
- updatedAt: Number(meta.updatedAt) || Date.now(),
147
+ updatedAt,
148
+ ttlMs,
149
+ expiresAt: Number(meta.expiresAt) || updatedAt + ttlMs,
143
150
  drainToken: typeof meta.drainToken === 'string' ? meta.drainToken : '',
144
151
  drainUntil: Math.max(0, Number(meta.drainUntil) || 0)
145
152
  }
@@ -180,10 +187,18 @@ export function createReceivedChunkStore ({
180
187
  const configuredMaxBytes = Number.isFinite(maxBytes) && maxBytes > 0 ? maxBytes : Infinity
181
188
  let dbPromise
182
189
  let readyPromise
190
+ let closed = false
183
191
 
184
192
  function database () {
193
+ if (closed) return Promise.reject(new Error('RECEIVED_CHUNK_STORE_CLOSED'))
185
194
  dbPromise ||= openDatabase(indexedDB, `${prefix}:idb`)
186
- return dbPromise
195
+ return dbPromise.then(db => {
196
+ if (closed) {
197
+ db.close()
198
+ throw new Error('RECEIVED_CHUNK_STORE_CLOSED')
199
+ }
200
+ return db
201
+ })
187
202
  }
188
203
 
189
204
  async function transaction (storeNames, mode, work) {
@@ -215,7 +230,7 @@ export function createReceivedChunkStore ({
215
230
  }
216
231
 
217
232
  async function deleteGroupInTransaction (tx, groupKey, usage, knownMeta) {
218
- const meta = knownMeta || normalizeMeta((await run('get', [groupKey], GROUPS_STORE, null, { tx })).result)
233
+ const meta = knownMeta || normalizeMeta((await run('get', [groupKey], GROUPS_STORE, null, { tx })).result, configuredTtlMs)
219
234
  const keys = (await run('getAllKeys', [globalThis.IDBKeyRange?.only?.(groupKey) ?? groupKey], CHUNKS_STORE, 'byGroup', { tx })).result
220
235
  for (const key of keys) await run('delete', [key], CHUNKS_STORE, null, { tx })
221
236
  await run('delete', [groupKey], GROUPS_STORE, null, { tx })
@@ -226,19 +241,18 @@ export function createReceivedChunkStore ({
226
241
  async function oldestGroupsInTransaction (tx, except = '') {
227
242
  const values = (await run('getAll', [], GROUPS_STORE, null, { tx })).result
228
243
  return values
229
- .map(normalizeMeta)
244
+ .map(value => normalizeMeta(value, configuredTtlMs))
230
245
  .filter(meta => meta && meta.groupKey !== except)
231
246
  .sort((left, right) => left.updatedAt - right.updatedAt || left.groupKey.localeCompare(right.groupKey))
232
247
  }
233
248
 
234
249
  async function cleanupStaleRaw (nowMs = Date.now()) {
235
- const cutoff = nowMs - configuredTtlMs
236
250
  return transaction([GROUPS_STORE, CHUNKS_STORE, STATE_STORE], 'readwrite', async tx => {
237
251
  const usage = await readUsage(tx)
238
252
  const groups = await oldestGroupsInTransaction(tx)
239
253
  let removed = 0
240
254
  for (const meta of groups) {
241
- if (meta.updatedAt > cutoff && (!Number.isFinite(configuredMaxBytes) || usage.usedBytes <= configuredMaxBytes)) continue
255
+ if (meta.expiresAt > nowMs && (!Number.isFinite(configuredMaxBytes) || usage.usedBytes <= configuredMaxBytes)) continue
242
256
  await deleteGroupInTransaction(tx, meta.groupKey, usage, meta)
243
257
  removed++
244
258
  }
@@ -247,26 +261,26 @@ export function createReceivedChunkStore ({
247
261
  })
248
262
  }
249
263
 
250
- function ready () {
251
- readyPromise ||= cleanupStaleRaw()
264
+ function ready (nowMs = Date.now()) {
265
+ readyPromise ||= cleanupStaleRaw(nowMs)
252
266
  return readyPromise
253
267
  }
254
268
 
255
269
  async function cleanupStale (nowMs = Date.now()) {
256
- await ready()
270
+ await ready(nowMs)
257
271
  return cleanupStaleRaw(nowMs)
258
272
  }
259
273
 
260
- async function putOnce ({ channelPubkey, routerPubkey, index, total, contentBytes }) {
274
+ async function putOnce ({ channelPubkey, routerPubkey, index, total, contentBytes, ttlMs }) {
261
275
  const groupKey = groupKeyFor(channelPubkey, routerPubkey)
262
276
  const bytes = normalizeBytes(contentBytes)
263
277
  const now = Date.now()
278
+ const groupTtlMs = Number.isFinite(ttlMs) && ttlMs >= 0 ? ttlMs : configuredTtlMs
264
279
 
265
280
  return transaction([GROUPS_STORE, CHUNKS_STORE, STATE_STORE], 'readwrite', async tx => {
266
281
  const usage = await readUsage(tx)
267
- let meta = normalizeMeta((await run('get', [groupKey], GROUPS_STORE, null, { tx })).result)
268
- const staleCutoff = now - configuredTtlMs
269
- if (meta && meta.updatedAt <= staleCutoff) {
282
+ let meta = normalizeMeta((await run('get', [groupKey], GROUPS_STORE, null, { tx })).result, configuredTtlMs)
283
+ if (meta && meta.expiresAt <= now) {
270
284
  await deleteGroupInTransaction(tx, groupKey, usage, meta)
271
285
  meta = null
272
286
  }
@@ -289,8 +303,10 @@ export function createReceivedChunkStore ({
289
303
  receiverPubkeys: [],
290
304
  byteSize: 0,
291
305
  createdAt: now,
292
- updatedAt: now
293
- })
306
+ updatedAt: now,
307
+ ttlMs: groupTtlMs,
308
+ expiresAt: now + groupTtlMs
309
+ }, configuredTtlMs)
294
310
  }
295
311
 
296
312
  const existing = (await run('get', [[groupKey, index]], CHUNKS_STORE, null, { tx })).result
@@ -304,11 +320,11 @@ export function createReceivedChunkStore ({
304
320
  const requiredBytes = bytes.byteLength
305
321
  const candidates = await oldestGroupsInTransaction(tx, groupKey)
306
322
  for (const candidate of candidates) {
307
- if (candidate.updatedAt > staleCutoff) continue
323
+ if (candidate.expiresAt > now) continue
308
324
  await deleteGroupInTransaction(tx, candidate.groupKey, usage, candidate)
309
325
  }
310
326
  for (const candidate of candidates) {
311
- if (candidate.updatedAt <= staleCutoff) continue
327
+ if (candidate.expiresAt <= now) continue
312
328
  if (!Number.isFinite(configuredMaxBytes) || usage.usedBytes + requiredBytes <= configuredMaxBytes) break
313
329
  await deleteGroupInTransaction(tx, candidate.groupKey, usage, candidate)
314
330
  }
@@ -327,6 +343,7 @@ export function createReceivedChunkStore ({
327
343
 
328
344
  meta.total = total
329
345
  meta.updatedAt = now
346
+ meta.expiresAt = now + meta.ttlMs
330
347
  await run('put', [meta], GROUPS_STORE, null, { tx })
331
348
  await writeUsage(tx, usage)
332
349
  return { tooLarge: false, meta }
@@ -344,7 +361,7 @@ export function createReceivedChunkStore ({
344
361
  })
345
362
  }
346
363
 
347
- async function put ({ channelPubkey, routerPubkey, index, total, contentBytes }) {
364
+ async function put ({ channelPubkey, routerPubkey, index, total, contentBytes, ttlMs }) {
348
365
  if (!channelPubkey || !routerPubkey) throw new Error('RECEIVED_CHUNK_GROUP_REQUIRED')
349
366
  if (!Number.isSafeInteger(index) || !Number.isSafeInteger(total) || index < 0 || total < 1 || index >= total) {
350
367
  throw new Error('INVALID_RECEIVED_CHUNK_INDEX')
@@ -353,7 +370,7 @@ export function createReceivedChunkStore ({
353
370
  await ready()
354
371
  while (true) {
355
372
  try {
356
- const result = await putOnce({ channelPubkey, routerPubkey, index, total, contentBytes: bytes })
373
+ const result = await putOnce({ channelPubkey, routerPubkey, index, total, contentBytes: bytes, ttlMs })
357
374
  if (result.tooLarge) throw new Error('RECEIVED_CHUNK_GROUP_TOO_LARGE')
358
375
  return result.meta
359
376
  } catch (err) {
@@ -365,12 +382,12 @@ export function createReceivedChunkStore ({
365
382
  async function readMeta (groupKey) {
366
383
  await ready()
367
384
  return transaction([GROUPS_STORE], 'readonly', async tx => {
368
- return normalizeMeta((await run('get', [groupKey], GROUPS_STORE, null, { tx })).result)
385
+ return normalizeMeta((await run('get', [groupKey], GROUPS_STORE, null, { tx })).result, configuredTtlMs)
369
386
  })
370
387
  }
371
388
 
372
389
  async function status (metaOrGroupKey) {
373
- const meta = typeof metaOrGroupKey === 'string' ? await readMeta(metaOrGroupKey) : normalizeMeta(metaOrGroupKey)
390
+ const meta = typeof metaOrGroupKey === 'string' ? await readMeta(metaOrGroupKey) : normalizeMeta(metaOrGroupKey, configuredTtlMs)
374
391
  if (!meta) return { received: 0, missing: [] }
375
392
  const missing = []
376
393
  let received = 0
@@ -393,13 +410,15 @@ export function createReceivedChunkStore ({
393
410
  while (true) {
394
411
  const now = Date.now()
395
412
  const result = await transaction([GROUPS_STORE], 'readwrite', async tx => {
396
- const meta = normalizeMeta((await run('get', [groupKey], GROUPS_STORE, null, { tx })).result)
413
+ const meta = normalizeMeta((await run('get', [groupKey], GROUPS_STORE, null, { tx })).result, configuredTtlMs)
397
414
  if (!meta) return { meta: null, waitMs: 0 }
398
415
  if (meta.drainToken && meta.drainToken !== token && meta.drainUntil > now) {
399
416
  return { meta: null, waitMs: Math.min(DRAIN_LEASE_MS, meta.drainUntil - now) }
400
417
  }
401
418
  meta.drainToken = token
402
419
  meta.drainUntil = now + DRAIN_LEASE_MS
420
+ meta.updatedAt = now
421
+ meta.expiresAt = now + meta.ttlMs
403
422
  await run('put', [meta], GROUPS_STORE, null, { tx })
404
423
  return { meta, waitMs: 0 }
405
424
  })
@@ -410,7 +429,7 @@ export function createReceivedChunkStore ({
410
429
 
411
430
  async function readDrainChunk (groupKey, token) {
412
431
  return transaction([GROUPS_STORE, CHUNKS_STORE], 'readonly', async tx => {
413
- const meta = normalizeMeta((await run('get', [groupKey], GROUPS_STORE, null, { tx })).result)
432
+ const meta = normalizeMeta((await run('get', [groupKey], GROUPS_STORE, null, { tx })).result, configuredTtlMs)
414
433
  if (!meta || meta.drainToken !== token) return { meta: null, bytes: null }
415
434
  if (meta.nextIndex >= meta.total) return { meta, bytes: null }
416
435
  const chunk = (await run('get', [[groupKey, meta.nextIndex]], CHUNKS_STORE, null, { tx })).result
@@ -420,7 +439,7 @@ export function createReceivedChunkStore ({
420
439
 
421
440
  async function commitDrainMeta (nextMeta, token) {
422
441
  return transaction([GROUPS_STORE], 'readwrite', async tx => {
423
- const current = normalizeMeta((await run('get', [nextMeta.groupKey], GROUPS_STORE, null, { tx })).result)
442
+ const current = normalizeMeta((await run('get', [nextMeta.groupKey], GROUPS_STORE, null, { tx })).result, configuredTtlMs)
424
443
  if (!current || current.drainToken !== token) return null
425
444
  current.nextIndex = nextMeta.nextIndex
426
445
  current.rowIndex = nextMeta.rowIndex
@@ -428,6 +447,7 @@ export function createReceivedChunkStore ({
428
447
  current.payloadCiphertext = nextMeta.payloadCiphertext
429
448
  current.receiverPubkeys = uniq(nextMeta.receiverPubkeys)
430
449
  current.updatedAt = nextMeta.updatedAt
450
+ current.expiresAt = current.updatedAt + current.ttlMs
431
451
  current.drainUntil = Date.now() + DRAIN_LEASE_MS
432
452
  await run('put', [current], GROUPS_STORE, null, { tx })
433
453
  return current
@@ -436,7 +456,7 @@ export function createReceivedChunkStore ({
436
456
 
437
457
  async function releaseDrain (groupKey, token) {
438
458
  return transaction([GROUPS_STORE], 'readwrite', async tx => {
439
- const meta = normalizeMeta((await run('get', [groupKey], GROUPS_STORE, null, { tx })).result)
459
+ const meta = normalizeMeta((await run('get', [groupKey], GROUPS_STORE, null, { tx })).result, configuredTtlMs)
440
460
  if (!meta || meta.drainToken !== token) return false
441
461
  meta.drainToken = ''
442
462
  meta.drainUntil = 0
@@ -504,7 +524,7 @@ export function createReceivedChunkStore ({
504
524
  async function readChunkBytes (groupKey) {
505
525
  await ready()
506
526
  return transaction([GROUPS_STORE, CHUNKS_STORE], 'readonly', async tx => {
507
- const meta = normalizeMeta((await run('get', [groupKey], GROUPS_STORE, null, { tx })).result)
527
+ const meta = normalizeMeta((await run('get', [groupKey], GROUPS_STORE, null, { tx })).result, configuredTtlMs)
508
528
  if (!meta) return { meta: null, chunks: [] }
509
529
  const chunks = []
510
530
  for (let index = 0; index < meta.total; index++) {
@@ -551,6 +571,12 @@ export function createReceivedChunkStore ({
551
571
  })
552
572
  }
553
573
 
574
+ function close () {
575
+ if (closed) return
576
+ closed = true
577
+ dbPromise?.then(db => db.close()).catch(() => {})
578
+ }
579
+
554
580
  return {
555
581
  cleanupStale,
556
582
  drainAvailable,
@@ -559,7 +585,22 @@ export function createReceivedChunkStore ({
559
585
  readChunkContents,
560
586
  readEnvelopeBundleContent,
561
587
  readEnvelopeBundleText,
588
+ ready,
562
589
  removeGroup,
563
- status
590
+ status,
591
+ close
592
+ }
593
+ }
594
+
595
+ export async function cleanupReceivedChunkStorage ({
596
+ indexedDB = globalThis.indexedDB,
597
+ prefix = DEFAULT_PREFIX,
598
+ now = Date.now()
599
+ } = {}) {
600
+ const store = createReceivedChunkStore({ prefix, indexedDB })
601
+ try {
602
+ return await store.cleanupStale(now)
603
+ } finally {
604
+ store.close()
564
605
  }
565
606
  }
@@ -12,6 +12,7 @@ const HEX_PUBKEY = /^[0-9a-f]{64}$/i
12
12
 
13
13
  const watchesByChannel = new Map()
14
14
  const subsByRelay = new Map()
15
+ let nextWatchRevision = 1
15
16
 
16
17
  function nowSeconds () {
17
18
  return Math.floor(Date.now() / 1000)
@@ -254,6 +255,27 @@ function maxWatchNumber (channels, field) {
254
255
  return values.length ? Math.max(...values) : undefined
255
256
  }
256
257
 
258
+ function watchNumbersForChannels (channels, field) {
259
+ const out = {}
260
+ for (const channel of channels) {
261
+ const value = watchesByChannel.get(channel)?.[field]
262
+ if (Number.isFinite(value)) out[channel] = value
263
+ }
264
+ return out
265
+ }
266
+
267
+ function watchRevisionsForChannels (channels) {
268
+ return Object.fromEntries(channels.map(channel => [channel, watchesByChannel.get(channel)?.revision || 0]))
269
+ }
270
+
271
+ function subscriptionMatches (current, channels) {
272
+ if (!current || !setEquals(current.channels, channels)) return false
273
+ for (const channel of channels) {
274
+ if (current.revisions?.[channel] !== watchesByChannel.get(channel)?.revision) return false
275
+ }
276
+ return true
277
+ }
278
+
257
279
  function firstWatchValue (channels, field) {
258
280
  for (const channel of channels) {
259
281
  const value = watchesByChannel.get(channel)?.[field]
@@ -263,25 +285,34 @@ function firstWatchValue (channels, field) {
263
285
  }
264
286
 
265
287
  function closeSubscription (sub, gracefulClose) {
266
- if (gracefulClose) setTimeout(() => sub.close(), RESUBSCRIBE_GRACE_MS)
267
- else sub.close()
288
+ if (gracefulClose) {
289
+ setTimeout(() => Promise.resolve().then(() => sub.close()).catch(() => {}), RESUBSCRIBE_GRACE_MS)
290
+ return null
291
+ }
292
+ try {
293
+ return Promise.resolve(sub.close())
294
+ } catch (err) {
295
+ return Promise.reject(err)
296
+ }
268
297
  }
269
298
 
270
299
  function rebuildSubscriptions ({ _subscribe = privateChannel.subscribe, gracefulClose = true } = {}) {
271
300
  const desired = desiredRelayState()
301
+ const closing = []
272
302
 
273
303
  for (const [relay, current] of subsByRelay) {
274
304
  const nextChannels = desired.get(relay)
275
- if (nextChannels && setEquals(current.channels, nextChannels)) continue
305
+ if (nextChannels && subscriptionMatches(current, nextChannels)) continue
276
306
  if (!nextChannels) {
277
- closeSubscription(current.sub, gracefulClose)
307
+ const close = closeSubscription(current.sub, gracefulClose)
308
+ if (close) closing.push(close)
278
309
  subsByRelay.delete(relay)
279
310
  }
280
311
  }
281
312
 
282
313
  for (const [relay, channels] of desired) {
283
314
  const current = subsByRelay.get(relay)
284
- if (current && setEquals(current.channels, channels)) continue
315
+ if (subscriptionMatches(current, channels)) continue
285
316
 
286
317
  const channelList = [...channels]
287
318
  const firstWatch = watchesByChannel.get(channelList[0])
@@ -300,6 +331,7 @@ function rebuildSubscriptions ({ _subscribe = privateChannel.subscribe, graceful
300
331
  mode: firstWatch.mode,
301
332
  modeByPubkey: modesForChannels(channelList),
302
333
  receivedChunkTtlMs: maxWatchNumber(channelList, 'receivedChunkTtlMs'),
334
+ receivedChunkTtlMsByPubkey: watchNumbersForChannels(channelList, 'receivedChunkTtlMs'),
303
335
  receivedChunkMaxBytes: maxWatchNumber(channelList, 'receivedChunkMaxBytes'),
304
336
  receivedChunkIndexedDB: firstWatchValue(channelList, 'receivedChunkIndexedDB'),
305
337
  ignoredGroupTtlMs: maxWatchNumber(channelList, 'ignoredGroupTtlMs'),
@@ -321,9 +353,17 @@ function rebuildSubscriptions ({ _subscribe = privateChannel.subscribe, graceful
321
353
  onError: err => firstWatch.callbacks.onError?.(err)
322
354
  })
323
355
 
324
- subsByRelay.set(relay, { channels: new Set(channels), sub })
325
- if (current) closeSubscription(current.sub, gracefulClose)
356
+ subsByRelay.set(relay, {
357
+ channels: new Set(channels),
358
+ revisions: watchRevisionsForChannels(channelList),
359
+ sub
360
+ })
361
+ if (current) {
362
+ const close = closeSubscription(current.sub, gracefulClose)
363
+ if (close) closing.push(close)
364
+ }
326
365
  }
366
+ return Promise.allSettled(closing)
327
367
  }
328
368
 
329
369
  export async function watch ({
@@ -379,19 +419,23 @@ export async function watch ({
379
419
  since
380
420
  }
381
421
  const current = watchesByChannel.get(channel)
382
- if (
422
+ const sameSettings = Boolean(
383
423
  current &&
384
- setEquals(new Set(current.relays), new Set(next.relays)) &&
424
+ current.receiverSigner === next.receiverSigner &&
425
+ current.iykcSigner === next.iykcSigner &&
385
426
  current.privateChannelSigner === next.privateChannelSigner &&
386
427
  current.privateChannelReaderSigner === next.privateChannelReaderSigner &&
387
428
  current.privateChannelReaderPubkey === next.privateChannelReaderPubkey &&
429
+ current.receiverPubkey === next.receiverPubkey &&
388
430
  current.mode === next.mode &&
389
431
  current.receivedChunkTtlMs === next.receivedChunkTtlMs &&
390
432
  current.receivedChunkMaxBytes === next.receivedChunkMaxBytes &&
391
433
  current.receivedChunkIndexedDB === next.receivedChunkIndexedDB &&
392
434
  current.ignoredGroupTtlMs === next.ignoredGroupTtlMs &&
393
435
  current.ignoredGroupMaxEntries === next.ignoredGroupMaxEntries
394
- ) {
436
+ )
437
+ next.revision = sameSettings ? current.revision : nextWatchRevision++
438
+ if (sameSettings && setEquals(new Set(current.relays), new Set(next.relays))) {
395
439
  current.callbacks = callbacks
396
440
  continue
397
441
  }
@@ -399,18 +443,19 @@ export async function watch ({
399
443
  changed = true
400
444
  }
401
445
 
402
- if (changed) rebuildSubscriptions({ _subscribe })
446
+ if (changed) await rebuildSubscriptions({ _subscribe })
403
447
  return () => unwatch(channelList)
404
448
  }
405
449
 
406
450
  export function unwatch (channels) {
407
451
  const channelList = channels ? uniq(Array.isArray(channels) ? channels : [channels]) : [...watchesByChannel.keys()]
408
452
  for (const channel of channelList) watchesByChannel.delete(channel)
409
- rebuildSubscriptions({ gracefulClose: false })
453
+ return rebuildSubscriptions({ gracefulClose: false })
410
454
  }
411
455
 
412
456
  export function clearChannelState (channelPubkey) {
413
- if (watchesByChannel.has(channelPubkey)) unwatch(channelPubkey)
457
+ if (watchesByChannel.has(channelPubkey)) return unwatch(channelPubkey)
458
+ return Promise.resolve([])
414
459
  }
415
460
 
416
461
  async function sendPrivateMessage ({