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.
- package/README.md +73 -13
- package/idb-queue/index.js +37 -11
- package/package.json +1 -1
- package/private-channel/index.js +35 -18
- package/private-channel/services/received-chunks.js +71 -30
- package/private-message/index.js +58 -13
- package/private-messenger/index.js +631 -86
- package/private-messenger/recovery/index.js +3 -1
- package/private-messenger/services/channel-state.js +57 -8
- package/private-messenger/services/storage-maintenance.js +420 -0
|
@@ -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 =
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
}
|
package/private-message/index.js
CHANGED
|
@@ -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)
|
|
267
|
-
|
|
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 &&
|
|
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 (
|
|
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, {
|
|
325
|
-
|
|
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
|
-
|
|
422
|
+
const sameSettings = Boolean(
|
|
383
423
|
current &&
|
|
384
|
-
|
|
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 ({
|