libp2r2p 0.6.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 +50 -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 +357 -70
- package/private-messenger/recovery/index.js +3 -1
- package/private-messenger/services/channel-state.js +33 -4
- package/private-messenger/services/storage-maintenance.js +324 -0
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// userSigner,
|
|
4
4
|
// contentKeySigner, // optional when userSigner handles content keys internally
|
|
5
5
|
// nymSigner: optionalDefaultNymSigner,
|
|
6
|
-
// channels: [{ signer: privateChannelSigner, relays, mode: 'leecher', seeders: optionalSeederPubkeys }],
|
|
6
|
+
// channels: [{ signer: privateChannelSigner, relays, mode: 'leecher', seeders: optionalSeederPubkeys, offlineRecoverySeconds: optionalChannelOverride }],
|
|
7
7
|
// onContentKeyChange: event => reviewContentKeyUse(event),
|
|
8
8
|
// onError: err => reportPrivateMessengerError(err)
|
|
9
9
|
// })
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
// - Each watched channel stores lastSeenAt/lastWatchedAt in IndexedDB.
|
|
30
30
|
// - Re-watching after reload fetches the gap from lastSeenAt to now.
|
|
31
31
|
// - Browser offline/online events add explicit offline ranges with a small skew.
|
|
32
|
-
// -
|
|
32
|
+
// - Recovery defaults to 7 days and can be overridden per channel; zero disables durable recovery.
|
|
33
|
+
// - Channel state not watched for 45 days is pruned.
|
|
33
34
|
// - Seeders announce presence every 10min and are used for the relay-uncovered left edge of a missed range.
|
|
34
35
|
// - Configured seeders are all asked; auto-discovered seeders are capped to the 8 most recently active.
|
|
35
36
|
// - Seeder/watchtower channels store reconstructed router events in a separate IndexedDB queue and auto-reply to recovery asks.
|
|
@@ -40,10 +41,17 @@ import * as privateMessage from '../private-message/index.js'
|
|
|
40
41
|
import { bytesToBase64 } from '../base64/index.js'
|
|
41
42
|
import { getRelaysByPubkey, pickRelaysForPubkeys, subscribeRelayListUpdates } from '../relay/index.js'
|
|
42
43
|
import * as privateChannel from '../private-channel/index.js'
|
|
43
|
-
import {
|
|
44
|
+
import { DEFAULT_RECEIVED_CHUNK_TTL_MS } from '../private-channel/services/received-chunks.js'
|
|
44
45
|
import { createQueue } from '../idb-queue/index.js'
|
|
45
46
|
import { createChannelStateStore } from './services/channel-state.js'
|
|
46
47
|
import { DEFAULT_STALE_CHANNEL_SECONDS } from './constants/index.js'
|
|
48
|
+
import {
|
|
49
|
+
activatePrivateMessengerStorage,
|
|
50
|
+
maintainPrivateMessengerStorage,
|
|
51
|
+
PRIVATE_MESSENGER_STORAGE_HEARTBEAT_MS,
|
|
52
|
+
PRIVATE_MESSENGER_STORAGE_MAINTENANCE_MS,
|
|
53
|
+
releasePrivateMessengerStorage
|
|
54
|
+
} from './services/storage-maintenance.js'
|
|
47
55
|
import {
|
|
48
56
|
compactSeedNymCarriers,
|
|
49
57
|
compactSeedRouterRows,
|
|
@@ -72,6 +80,7 @@ export {
|
|
|
72
80
|
} from './recovery/index.js'
|
|
73
81
|
|
|
74
82
|
const DEFAULT_OFFLINE_RECOVERY_SECONDS = 7 * 24 * 60 * 60
|
|
83
|
+
const MAX_OFFLINE_RECOVERY_SECONDS = Math.floor(Number.MAX_SAFE_INTEGER / 1000)
|
|
75
84
|
const DEFAULT_OFFLINE_SKEW_SECONDS = 30
|
|
76
85
|
const DEFAULT_RELOAD_GAP_DELAY_MS = 500
|
|
77
86
|
const DEFAULT_SEEDER_PRESENCE_INTERVAL_MS = 10 * 60 * 1000
|
|
@@ -109,6 +118,13 @@ function nowSeconds () {
|
|
|
109
118
|
return Math.floor(Date.now() / 1000)
|
|
110
119
|
}
|
|
111
120
|
|
|
121
|
+
function normalizeOfflineRecoverySeconds (value) {
|
|
122
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > MAX_OFFLINE_RECOVERY_SECONDS) {
|
|
123
|
+
throw new Error('INVALID_OFFLINE_RECOVERY_SECONDS')
|
|
124
|
+
}
|
|
125
|
+
return value
|
|
126
|
+
}
|
|
127
|
+
|
|
112
128
|
function uniq (values) {
|
|
113
129
|
return [...new Set((values || []).filter(Boolean))]
|
|
114
130
|
}
|
|
@@ -130,9 +146,19 @@ function storesRecoverySeeds (mode) {
|
|
|
130
146
|
return mode === 'seeder' || mode === 'watchtower'
|
|
131
147
|
}
|
|
132
148
|
|
|
149
|
+
function randomStorageLeaseId () {
|
|
150
|
+
if (globalThis.crypto?.randomUUID) return globalThis.crypto.randomUUID()
|
|
151
|
+
const bytes = globalThis.crypto?.getRandomValues?.(new Uint8Array(16))
|
|
152
|
+
if (bytes) return [...bytes].map(value => value.toString(16).padStart(2, '0')).join('')
|
|
153
|
+
return `${Date.now().toString(36)}:${Math.random().toString(36).slice(2)}`
|
|
154
|
+
}
|
|
155
|
+
|
|
133
156
|
export class PrivateMessenger {
|
|
134
|
-
static
|
|
135
|
-
|
|
157
|
+
static maintainStorage ({
|
|
158
|
+
indexedDB = globalThis.indexedDB,
|
|
159
|
+
temporaryStorageArea = globalThis.sessionStorage
|
|
160
|
+
} = {}) {
|
|
161
|
+
return maintainPrivateMessengerStorage({ indexedDB, temporaryStorageArea })
|
|
136
162
|
}
|
|
137
163
|
|
|
138
164
|
constructor ({
|
|
@@ -160,9 +186,11 @@ export class PrivateMessenger {
|
|
|
160
186
|
_subscribeRelayListUpdates = subscribeRelayListUpdates,
|
|
161
187
|
_setTimeout = globalThis.setTimeout.bind(globalThis),
|
|
162
188
|
_setInterval = globalThis.setInterval.bind(globalThis),
|
|
163
|
-
_clearInterval = globalThis.clearInterval.bind(globalThis)
|
|
189
|
+
_clearInterval = globalThis.clearInterval.bind(globalThis),
|
|
190
|
+
_storageSetInterval = globalThis.setInterval.bind(globalThis),
|
|
191
|
+
_storageClearInterval = globalThis.clearInterval.bind(globalThis)
|
|
164
192
|
} = {}) {
|
|
165
|
-
this.offlineRecoverySeconds = offlineRecoverySeconds
|
|
193
|
+
this.offlineRecoverySeconds = normalizeOfflineRecoverySeconds(offlineRecoverySeconds)
|
|
166
194
|
this.staleChannelSeconds = staleChannelSeconds
|
|
167
195
|
this.offlineSkewSeconds = offlineSkewSeconds
|
|
168
196
|
this.reloadGapDelayMs = reloadGapDelayMs
|
|
@@ -187,6 +215,8 @@ export class PrivateMessenger {
|
|
|
187
215
|
this._setTimeout = _setTimeout
|
|
188
216
|
this._setInterval = _setInterval
|
|
189
217
|
this._clearInterval = _clearInterval
|
|
218
|
+
this._storageSetInterval = _storageSetInterval
|
|
219
|
+
this._storageClearInterval = _storageClearInterval
|
|
190
220
|
|
|
191
221
|
this.userSigner = null
|
|
192
222
|
this.contentKeySigner = null
|
|
@@ -208,42 +238,170 @@ export class PrivateMessenger {
|
|
|
208
238
|
this.stopOnline = null
|
|
209
239
|
this.stopOffline = null
|
|
210
240
|
this.queueOperationTail = Promise.resolve()
|
|
241
|
+
this.storageActive = false
|
|
242
|
+
this.storageLeaseId = randomStorageLeaseId()
|
|
243
|
+
this.lastStorageTouch = 0
|
|
244
|
+
this.storageTouchPromise = null
|
|
245
|
+
this.storageHeartbeatTimer = null
|
|
246
|
+
this.storageMaintenanceTimer = null
|
|
247
|
+
this.storageMaintenancePromise = null
|
|
248
|
+
this.closePromise = null
|
|
249
|
+
this.initSettledPromise = null
|
|
250
|
+
this.initialized = false
|
|
211
251
|
}
|
|
212
252
|
|
|
213
253
|
async init ({ userSigner, contentKeySigner, nymSigner, channels = [], relays = [], mode = 'leecher' }) {
|
|
214
254
|
if (!userSigner?.getPublicKey) throw new Error('USER_SIGNER_REQUIRED')
|
|
215
|
-
|
|
216
|
-
this.
|
|
217
|
-
this.
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
this.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
255
|
+
this.assertOpen()
|
|
256
|
+
if (this.initSettledPromise) throw new Error('PRIVATE_MESSENGER_INIT_IN_PROGRESS')
|
|
257
|
+
if (this.initialized) throw new Error('PRIVATE_MESSENGER_ALREADY_INITIALIZED')
|
|
258
|
+
let settleInit
|
|
259
|
+
const initSettledPromise = new Promise(resolve => { settleInit = resolve })
|
|
260
|
+
this.initSettledPromise = initSettledPromise
|
|
261
|
+
try {
|
|
262
|
+
this.userSigner = userSigner
|
|
263
|
+
this.contentKeySigner = contentKeySigner || null
|
|
264
|
+
this.nymSigner = nymSigner || null
|
|
265
|
+
this.userPubkey = await userSigner.getPublicKey()
|
|
266
|
+
this.prefix = `libp2r2p:private-messenger:${this.userPubkey}`
|
|
267
|
+
await activatePrivateMessengerStorage({
|
|
268
|
+
userPubkey: this.userPubkey,
|
|
269
|
+
leaseId: this.storageLeaseId,
|
|
270
|
+
indexedDB: this._indexedDB
|
|
271
|
+
})
|
|
272
|
+
this.storageActive = true
|
|
273
|
+
this.lastStorageTouch = Date.now()
|
|
274
|
+
this.assertOpen()
|
|
275
|
+
await PrivateMessenger.maintainStorage({
|
|
276
|
+
indexedDB: this._indexedDB,
|
|
277
|
+
temporaryStorageArea: this.temporaryStorageArea
|
|
278
|
+
})
|
|
279
|
+
this.assertOpen()
|
|
280
|
+
this.contentKeyPubkey = await this.contentKeySigner?.getPublicKey?.() || ''
|
|
281
|
+
this.assertOpen()
|
|
282
|
+
this.queue = await createQueue({
|
|
283
|
+
prefix: this.prefix,
|
|
284
|
+
indexes: MESSAGE_QUEUE_INDEXES,
|
|
285
|
+
maxBytes: this.messageQueueMaxBytes,
|
|
286
|
+
evictionPolicy: 'fifo',
|
|
287
|
+
indexedDB: this._indexedDB
|
|
288
|
+
})
|
|
289
|
+
this.assertOpen()
|
|
290
|
+
this.seedQueue = await createQueue({
|
|
291
|
+
prefix: `${this.prefix}:seeds`,
|
|
292
|
+
indexes: SEED_QUEUE_INDEXES,
|
|
293
|
+
maxBytes: this.seedQueueMaxBytes,
|
|
294
|
+
evictionPolicy: 'fifo',
|
|
295
|
+
indexedDB: this._indexedDB
|
|
296
|
+
})
|
|
297
|
+
this.assertOpen()
|
|
298
|
+
this.stateStore = await createChannelStateStore({
|
|
299
|
+
prefix: this.prefix,
|
|
300
|
+
indexedDB: this._indexedDB
|
|
301
|
+
})
|
|
302
|
+
this.assertOpen()
|
|
303
|
+
this.startStorageMaintenance()
|
|
304
|
+
this.state = { channels: await this.stateStore.load() }
|
|
305
|
+
await this.cleanupStaleChannels()
|
|
306
|
+
await this.update({ userSigner, contentKeySigner, nymSigner: this.nymSigner, channels, relays, mode })
|
|
307
|
+
await this.pruneStoredSeeds()
|
|
308
|
+
this.initialized = true
|
|
309
|
+
return this
|
|
310
|
+
} catch (err) {
|
|
311
|
+
this.stopStorageMaintenance()
|
|
312
|
+
try { await this.queueOperationTail } catch {}
|
|
313
|
+
try { await this.stateWriteTail } catch {}
|
|
314
|
+
await Promise.allSettled([
|
|
315
|
+
this.queue?.close?.(),
|
|
316
|
+
this.seedQueue?.close?.(),
|
|
317
|
+
this.stateStore?.close?.()
|
|
318
|
+
])
|
|
319
|
+
if (this.storageActive) {
|
|
320
|
+
try {
|
|
321
|
+
await releasePrivateMessengerStorage({
|
|
322
|
+
userPubkey: this.userPubkey,
|
|
323
|
+
leaseId: this.storageLeaseId,
|
|
324
|
+
indexedDB: this._indexedDB
|
|
325
|
+
})
|
|
326
|
+
} catch {}
|
|
327
|
+
}
|
|
328
|
+
this.storageActive = false
|
|
329
|
+
this.initialized = false
|
|
330
|
+
throw err
|
|
331
|
+
} finally {
|
|
332
|
+
settleInit()
|
|
333
|
+
if (this.initSettledPromise === initSettledPromise) this.initSettledPromise = null
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
assertOpen () {
|
|
338
|
+
if (this.closePromise) throw new Error('PRIVATE_MESSENGER_CLOSED')
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
startStorageMaintenance () {
|
|
342
|
+
if (this.storageHeartbeatTimer || this.storageMaintenanceTimer) return
|
|
343
|
+
this.storageHeartbeatTimer = this._storageSetInterval(() => (
|
|
344
|
+
this.touchStorageActivity({ force: true }).catch(err => {
|
|
345
|
+
try { this.onError?.(err) } catch {}
|
|
346
|
+
})
|
|
347
|
+
), PRIVATE_MESSENGER_STORAGE_HEARTBEAT_MS)
|
|
348
|
+
this.storageMaintenanceTimer = this._storageSetInterval(
|
|
349
|
+
() => this.runStorageMaintenance(),
|
|
350
|
+
PRIVATE_MESSENGER_STORAGE_MAINTENANCE_MS
|
|
351
|
+
)
|
|
352
|
+
this.storageHeartbeatTimer?.unref?.()
|
|
353
|
+
this.storageMaintenanceTimer?.unref?.()
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
stopStorageMaintenance () {
|
|
357
|
+
if (this.storageHeartbeatTimer) this._storageClearInterval(this.storageHeartbeatTimer)
|
|
358
|
+
if (this.storageMaintenanceTimer) this._storageClearInterval(this.storageMaintenanceTimer)
|
|
359
|
+
this.storageHeartbeatTimer = null
|
|
360
|
+
this.storageMaintenanceTimer = null
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
runStorageMaintenance () {
|
|
364
|
+
if (this.storageMaintenancePromise) return this.storageMaintenancePromise
|
|
365
|
+
const maintenance = Promise.all([
|
|
366
|
+
PrivateMessenger.maintainStorage({
|
|
367
|
+
indexedDB: this._indexedDB,
|
|
368
|
+
temporaryStorageArea: this.temporaryStorageArea
|
|
369
|
+
}),
|
|
370
|
+
this.pruneStoredSeeds()
|
|
371
|
+
]).catch(err => {
|
|
372
|
+
try { this.onError?.(err) } catch {}
|
|
373
|
+
}).finally(() => {
|
|
374
|
+
if (this.storageMaintenancePromise === maintenance) this.storageMaintenancePromise = null
|
|
235
375
|
})
|
|
236
|
-
this.
|
|
237
|
-
|
|
238
|
-
|
|
376
|
+
this.storageMaintenancePromise = maintenance
|
|
377
|
+
return maintenance
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async touchStorageActivity ({ force = false } = {}) {
|
|
381
|
+
if (!this.storageActive || this.closePromise) return false
|
|
382
|
+
if (this.storageTouchPromise) return this.storageTouchPromise
|
|
383
|
+
const now = Date.now()
|
|
384
|
+
if (!force && now - this.lastStorageTouch < PRIVATE_MESSENGER_STORAGE_HEARTBEAT_MS) return false
|
|
385
|
+
const previousTouch = this.lastStorageTouch
|
|
386
|
+
this.lastStorageTouch = now
|
|
387
|
+
const touch = activatePrivateMessengerStorage({
|
|
388
|
+
userPubkey: this.userPubkey,
|
|
389
|
+
leaseId: this.storageLeaseId,
|
|
390
|
+
indexedDB: this._indexedDB,
|
|
391
|
+
now
|
|
392
|
+
}).then(() => true, err => {
|
|
393
|
+
this.lastStorageTouch = previousTouch
|
|
394
|
+
throw err
|
|
395
|
+
}).finally(() => {
|
|
396
|
+
if (this.storageTouchPromise === touch) this.storageTouchPromise = null
|
|
239
397
|
})
|
|
240
|
-
this.
|
|
241
|
-
|
|
242
|
-
await this.update({ userSigner, contentKeySigner, nymSigner: this.nymSigner, channels, relays, mode })
|
|
243
|
-
return this
|
|
398
|
+
this.storageTouchPromise = touch
|
|
399
|
+
return touch
|
|
244
400
|
}
|
|
245
401
|
|
|
246
402
|
runQueueOperation (operation) {
|
|
403
|
+
if (this.closePromise) return Promise.reject(new Error('PRIVATE_MESSENGER_CLOSED'))
|
|
404
|
+
this.touchStorageActivity().catch(err => this.onError?.(err))
|
|
247
405
|
const run = this.queueOperationTail.then(operation)
|
|
248
406
|
this.queueOperationTail = run.catch(err => {
|
|
249
407
|
try { this.onError?.(err) } catch {}
|
|
@@ -278,6 +436,7 @@ export class PrivateMessenger {
|
|
|
278
436
|
}
|
|
279
437
|
|
|
280
438
|
async update ({ userSigner = this.userSigner, contentKeySigner = this.contentKeySigner, nymSigner = this.nymSigner, channels = [...this.channels.values()], relays = [], mode = 'leecher' } = {}) {
|
|
439
|
+
this.assertOpen()
|
|
281
440
|
if (userSigner) {
|
|
282
441
|
const userPubkey = await userSigner.getPublicKey?.()
|
|
283
442
|
if (!userPubkey) throw new Error('USER_SIGNER_REQUIRED')
|
|
@@ -288,17 +447,19 @@ export class PrivateMessenger {
|
|
|
288
447
|
this.nymSigner = nymSigner || null
|
|
289
448
|
this.contentKeyPubkey = await this.contentKeySigner?.getPublicKey?.() || ''
|
|
290
449
|
const nextChannels = await this.normalizeChannels(channels, { relays, mode })
|
|
450
|
+
this.assertOpen()
|
|
291
451
|
const nextPubkeys = new Set(nextChannels.map(channel => channel.pubkey))
|
|
292
452
|
|
|
293
453
|
for (const pubkey of [...this.channels.keys()]) {
|
|
294
454
|
if (!nextPubkeys.has(pubkey)) {
|
|
295
|
-
this.unwatch(pubkey)
|
|
455
|
+
await this.unwatch(pubkey)
|
|
296
456
|
this.channels.delete(pubkey)
|
|
297
457
|
}
|
|
298
458
|
}
|
|
299
459
|
for (const channel of nextChannels) this.channels.set(channel.pubkey, channel)
|
|
300
460
|
|
|
301
461
|
await this.cleanupStaleChannels()
|
|
462
|
+
await this.applyRecoveryPolicies(nextChannels)
|
|
302
463
|
await this.watch()
|
|
303
464
|
await this.reconcilePresencePublishers()
|
|
304
465
|
return this
|
|
@@ -323,6 +484,9 @@ export class PrivateMessenger {
|
|
|
323
484
|
const autoDeletionCapability = channel.autoDeletionCapability === undefined
|
|
324
485
|
? undefined
|
|
325
486
|
: normalizeAutoDeletionCapability(channel.autoDeletionCapability)
|
|
487
|
+
const offlineRecoverySeconds = normalizeOfflineRecoverySeconds(
|
|
488
|
+
channel.offlineRecoverySeconds ?? this.offlineRecoverySeconds
|
|
489
|
+
)
|
|
326
490
|
out.push({
|
|
327
491
|
pubkey,
|
|
328
492
|
signer,
|
|
@@ -334,6 +498,7 @@ export class PrivateMessenger {
|
|
|
334
498
|
usesNip65WatchRelays: !hasChannelRelays && !hasDefaultRelays,
|
|
335
499
|
mode,
|
|
336
500
|
seeders: uniq(channel.seeders),
|
|
501
|
+
offlineRecoverySeconds,
|
|
337
502
|
autoDeletionCapability
|
|
338
503
|
})
|
|
339
504
|
}
|
|
@@ -355,6 +520,7 @@ export class PrivateMessenger {
|
|
|
355
520
|
}
|
|
356
521
|
|
|
357
522
|
async recoveryMirrorRelays (channelPubkey) {
|
|
523
|
+
if (!this.offlineRecoverySecondsFor(channelPubkey)) return []
|
|
358
524
|
const seeders = this.recoverySeeders(channelPubkey)
|
|
359
525
|
if (!seeders.length) return []
|
|
360
526
|
try {
|
|
@@ -386,6 +552,7 @@ export class PrivateMessenger {
|
|
|
386
552
|
}
|
|
387
553
|
|
|
388
554
|
writeState (state) {
|
|
555
|
+
this.touchStorageActivity().catch(err => this.onError?.(err))
|
|
389
556
|
const next = {
|
|
390
557
|
channels: isPlainObject(state?.channels) ? structuredClone(state.channels) : {}
|
|
391
558
|
}
|
|
@@ -432,6 +599,7 @@ export class PrivateMessenger {
|
|
|
432
599
|
}
|
|
433
600
|
|
|
434
601
|
recoverySeeders (pubkey) {
|
|
602
|
+
if (!this.offlineRecoverySecondsFor(pubkey)) return []
|
|
435
603
|
const channel = this.channels.get(pubkey)
|
|
436
604
|
const configuredSeeders = channel?.seeders || []
|
|
437
605
|
if (configuredSeeders.length) return configuredSeeders.filter(seeder => seeder !== this.userPubkey)
|
|
@@ -446,6 +614,7 @@ export class PrivateMessenger {
|
|
|
446
614
|
}
|
|
447
615
|
|
|
448
616
|
markSeederActive (channelPubkey, seederPubkey, { announced = false, at = nowSeconds() } = {}) {
|
|
617
|
+
if (!this.offlineRecoverySecondsFor(channelPubkey)) return false
|
|
449
618
|
const state = this.readState()
|
|
450
619
|
const current = state.channels[channelPubkey] || {}
|
|
451
620
|
const activity = current.seederActivity || {}
|
|
@@ -459,6 +628,7 @@ export class PrivateMessenger {
|
|
|
459
628
|
current.seederActivity = activity
|
|
460
629
|
state.channels[channelPubkey] = current
|
|
461
630
|
this.writeState(state)
|
|
631
|
+
return true
|
|
462
632
|
}
|
|
463
633
|
|
|
464
634
|
trackSeederActivity (channelPubkey, message) {
|
|
@@ -544,8 +714,10 @@ export class PrivateMessenger {
|
|
|
544
714
|
}
|
|
545
715
|
|
|
546
716
|
addOfflineRange (pubkey, start, end) {
|
|
717
|
+
const recoverySeconds = this.offlineRecoverySecondsFor(pubkey)
|
|
718
|
+
if (!recoverySeconds) return
|
|
547
719
|
const now = nowSeconds()
|
|
548
|
-
const minStart = now -
|
|
720
|
+
const minStart = now - recoverySeconds
|
|
549
721
|
const normalized = {
|
|
550
722
|
start: Math.max(0, Math.floor(start)),
|
|
551
723
|
end: Math.floor(end)
|
|
@@ -567,10 +739,17 @@ export class PrivateMessenger {
|
|
|
567
739
|
closeOpenOfflineRanges () {
|
|
568
740
|
const state = this.readState()
|
|
569
741
|
const end = nowSeconds()
|
|
570
|
-
const minStart = end - this.offlineRecoverySeconds
|
|
571
742
|
for (const pubkey of Object.keys(state.channels)) {
|
|
572
743
|
const current = state.channels[pubkey]
|
|
573
744
|
if (!current.openOfflineStart) continue
|
|
745
|
+
const recoverySeconds = this.offlineRecoverySecondsFor(pubkey)
|
|
746
|
+
if (!recoverySeconds) {
|
|
747
|
+
delete current.openOfflineStart
|
|
748
|
+
current.offlineRanges = []
|
|
749
|
+
state.channels[pubkey] = current
|
|
750
|
+
continue
|
|
751
|
+
}
|
|
752
|
+
const minStart = end - recoverySeconds
|
|
574
753
|
const start = Math.max(minStart, Math.max(0, current.openOfflineStart))
|
|
575
754
|
if (end > start) {
|
|
576
755
|
current.offlineRanges = mergeRanges((current.offlineRanges || []).concat([{ start, end }]))
|
|
@@ -582,11 +761,13 @@ export class PrivateMessenger {
|
|
|
582
761
|
}
|
|
583
762
|
|
|
584
763
|
async watch (channels = [...this.channels.keys()], { scheduleReloadGap = true } = {}) {
|
|
764
|
+
this.assertOpen()
|
|
585
765
|
const channelPubkeys = uniq(channels)
|
|
586
766
|
for (const pubkey of channelPubkeys) {
|
|
587
767
|
const channel = this.channels.get(pubkey)
|
|
588
768
|
if (!channel) throw new Error('UNKNOWN_CHANNEL')
|
|
589
769
|
const watchRelays = await this.resolveWatchRelays(channel)
|
|
770
|
+
this.assertOpen()
|
|
590
771
|
const stop = await this._privateMessage.watch({
|
|
591
772
|
channels: [pubkey],
|
|
592
773
|
relays: watchRelays,
|
|
@@ -604,16 +785,21 @@ export class PrivateMessenger {
|
|
|
604
785
|
onMessage: message => this.queueIncoming(() => this.handleMessage(pubkey, message)),
|
|
605
786
|
onSeed: seed => this.queueIncoming(() => this.enqueueSeed(pubkey, seed)),
|
|
606
787
|
onContentKeyUsage: usage => this.handleContentKeyUsage(pubkey, usage),
|
|
607
|
-
receivedChunkTtlMs: this.
|
|
788
|
+
receivedChunkTtlMs: this.receivedChunkTtlMsFor(channel),
|
|
608
789
|
receivedChunkIndexedDB: this._indexedDB,
|
|
609
790
|
onError: err => this.onError?.(err)
|
|
610
791
|
})
|
|
792
|
+
if (this.closePromise) {
|
|
793
|
+
await stop?.()
|
|
794
|
+
this.assertOpen()
|
|
795
|
+
}
|
|
611
796
|
this.stopByChannel.set(pubkey, stop)
|
|
612
797
|
this.updateChannelState(pubkey, {
|
|
613
798
|
lastWatchedAt: nowSeconds(),
|
|
614
799
|
mode: channel.mode,
|
|
615
800
|
relays: watchRelays,
|
|
616
|
-
seeders: channel.seeders
|
|
801
|
+
seeders: channel.seeders,
|
|
802
|
+
offlineRecoverySeconds: channel.offlineRecoverySeconds
|
|
617
803
|
})
|
|
618
804
|
this.debug('watch', {
|
|
619
805
|
channelPubkey: pubkey,
|
|
@@ -631,12 +817,15 @@ export class PrivateMessenger {
|
|
|
631
817
|
|
|
632
818
|
unwatch (channels) {
|
|
633
819
|
const channelPubkeys = channels ? uniq(Array.isArray(channels) ? channels : [channels]) : [...this.stopByChannel.keys()]
|
|
820
|
+
const closing = []
|
|
634
821
|
for (const pubkey of channelPubkeys) {
|
|
635
|
-
this.stopByChannel.get(pubkey)?.()
|
|
822
|
+
const close = this.stopByChannel.get(pubkey)?.()
|
|
823
|
+
if (close && typeof close.then === 'function') closing.push(close)
|
|
636
824
|
this.stopByChannel.delete(pubkey)
|
|
637
825
|
this.stopPresencePublisher(pubkey)
|
|
638
826
|
}
|
|
639
827
|
this.ensureRelayListWatcher()
|
|
828
|
+
return Promise.allSettled(closing)
|
|
640
829
|
}
|
|
641
830
|
|
|
642
831
|
nip65WatchChannelPubkeys () {
|
|
@@ -762,6 +951,7 @@ export class PrivateMessenger {
|
|
|
762
951
|
}
|
|
763
952
|
|
|
764
953
|
async enqueueSeed (channelPubkey, seed) {
|
|
954
|
+
if (!this.offlineRecoverySecondsFor(channelPubkey)) return
|
|
765
955
|
const receivedAt = nowSeconds()
|
|
766
956
|
if (seed.recordType === NYM_CARRIER_SEED_RECORD_TYPE || seed.carriers?.length) {
|
|
767
957
|
const carriers = compactSeedNymCarriers(seed.carriers)
|
|
@@ -818,6 +1008,7 @@ export class PrivateMessenger {
|
|
|
818
1008
|
|
|
819
1009
|
async nextMessage () {
|
|
820
1010
|
// seedQueue is retained replay material for recovery replies, not an app-message stream.
|
|
1011
|
+
this.touchStorageActivity().catch(err => this.onError?.(err))
|
|
821
1012
|
await this.queueOperationTail
|
|
822
1013
|
return withoutQueueMetadata(await this.queue.shift())
|
|
823
1014
|
}
|
|
@@ -833,7 +1024,7 @@ export class PrivateMessenger {
|
|
|
833
1024
|
privateChannelReaderPubkey: channel.readerPubkey,
|
|
834
1025
|
receiverPubkey,
|
|
835
1026
|
...routing,
|
|
836
|
-
expirationSeconds: this.
|
|
1027
|
+
expirationSeconds: this.eventExpirationSecondsFor(channel),
|
|
837
1028
|
temporaryStorageArea: this.temporaryStorageArea,
|
|
838
1029
|
deletionPubkey,
|
|
839
1030
|
autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
|
|
@@ -859,7 +1050,7 @@ export class PrivateMessenger {
|
|
|
859
1050
|
question,
|
|
860
1051
|
receiverPubkey,
|
|
861
1052
|
...routing,
|
|
862
|
-
expirationSeconds: this.
|
|
1053
|
+
expirationSeconds: this.eventExpirationSecondsFor(channel),
|
|
863
1054
|
temporaryStorageArea: this.temporaryStorageArea,
|
|
864
1055
|
deletionPubkey,
|
|
865
1056
|
autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
|
|
@@ -883,7 +1074,7 @@ export class PrivateMessenger {
|
|
|
883
1074
|
privateChannelReaderPubkey: channel.readerPubkey,
|
|
884
1075
|
receiverPubkey,
|
|
885
1076
|
...routing,
|
|
886
|
-
expirationSeconds: this.
|
|
1077
|
+
expirationSeconds: this.eventExpirationSecondsFor(channel),
|
|
887
1078
|
temporaryStorageArea: this.temporaryStorageArea,
|
|
888
1079
|
deletionPubkey,
|
|
889
1080
|
autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
|
|
@@ -907,7 +1098,7 @@ export class PrivateMessenger {
|
|
|
907
1098
|
privateChannelReaderPubkey: channel.readerPubkey,
|
|
908
1099
|
receiverPubkeys,
|
|
909
1100
|
...routing,
|
|
910
|
-
expirationSeconds: this.
|
|
1101
|
+
expirationSeconds: this.eventExpirationSecondsFor(channel),
|
|
911
1102
|
temporaryStorageArea: this.temporaryStorageArea,
|
|
912
1103
|
deletionPubkey,
|
|
913
1104
|
autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
|
|
@@ -931,7 +1122,7 @@ export class PrivateMessenger {
|
|
|
931
1122
|
privateChannelReaderPubkey: channel.readerPubkey,
|
|
932
1123
|
receiverPubkeys,
|
|
933
1124
|
...routing,
|
|
934
|
-
expirationSeconds: this.
|
|
1125
|
+
expirationSeconds: this.eventExpirationSecondsFor(channel),
|
|
935
1126
|
temporaryStorageArea: this.temporaryStorageArea,
|
|
936
1127
|
deletionPubkey,
|
|
937
1128
|
autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
|
|
@@ -951,7 +1142,7 @@ export class PrivateMessenger {
|
|
|
951
1142
|
privateChannelReaderPubkey: channel.readerPubkey,
|
|
952
1143
|
receiverPubkeys,
|
|
953
1144
|
...routing,
|
|
954
|
-
expirationSeconds: this.
|
|
1145
|
+
expirationSeconds: this.eventExpirationSecondsFor(channel),
|
|
955
1146
|
temporaryStorageArea: this.temporaryStorageArea,
|
|
956
1147
|
deletionPubkey,
|
|
957
1148
|
autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
|
|
@@ -970,7 +1161,7 @@ export class PrivateMessenger {
|
|
|
970
1161
|
privateChannelSigner: channel.signer,
|
|
971
1162
|
privateChannelReaderPubkey: channel.readerPubkey,
|
|
972
1163
|
...routing,
|
|
973
|
-
expirationSeconds: this.
|
|
1164
|
+
expirationSeconds: this.eventExpirationSecondsFor(channel),
|
|
974
1165
|
deletionPubkey,
|
|
975
1166
|
autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
|
|
976
1167
|
rumor
|
|
@@ -987,7 +1178,7 @@ export class PrivateMessenger {
|
|
|
987
1178
|
privateChannelSigner: channel.signer,
|
|
988
1179
|
privateChannelReaderPubkey: channel.readerPubkey,
|
|
989
1180
|
...routing,
|
|
990
|
-
expirationSeconds: this.
|
|
1181
|
+
expirationSeconds: this.eventExpirationSecondsFor(channel),
|
|
991
1182
|
deletionPubkey,
|
|
992
1183
|
autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
|
|
993
1184
|
event
|
|
@@ -996,6 +1187,7 @@ export class PrivateMessenger {
|
|
|
996
1187
|
|
|
997
1188
|
async publishSeederPresence (channelPubkey = this.defaultChannelPubkey()) {
|
|
998
1189
|
const channel = this.requireWritableChannel(channelPubkey)
|
|
1190
|
+
if (!this.offlineRecoverySecondsFor(channel)) return null
|
|
999
1191
|
const receiverPubkeys = uniq([...this.knownSeeders(channelPubkey), this.userPubkey])
|
|
1000
1192
|
const routing = await this.resolveSendRouting({ channel, receiverPubkeys })
|
|
1001
1193
|
this.debugSend('yell', channelPubkey, { code: SEEDER_PRESENCE_CODE, receiverPubkeys })
|
|
@@ -1006,7 +1198,7 @@ export class PrivateMessenger {
|
|
|
1006
1198
|
privateChannelReaderPubkey: channel.readerPubkey,
|
|
1007
1199
|
receiverPubkeys,
|
|
1008
1200
|
...routing,
|
|
1009
|
-
expirationSeconds: this.
|
|
1201
|
+
expirationSeconds: this.eventExpirationSecondsFor(channel),
|
|
1010
1202
|
temporaryStorageArea: this.temporaryStorageArea,
|
|
1011
1203
|
autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
|
|
1012
1204
|
code: SEEDER_PRESENCE_CODE,
|
|
@@ -1016,6 +1208,7 @@ export class PrivateMessenger {
|
|
|
1016
1208
|
}
|
|
1017
1209
|
|
|
1018
1210
|
async startPresencePublisher (channelPubkey) {
|
|
1211
|
+
if (!this.offlineRecoverySecondsFor(channelPubkey)) return
|
|
1019
1212
|
if (this.presenceTimers.has(channelPubkey)) return
|
|
1020
1213
|
try {
|
|
1021
1214
|
await this.publishSeederPresence(channelPubkey)
|
|
@@ -1040,10 +1233,10 @@ export class PrivateMessenger {
|
|
|
1040
1233
|
async reconcilePresencePublishers () {
|
|
1041
1234
|
const starts = []
|
|
1042
1235
|
for (const pubkey of [...this.presenceTimers.keys()]) {
|
|
1043
|
-
if (!storesRecoverySeeds(this.channels.get(pubkey)?.mode)) this.stopPresencePublisher(pubkey)
|
|
1236
|
+
if (!storesRecoverySeeds(this.channels.get(pubkey)?.mode) || !this.offlineRecoverySecondsFor(pubkey)) this.stopPresencePublisher(pubkey)
|
|
1044
1237
|
}
|
|
1045
1238
|
for (const [pubkey, channel] of this.channels) {
|
|
1046
|
-
if (storesRecoverySeeds(channel.mode)) starts.push(this.startPresencePublisher(pubkey))
|
|
1239
|
+
if (storesRecoverySeeds(channel.mode) && this.offlineRecoverySecondsFor(channel)) starts.push(this.startPresencePublisher(pubkey))
|
|
1047
1240
|
else this.stopPresencePublisher(pubkey)
|
|
1048
1241
|
}
|
|
1049
1242
|
await Promise.all(starts)
|
|
@@ -1077,6 +1270,53 @@ export class PrivateMessenger {
|
|
|
1077
1270
|
return channel.autoDeletionCapability ?? this.autoDeletionCapability
|
|
1078
1271
|
}
|
|
1079
1272
|
|
|
1273
|
+
offlineRecoverySecondsFor (channelOrPubkey) {
|
|
1274
|
+
const channel = typeof channelOrPubkey === 'string'
|
|
1275
|
+
? this.channels.get(channelOrPubkey)
|
|
1276
|
+
: channelOrPubkey
|
|
1277
|
+
if (channel?.offlineRecoverySeconds !== undefined) return channel.offlineRecoverySeconds
|
|
1278
|
+
const pubkey = typeof channelOrPubkey === 'string' ? channelOrPubkey : channelOrPubkey?.pubkey
|
|
1279
|
+
const persisted = pubkey ? this.state.channels[pubkey]?.offlineRecoverySeconds : undefined
|
|
1280
|
+
return persisted === undefined
|
|
1281
|
+
? this.offlineRecoverySeconds
|
|
1282
|
+
: normalizeOfflineRecoverySeconds(persisted)
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
eventExpirationSecondsFor (channel) {
|
|
1286
|
+
return this.offlineRecoverySecondsFor(channel) || privateChannel.EXPIRATION_SECONDS
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
receivedChunkTtlMsFor (channel) {
|
|
1290
|
+
const seconds = this.offlineRecoverySecondsFor(channel)
|
|
1291
|
+
return seconds ? seconds * 1000 : DEFAULT_RECEIVED_CHUNK_TTL_MS
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
async applyRecoveryPolicies (channels) {
|
|
1295
|
+
return this.runQueueOperation(async () => {
|
|
1296
|
+
const state = this.readState()
|
|
1297
|
+
const now = nowSeconds()
|
|
1298
|
+
for (const channel of channels) {
|
|
1299
|
+
const current = state.channels[channel.pubkey] || {}
|
|
1300
|
+
const seconds = channel.offlineRecoverySeconds
|
|
1301
|
+
current.offlineRecoverySeconds = seconds
|
|
1302
|
+
if (!seconds) {
|
|
1303
|
+
delete current.openOfflineStart
|
|
1304
|
+
current.offlineRanges = []
|
|
1305
|
+
} else {
|
|
1306
|
+
const cutoff = now - seconds
|
|
1307
|
+
current.offlineRanges = mergeRanges((current.offlineRanges || [])
|
|
1308
|
+
.filter(range => range.end >= cutoff)
|
|
1309
|
+
.map(range => ({ ...range, start: Math.max(range.start, cutoff) })))
|
|
1310
|
+
if (current.openOfflineStart) current.openOfflineStart = Math.max(current.openOfflineStart, cutoff)
|
|
1311
|
+
}
|
|
1312
|
+
state.channels[channel.pubkey] = current
|
|
1313
|
+
}
|
|
1314
|
+
this.writeState(state)
|
|
1315
|
+
await this.flushStateWrites()
|
|
1316
|
+
for (const channel of channels) await this.pruneStoredSeeds(channel.pubkey)
|
|
1317
|
+
})
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1080
1320
|
requireNymSigner (channel, override) {
|
|
1081
1321
|
const signer = override || channel?.nymSigner || this.nymSigner
|
|
1082
1322
|
if (!signer?.getPublicKey) throw new Error('NYM_SIGNER_REQUIRED')
|
|
@@ -1088,6 +1328,7 @@ export class PrivateMessenger {
|
|
|
1088
1328
|
}
|
|
1089
1329
|
|
|
1090
1330
|
scheduleReloadGap (pubkey) {
|
|
1331
|
+
if (!this.offlineRecoverySecondsFor(pubkey)) return
|
|
1091
1332
|
const current = this.readState().channels[pubkey]
|
|
1092
1333
|
const start = current?.openOfflineStart || current?.lastSeenAt
|
|
1093
1334
|
if (!start) return
|
|
@@ -1117,6 +1358,7 @@ export class PrivateMessenger {
|
|
|
1117
1358
|
const state = this.readState()
|
|
1118
1359
|
const start = Math.max(0, nowSeconds() - this.offlineSkewSeconds)
|
|
1119
1360
|
for (const pubkey of this.channels.keys()) {
|
|
1361
|
+
if (!this.offlineRecoverySecondsFor(pubkey)) continue
|
|
1120
1362
|
const current = state.channels[pubkey] || {}
|
|
1121
1363
|
current.openOfflineStart ||= start
|
|
1122
1364
|
state.channels[pubkey] = current
|
|
@@ -1139,6 +1381,7 @@ export class PrivateMessenger {
|
|
|
1139
1381
|
}
|
|
1140
1382
|
|
|
1141
1383
|
async askSeedersForMissingRange (channelPubkey, since, until) {
|
|
1384
|
+
if (!this.offlineRecoverySecondsFor(channelPubkey)) return []
|
|
1142
1385
|
if (!this.channels.get(channelPubkey)?.signer) return []
|
|
1143
1386
|
const seeders = this.recoverySeeders(channelPubkey)
|
|
1144
1387
|
if (!seeders.length || until < since) return []
|
|
@@ -1175,11 +1418,14 @@ export class PrivateMessenger {
|
|
|
1175
1418
|
question: message.event,
|
|
1176
1419
|
receiverPubkey: message.event?.pubkey,
|
|
1177
1420
|
since,
|
|
1178
|
-
until
|
|
1421
|
+
until,
|
|
1422
|
+
sendEmptyReply: !this.offlineRecoverySecondsFor(channelPubkey)
|
|
1179
1423
|
})
|
|
1180
1424
|
|
|
1181
|
-
|
|
1182
|
-
await
|
|
1425
|
+
if (this.offlineRecoverySecondsFor(channelPubkey)) {
|
|
1426
|
+
for await (const seed of this.seedQueue.storedItemsBy('byChannel', channelPubkey)) {
|
|
1427
|
+
await packer.update(seed)
|
|
1428
|
+
}
|
|
1183
1429
|
}
|
|
1184
1430
|
await packer.finalize()
|
|
1185
1431
|
}
|
|
@@ -1263,12 +1509,14 @@ export class PrivateMessenger {
|
|
|
1263
1509
|
async recoverOfflineRanges (channels = [...this.stopByChannel.keys()]) {
|
|
1264
1510
|
const state = this.readState()
|
|
1265
1511
|
const now = nowSeconds()
|
|
1266
|
-
const minStart = now - this.offlineRecoverySeconds
|
|
1267
1512
|
|
|
1268
1513
|
for (const pubkey of uniq(channels)) {
|
|
1269
1514
|
const channel = this.channels.get(pubkey)
|
|
1270
1515
|
const current = state.channels[pubkey]
|
|
1271
1516
|
if (!channel || !current?.offlineRanges?.length) continue
|
|
1517
|
+
const recoverySeconds = this.offlineRecoverySecondsFor(channel)
|
|
1518
|
+
if (!recoverySeconds) continue
|
|
1519
|
+
const minStart = now - recoverySeconds
|
|
1272
1520
|
|
|
1273
1521
|
const remaining = []
|
|
1274
1522
|
for (const range of current.offlineRanges) {
|
|
@@ -1288,7 +1536,7 @@ export class PrivateMessenger {
|
|
|
1288
1536
|
until: range.end,
|
|
1289
1537
|
mode: channel.mode,
|
|
1290
1538
|
modeByPubkey: { [pubkey]: channel.mode },
|
|
1291
|
-
receivedChunkTtlMs: this.
|
|
1539
|
+
receivedChunkTtlMs: this.receivedChunkTtlMsFor(channel),
|
|
1292
1540
|
receivedChunkIndexedDB: this._indexedDB,
|
|
1293
1541
|
onEvent: (event, outer, meta) => this.queueIncoming(() => this.enqueueRumor(eventType(event), pubkey, { event, outer, meta, payload: parseEventContent(event) })),
|
|
1294
1542
|
onNymEvent: (event, outer, meta) => this.queueIncoming(() => this.enqueueRumor('nym', pubkey, { event, outer, meta, payload: parseEventContent(event) })),
|
|
@@ -1313,8 +1561,8 @@ export class PrivateMessenger {
|
|
|
1313
1561
|
|
|
1314
1562
|
async clearChannel (pubkey) {
|
|
1315
1563
|
return this.runQueueOperation(async () => {
|
|
1316
|
-
this.unwatch(pubkey)
|
|
1317
|
-
this._privateMessage.clearChannelState?.(pubkey)
|
|
1564
|
+
await this.unwatch(pubkey)
|
|
1565
|
+
await this._privateMessage.clearChannelState?.(pubkey)
|
|
1318
1566
|
this.channels.delete(pubkey)
|
|
1319
1567
|
this.removeChannelState(pubkey)
|
|
1320
1568
|
await this.flushStateWrites()
|
|
@@ -1346,24 +1594,39 @@ export class PrivateMessenger {
|
|
|
1346
1594
|
|
|
1347
1595
|
async pruneStoredSeeds (channelPubkey) {
|
|
1348
1596
|
if (!this.seedQueue) return
|
|
1349
|
-
const cutoff = nowSeconds() - this.offlineRecoverySeconds
|
|
1350
1597
|
const keyRange = globalThis.IDBKeyRange
|
|
1351
|
-
if (channelPubkey
|
|
1352
|
-
|
|
1598
|
+
if (!channelPubkey) {
|
|
1599
|
+
const pubkeys = new Set([...Object.keys(this.state.channels), ...this.channels.keys()])
|
|
1600
|
+
for (const pubkey of pubkeys) await this.pruneStoredSeeds(pubkey)
|
|
1601
|
+
await this.seedQueue.removeWhere(item => !pubkeys.has(item.channelPubkey))
|
|
1353
1602
|
return
|
|
1354
1603
|
}
|
|
1355
|
-
|
|
1356
|
-
|
|
1604
|
+
const recoverySeconds = this.offlineRecoverySecondsFor(channelPubkey)
|
|
1605
|
+
if (!recoverySeconds) {
|
|
1606
|
+
await this.seedQueue.removeBy('byChannel', channelPubkey)
|
|
1607
|
+
return
|
|
1608
|
+
}
|
|
1609
|
+
const cutoff = nowSeconds() - recoverySeconds
|
|
1610
|
+
if (cutoff <= 0) return
|
|
1611
|
+
if (keyRange?.bound) {
|
|
1612
|
+
await this.seedQueue.removeBy('byChannelTime', keyRange.bound([channelPubkey, 0], [channelPubkey, cutoff], false, true))
|
|
1357
1613
|
return
|
|
1358
1614
|
}
|
|
1359
1615
|
await this.seedQueue.removeWhere(item => {
|
|
1360
|
-
if (
|
|
1616
|
+
if (item.channelPubkey !== channelPubkey) return false
|
|
1361
1617
|
return (seedRecordTime(item) || item.receivedAt || 0) < cutoff
|
|
1362
1618
|
})
|
|
1363
1619
|
}
|
|
1364
1620
|
|
|
1365
1621
|
close () {
|
|
1366
|
-
this.
|
|
1622
|
+
if (this.closePromise) return this.closePromise
|
|
1623
|
+
const initSettledPromise = this.initSettledPromise
|
|
1624
|
+
let unwatchPromise
|
|
1625
|
+
try {
|
|
1626
|
+
unwatchPromise = Promise.resolve(this.unwatch())
|
|
1627
|
+
} catch (err) {
|
|
1628
|
+
unwatchPromise = Promise.reject(err)
|
|
1629
|
+
}
|
|
1367
1630
|
for (const pubkey of [...this.presenceTimers.keys()]) this.stopPresencePublisher(pubkey)
|
|
1368
1631
|
this.stopRelayListWatcher?.()
|
|
1369
1632
|
this.stopRelayListWatcher = null
|
|
@@ -1372,6 +1635,32 @@ export class PrivateMessenger {
|
|
|
1372
1635
|
this.stopOnline?.()
|
|
1373
1636
|
this.stopOffline = null
|
|
1374
1637
|
this.stopOnline = null
|
|
1638
|
+
this.stopStorageMaintenance()
|
|
1639
|
+
|
|
1640
|
+
this.closePromise = (async () => {
|
|
1641
|
+
let unwatchError
|
|
1642
|
+
try { await unwatchPromise } catch (err) { unwatchError = err }
|
|
1643
|
+
await initSettledPromise
|
|
1644
|
+
await this.queueOperationTail
|
|
1645
|
+
await this.stateWriteTail
|
|
1646
|
+
try { await this.storageTouchPromise } catch {}
|
|
1647
|
+
await this.storageMaintenancePromise
|
|
1648
|
+
await Promise.all([
|
|
1649
|
+
this.queue?.close?.(),
|
|
1650
|
+
this.seedQueue?.close?.(),
|
|
1651
|
+
this.stateStore?.close?.()
|
|
1652
|
+
])
|
|
1653
|
+
if (this.storageActive) {
|
|
1654
|
+
await releasePrivateMessengerStorage({
|
|
1655
|
+
userPubkey: this.userPubkey,
|
|
1656
|
+
leaseId: this.storageLeaseId,
|
|
1657
|
+
indexedDB: this._indexedDB
|
|
1658
|
+
})
|
|
1659
|
+
}
|
|
1660
|
+
this.storageActive = false
|
|
1661
|
+
if (unwatchError) throw unwatchError
|
|
1662
|
+
})()
|
|
1663
|
+
return this.closePromise
|
|
1375
1664
|
}
|
|
1376
1665
|
}
|
|
1377
1666
|
|
|
@@ -1452,11 +1741,9 @@ function nymCarrierSeedKey (record) {
|
|
|
1452
1741
|
|
|
1453
1742
|
function withoutQueueMetadata (item) {
|
|
1454
1743
|
if (!item) return null
|
|
1455
|
-
const {
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
...value
|
|
1459
|
-
} = item
|
|
1744
|
+
const value = { ...item }
|
|
1745
|
+
delete value[SEED_KEY]
|
|
1746
|
+
delete value[SEED_TIME]
|
|
1460
1747
|
return value
|
|
1461
1748
|
}
|
|
1462
1749
|
|