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 +58 -11
- package/idb-queue/index.js +60 -11
- package/package.json +1 -1
- package/private-channel/index.js +49 -31
- package/private-channel/services/received-chunks.js +453 -245
- package/private-message/index.js +62 -17
- package/private-messenger/index.js +383 -76
- package/private-messenger/recovery/index.js +3 -1
- package/private-messenger/services/channel-state.js +132 -0
- 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
|
// })
|
|
@@ -26,10 +26,11 @@
|
|
|
26
26
|
// await messenger.clearChannel(channelPubkey)
|
|
27
27
|
//
|
|
28
28
|
// Missed-message recovery:
|
|
29
|
-
// - Each watched channel stores lastSeenAt/lastWatchedAt in
|
|
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,9 +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'
|
|
46
|
+
import { createChannelStateStore } from './services/channel-state.js'
|
|
45
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'
|
|
46
55
|
import {
|
|
47
56
|
compactSeedNymCarriers,
|
|
48
57
|
compactSeedRouterRows,
|
|
@@ -71,6 +80,7 @@ export {
|
|
|
71
80
|
} from './recovery/index.js'
|
|
72
81
|
|
|
73
82
|
const DEFAULT_OFFLINE_RECOVERY_SECONDS = 7 * 24 * 60 * 60
|
|
83
|
+
const MAX_OFFLINE_RECOVERY_SECONDS = Math.floor(Number.MAX_SAFE_INTEGER / 1000)
|
|
74
84
|
const DEFAULT_OFFLINE_SKEW_SECONDS = 30
|
|
75
85
|
const DEFAULT_RELOAD_GAP_DELAY_MS = 500
|
|
76
86
|
const DEFAULT_SEEDER_PRESENCE_INTERVAL_MS = 10 * 60 * 1000
|
|
@@ -108,6 +118,13 @@ function nowSeconds () {
|
|
|
108
118
|
return Math.floor(Date.now() / 1000)
|
|
109
119
|
}
|
|
110
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
|
+
|
|
111
128
|
function uniq (values) {
|
|
112
129
|
return [...new Set((values || []).filter(Boolean))]
|
|
113
130
|
}
|
|
@@ -121,17 +138,27 @@ function isPlainObject (value) {
|
|
|
121
138
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
122
139
|
}
|
|
123
140
|
|
|
141
|
+
function parseJson (raw, fallback) {
|
|
142
|
+
try { return JSON.parse(raw || '') } catch { return fallback }
|
|
143
|
+
}
|
|
144
|
+
|
|
124
145
|
function storesRecoverySeeds (mode) {
|
|
125
146
|
return mode === 'seeder' || mode === 'watchtower'
|
|
126
147
|
}
|
|
127
148
|
|
|
128
|
-
function
|
|
129
|
-
|
|
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)}`
|
|
130
154
|
}
|
|
131
155
|
|
|
132
156
|
export class PrivateMessenger {
|
|
133
|
-
static
|
|
134
|
-
|
|
157
|
+
static maintainStorage ({
|
|
158
|
+
indexedDB = globalThis.indexedDB,
|
|
159
|
+
temporaryStorageArea = globalThis.sessionStorage
|
|
160
|
+
} = {}) {
|
|
161
|
+
return maintainPrivateMessengerStorage({ indexedDB, temporaryStorageArea })
|
|
135
162
|
}
|
|
136
163
|
|
|
137
164
|
constructor ({
|
|
@@ -159,9 +186,11 @@ export class PrivateMessenger {
|
|
|
159
186
|
_subscribeRelayListUpdates = subscribeRelayListUpdates,
|
|
160
187
|
_setTimeout = globalThis.setTimeout.bind(globalThis),
|
|
161
188
|
_setInterval = globalThis.setInterval.bind(globalThis),
|
|
162
|
-
_clearInterval = globalThis.clearInterval.bind(globalThis)
|
|
189
|
+
_clearInterval = globalThis.clearInterval.bind(globalThis),
|
|
190
|
+
_storageSetInterval = globalThis.setInterval.bind(globalThis),
|
|
191
|
+
_storageClearInterval = globalThis.clearInterval.bind(globalThis)
|
|
163
192
|
} = {}) {
|
|
164
|
-
this.offlineRecoverySeconds = offlineRecoverySeconds
|
|
193
|
+
this.offlineRecoverySeconds = normalizeOfflineRecoverySeconds(offlineRecoverySeconds)
|
|
165
194
|
this.staleChannelSeconds = staleChannelSeconds
|
|
166
195
|
this.offlineSkewSeconds = offlineSkewSeconds
|
|
167
196
|
this.reloadGapDelayMs = reloadGapDelayMs
|
|
@@ -186,6 +215,8 @@ export class PrivateMessenger {
|
|
|
186
215
|
this._setTimeout = _setTimeout
|
|
187
216
|
this._setInterval = _setInterval
|
|
188
217
|
this._clearInterval = _clearInterval
|
|
218
|
+
this._storageSetInterval = _storageSetInterval
|
|
219
|
+
this._storageClearInterval = _storageClearInterval
|
|
189
220
|
|
|
190
221
|
this.userSigner = null
|
|
191
222
|
this.contentKeySigner = null
|
|
@@ -195,6 +226,9 @@ export class PrivateMessenger {
|
|
|
195
226
|
this.prefix = ''
|
|
196
227
|
this.queue = null
|
|
197
228
|
this.seedQueue = null
|
|
229
|
+
this.stateStore = null
|
|
230
|
+
this.state = { channels: {} }
|
|
231
|
+
this.stateWriteTail = Promise.resolve()
|
|
198
232
|
this.channels = new Map()
|
|
199
233
|
this.stopByChannel = new Map()
|
|
200
234
|
this.presenceTimers = new Map()
|
|
@@ -204,37 +238,170 @@ export class PrivateMessenger {
|
|
|
204
238
|
this.stopOnline = null
|
|
205
239
|
this.stopOffline = null
|
|
206
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
|
|
207
251
|
}
|
|
208
252
|
|
|
209
253
|
async init ({ userSigner, contentKeySigner, nymSigner, channels = [], relays = [], mode = 'leecher' }) {
|
|
210
254
|
if (!userSigner?.getPublicKey) throw new Error('USER_SIGNER_REQUIRED')
|
|
211
|
-
|
|
212
|
-
this.
|
|
213
|
-
this.
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
this.
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
224
375
|
})
|
|
225
|
-
this.
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
|
231
397
|
})
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
return this
|
|
398
|
+
this.storageTouchPromise = touch
|
|
399
|
+
return touch
|
|
235
400
|
}
|
|
236
401
|
|
|
237
402
|
runQueueOperation (operation) {
|
|
403
|
+
if (this.closePromise) return Promise.reject(new Error('PRIVATE_MESSENGER_CLOSED'))
|
|
404
|
+
this.touchStorageActivity().catch(err => this.onError?.(err))
|
|
238
405
|
const run = this.queueOperationTail.then(operation)
|
|
239
406
|
this.queueOperationTail = run.catch(err => {
|
|
240
407
|
try { this.onError?.(err) } catch {}
|
|
@@ -269,6 +436,7 @@ export class PrivateMessenger {
|
|
|
269
436
|
}
|
|
270
437
|
|
|
271
438
|
async update ({ userSigner = this.userSigner, contentKeySigner = this.contentKeySigner, nymSigner = this.nymSigner, channels = [...this.channels.values()], relays = [], mode = 'leecher' } = {}) {
|
|
439
|
+
this.assertOpen()
|
|
272
440
|
if (userSigner) {
|
|
273
441
|
const userPubkey = await userSigner.getPublicKey?.()
|
|
274
442
|
if (!userPubkey) throw new Error('USER_SIGNER_REQUIRED')
|
|
@@ -279,17 +447,19 @@ export class PrivateMessenger {
|
|
|
279
447
|
this.nymSigner = nymSigner || null
|
|
280
448
|
this.contentKeyPubkey = await this.contentKeySigner?.getPublicKey?.() || ''
|
|
281
449
|
const nextChannels = await this.normalizeChannels(channels, { relays, mode })
|
|
450
|
+
this.assertOpen()
|
|
282
451
|
const nextPubkeys = new Set(nextChannels.map(channel => channel.pubkey))
|
|
283
452
|
|
|
284
453
|
for (const pubkey of [...this.channels.keys()]) {
|
|
285
454
|
if (!nextPubkeys.has(pubkey)) {
|
|
286
|
-
this.unwatch(pubkey)
|
|
455
|
+
await this.unwatch(pubkey)
|
|
287
456
|
this.channels.delete(pubkey)
|
|
288
457
|
}
|
|
289
458
|
}
|
|
290
459
|
for (const channel of nextChannels) this.channels.set(channel.pubkey, channel)
|
|
291
460
|
|
|
292
461
|
await this.cleanupStaleChannels()
|
|
462
|
+
await this.applyRecoveryPolicies(nextChannels)
|
|
293
463
|
await this.watch()
|
|
294
464
|
await this.reconcilePresencePublishers()
|
|
295
465
|
return this
|
|
@@ -314,6 +484,9 @@ export class PrivateMessenger {
|
|
|
314
484
|
const autoDeletionCapability = channel.autoDeletionCapability === undefined
|
|
315
485
|
? undefined
|
|
316
486
|
: normalizeAutoDeletionCapability(channel.autoDeletionCapability)
|
|
487
|
+
const offlineRecoverySeconds = normalizeOfflineRecoverySeconds(
|
|
488
|
+
channel.offlineRecoverySeconds ?? this.offlineRecoverySeconds
|
|
489
|
+
)
|
|
317
490
|
out.push({
|
|
318
491
|
pubkey,
|
|
319
492
|
signer,
|
|
@@ -325,6 +498,7 @@ export class PrivateMessenger {
|
|
|
325
498
|
usesNip65WatchRelays: !hasChannelRelays && !hasDefaultRelays,
|
|
326
499
|
mode,
|
|
327
500
|
seeders: uniq(channel.seeders),
|
|
501
|
+
offlineRecoverySeconds,
|
|
328
502
|
autoDeletionCapability
|
|
329
503
|
})
|
|
330
504
|
}
|
|
@@ -346,6 +520,7 @@ export class PrivateMessenger {
|
|
|
346
520
|
}
|
|
347
521
|
|
|
348
522
|
async recoveryMirrorRelays (channelPubkey) {
|
|
523
|
+
if (!this.offlineRecoverySecondsFor(channelPubkey)) return []
|
|
349
524
|
const seeders = this.recoverySeeders(channelPubkey)
|
|
350
525
|
if (!seeders.length) return []
|
|
351
526
|
try {
|
|
@@ -372,18 +547,26 @@ export class PrivateMessenger {
|
|
|
372
547
|
return { relayToReceivers: derived, recoveryRelays }
|
|
373
548
|
}
|
|
374
549
|
|
|
375
|
-
stateKey () {
|
|
376
|
-
return `${this.prefix}:state`
|
|
377
|
-
}
|
|
378
|
-
|
|
379
550
|
readState () {
|
|
380
|
-
|
|
381
|
-
if (!isPlainObject(state.channels)) state.channels = {}
|
|
382
|
-
return state
|
|
551
|
+
return structuredClone(this.state)
|
|
383
552
|
}
|
|
384
553
|
|
|
385
554
|
writeState (state) {
|
|
386
|
-
|
|
555
|
+
this.touchStorageActivity().catch(err => this.onError?.(err))
|
|
556
|
+
const next = {
|
|
557
|
+
channels: isPlainObject(state?.channels) ? structuredClone(state.channels) : {}
|
|
558
|
+
}
|
|
559
|
+
this.state = next
|
|
560
|
+
const snapshot = structuredClone(next.channels)
|
|
561
|
+
const write = this.stateWriteTail.then(() => this.stateStore.replace(snapshot))
|
|
562
|
+
this.stateWriteTail = write.catch(err => {
|
|
563
|
+
try { this.onError?.(err) } catch {}
|
|
564
|
+
})
|
|
565
|
+
return write
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
async flushStateWrites () {
|
|
569
|
+
await this.stateWriteTail
|
|
387
570
|
}
|
|
388
571
|
|
|
389
572
|
updateChannelState (pubkey, patch) {
|
|
@@ -416,6 +599,7 @@ export class PrivateMessenger {
|
|
|
416
599
|
}
|
|
417
600
|
|
|
418
601
|
recoverySeeders (pubkey) {
|
|
602
|
+
if (!this.offlineRecoverySecondsFor(pubkey)) return []
|
|
419
603
|
const channel = this.channels.get(pubkey)
|
|
420
604
|
const configuredSeeders = channel?.seeders || []
|
|
421
605
|
if (configuredSeeders.length) return configuredSeeders.filter(seeder => seeder !== this.userPubkey)
|
|
@@ -430,6 +614,7 @@ export class PrivateMessenger {
|
|
|
430
614
|
}
|
|
431
615
|
|
|
432
616
|
markSeederActive (channelPubkey, seederPubkey, { announced = false, at = nowSeconds() } = {}) {
|
|
617
|
+
if (!this.offlineRecoverySecondsFor(channelPubkey)) return false
|
|
433
618
|
const state = this.readState()
|
|
434
619
|
const current = state.channels[channelPubkey] || {}
|
|
435
620
|
const activity = current.seederActivity || {}
|
|
@@ -443,6 +628,7 @@ export class PrivateMessenger {
|
|
|
443
628
|
current.seederActivity = activity
|
|
444
629
|
state.channels[channelPubkey] = current
|
|
445
630
|
this.writeState(state)
|
|
631
|
+
return true
|
|
446
632
|
}
|
|
447
633
|
|
|
448
634
|
trackSeederActivity (channelPubkey, message) {
|
|
@@ -528,8 +714,10 @@ export class PrivateMessenger {
|
|
|
528
714
|
}
|
|
529
715
|
|
|
530
716
|
addOfflineRange (pubkey, start, end) {
|
|
717
|
+
const recoverySeconds = this.offlineRecoverySecondsFor(pubkey)
|
|
718
|
+
if (!recoverySeconds) return
|
|
531
719
|
const now = nowSeconds()
|
|
532
|
-
const minStart = now -
|
|
720
|
+
const minStart = now - recoverySeconds
|
|
533
721
|
const normalized = {
|
|
534
722
|
start: Math.max(0, Math.floor(start)),
|
|
535
723
|
end: Math.floor(end)
|
|
@@ -551,10 +739,17 @@ export class PrivateMessenger {
|
|
|
551
739
|
closeOpenOfflineRanges () {
|
|
552
740
|
const state = this.readState()
|
|
553
741
|
const end = nowSeconds()
|
|
554
|
-
const minStart = end - this.offlineRecoverySeconds
|
|
555
742
|
for (const pubkey of Object.keys(state.channels)) {
|
|
556
743
|
const current = state.channels[pubkey]
|
|
557
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
|
|
558
753
|
const start = Math.max(minStart, Math.max(0, current.openOfflineStart))
|
|
559
754
|
if (end > start) {
|
|
560
755
|
current.offlineRanges = mergeRanges((current.offlineRanges || []).concat([{ start, end }]))
|
|
@@ -566,11 +761,13 @@ export class PrivateMessenger {
|
|
|
566
761
|
}
|
|
567
762
|
|
|
568
763
|
async watch (channels = [...this.channels.keys()], { scheduleReloadGap = true } = {}) {
|
|
764
|
+
this.assertOpen()
|
|
569
765
|
const channelPubkeys = uniq(channels)
|
|
570
766
|
for (const pubkey of channelPubkeys) {
|
|
571
767
|
const channel = this.channels.get(pubkey)
|
|
572
768
|
if (!channel) throw new Error('UNKNOWN_CHANNEL')
|
|
573
769
|
const watchRelays = await this.resolveWatchRelays(channel)
|
|
770
|
+
this.assertOpen()
|
|
574
771
|
const stop = await this._privateMessage.watch({
|
|
575
772
|
channels: [pubkey],
|
|
576
773
|
relays: watchRelays,
|
|
@@ -588,15 +785,21 @@ export class PrivateMessenger {
|
|
|
588
785
|
onMessage: message => this.queueIncoming(() => this.handleMessage(pubkey, message)),
|
|
589
786
|
onSeed: seed => this.queueIncoming(() => this.enqueueSeed(pubkey, seed)),
|
|
590
787
|
onContentKeyUsage: usage => this.handleContentKeyUsage(pubkey, usage),
|
|
591
|
-
receivedChunkTtlMs: this.
|
|
788
|
+
receivedChunkTtlMs: this.receivedChunkTtlMsFor(channel),
|
|
789
|
+
receivedChunkIndexedDB: this._indexedDB,
|
|
592
790
|
onError: err => this.onError?.(err)
|
|
593
791
|
})
|
|
792
|
+
if (this.closePromise) {
|
|
793
|
+
await stop?.()
|
|
794
|
+
this.assertOpen()
|
|
795
|
+
}
|
|
594
796
|
this.stopByChannel.set(pubkey, stop)
|
|
595
797
|
this.updateChannelState(pubkey, {
|
|
596
798
|
lastWatchedAt: nowSeconds(),
|
|
597
799
|
mode: channel.mode,
|
|
598
800
|
relays: watchRelays,
|
|
599
|
-
seeders: channel.seeders
|
|
801
|
+
seeders: channel.seeders,
|
|
802
|
+
offlineRecoverySeconds: channel.offlineRecoverySeconds
|
|
600
803
|
})
|
|
601
804
|
this.debug('watch', {
|
|
602
805
|
channelPubkey: pubkey,
|
|
@@ -614,12 +817,15 @@ export class PrivateMessenger {
|
|
|
614
817
|
|
|
615
818
|
unwatch (channels) {
|
|
616
819
|
const channelPubkeys = channels ? uniq(Array.isArray(channels) ? channels : [channels]) : [...this.stopByChannel.keys()]
|
|
820
|
+
const closing = []
|
|
617
821
|
for (const pubkey of channelPubkeys) {
|
|
618
|
-
this.stopByChannel.get(pubkey)?.()
|
|
822
|
+
const close = this.stopByChannel.get(pubkey)?.()
|
|
823
|
+
if (close && typeof close.then === 'function') closing.push(close)
|
|
619
824
|
this.stopByChannel.delete(pubkey)
|
|
620
825
|
this.stopPresencePublisher(pubkey)
|
|
621
826
|
}
|
|
622
827
|
this.ensureRelayListWatcher()
|
|
828
|
+
return Promise.allSettled(closing)
|
|
623
829
|
}
|
|
624
830
|
|
|
625
831
|
nip65WatchChannelPubkeys () {
|
|
@@ -745,6 +951,7 @@ export class PrivateMessenger {
|
|
|
745
951
|
}
|
|
746
952
|
|
|
747
953
|
async enqueueSeed (channelPubkey, seed) {
|
|
954
|
+
if (!this.offlineRecoverySecondsFor(channelPubkey)) return
|
|
748
955
|
const receivedAt = nowSeconds()
|
|
749
956
|
if (seed.recordType === NYM_CARRIER_SEED_RECORD_TYPE || seed.carriers?.length) {
|
|
750
957
|
const carriers = compactSeedNymCarriers(seed.carriers)
|
|
@@ -801,6 +1008,7 @@ export class PrivateMessenger {
|
|
|
801
1008
|
|
|
802
1009
|
async nextMessage () {
|
|
803
1010
|
// seedQueue is retained replay material for recovery replies, not an app-message stream.
|
|
1011
|
+
this.touchStorageActivity().catch(err => this.onError?.(err))
|
|
804
1012
|
await this.queueOperationTail
|
|
805
1013
|
return withoutQueueMetadata(await this.queue.shift())
|
|
806
1014
|
}
|
|
@@ -816,7 +1024,7 @@ export class PrivateMessenger {
|
|
|
816
1024
|
privateChannelReaderPubkey: channel.readerPubkey,
|
|
817
1025
|
receiverPubkey,
|
|
818
1026
|
...routing,
|
|
819
|
-
expirationSeconds: this.
|
|
1027
|
+
expirationSeconds: this.eventExpirationSecondsFor(channel),
|
|
820
1028
|
temporaryStorageArea: this.temporaryStorageArea,
|
|
821
1029
|
deletionPubkey,
|
|
822
1030
|
autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
|
|
@@ -842,7 +1050,7 @@ export class PrivateMessenger {
|
|
|
842
1050
|
question,
|
|
843
1051
|
receiverPubkey,
|
|
844
1052
|
...routing,
|
|
845
|
-
expirationSeconds: this.
|
|
1053
|
+
expirationSeconds: this.eventExpirationSecondsFor(channel),
|
|
846
1054
|
temporaryStorageArea: this.temporaryStorageArea,
|
|
847
1055
|
deletionPubkey,
|
|
848
1056
|
autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
|
|
@@ -866,7 +1074,7 @@ export class PrivateMessenger {
|
|
|
866
1074
|
privateChannelReaderPubkey: channel.readerPubkey,
|
|
867
1075
|
receiverPubkey,
|
|
868
1076
|
...routing,
|
|
869
|
-
expirationSeconds: this.
|
|
1077
|
+
expirationSeconds: this.eventExpirationSecondsFor(channel),
|
|
870
1078
|
temporaryStorageArea: this.temporaryStorageArea,
|
|
871
1079
|
deletionPubkey,
|
|
872
1080
|
autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
|
|
@@ -890,7 +1098,7 @@ export class PrivateMessenger {
|
|
|
890
1098
|
privateChannelReaderPubkey: channel.readerPubkey,
|
|
891
1099
|
receiverPubkeys,
|
|
892
1100
|
...routing,
|
|
893
|
-
expirationSeconds: this.
|
|
1101
|
+
expirationSeconds: this.eventExpirationSecondsFor(channel),
|
|
894
1102
|
temporaryStorageArea: this.temporaryStorageArea,
|
|
895
1103
|
deletionPubkey,
|
|
896
1104
|
autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
|
|
@@ -914,7 +1122,7 @@ export class PrivateMessenger {
|
|
|
914
1122
|
privateChannelReaderPubkey: channel.readerPubkey,
|
|
915
1123
|
receiverPubkeys,
|
|
916
1124
|
...routing,
|
|
917
|
-
expirationSeconds: this.
|
|
1125
|
+
expirationSeconds: this.eventExpirationSecondsFor(channel),
|
|
918
1126
|
temporaryStorageArea: this.temporaryStorageArea,
|
|
919
1127
|
deletionPubkey,
|
|
920
1128
|
autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
|
|
@@ -934,7 +1142,7 @@ export class PrivateMessenger {
|
|
|
934
1142
|
privateChannelReaderPubkey: channel.readerPubkey,
|
|
935
1143
|
receiverPubkeys,
|
|
936
1144
|
...routing,
|
|
937
|
-
expirationSeconds: this.
|
|
1145
|
+
expirationSeconds: this.eventExpirationSecondsFor(channel),
|
|
938
1146
|
temporaryStorageArea: this.temporaryStorageArea,
|
|
939
1147
|
deletionPubkey,
|
|
940
1148
|
autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
|
|
@@ -953,7 +1161,7 @@ export class PrivateMessenger {
|
|
|
953
1161
|
privateChannelSigner: channel.signer,
|
|
954
1162
|
privateChannelReaderPubkey: channel.readerPubkey,
|
|
955
1163
|
...routing,
|
|
956
|
-
expirationSeconds: this.
|
|
1164
|
+
expirationSeconds: this.eventExpirationSecondsFor(channel),
|
|
957
1165
|
deletionPubkey,
|
|
958
1166
|
autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
|
|
959
1167
|
rumor
|
|
@@ -970,7 +1178,7 @@ export class PrivateMessenger {
|
|
|
970
1178
|
privateChannelSigner: channel.signer,
|
|
971
1179
|
privateChannelReaderPubkey: channel.readerPubkey,
|
|
972
1180
|
...routing,
|
|
973
|
-
expirationSeconds: this.
|
|
1181
|
+
expirationSeconds: this.eventExpirationSecondsFor(channel),
|
|
974
1182
|
deletionPubkey,
|
|
975
1183
|
autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
|
|
976
1184
|
event
|
|
@@ -979,6 +1187,7 @@ export class PrivateMessenger {
|
|
|
979
1187
|
|
|
980
1188
|
async publishSeederPresence (channelPubkey = this.defaultChannelPubkey()) {
|
|
981
1189
|
const channel = this.requireWritableChannel(channelPubkey)
|
|
1190
|
+
if (!this.offlineRecoverySecondsFor(channel)) return null
|
|
982
1191
|
const receiverPubkeys = uniq([...this.knownSeeders(channelPubkey), this.userPubkey])
|
|
983
1192
|
const routing = await this.resolveSendRouting({ channel, receiverPubkeys })
|
|
984
1193
|
this.debugSend('yell', channelPubkey, { code: SEEDER_PRESENCE_CODE, receiverPubkeys })
|
|
@@ -989,7 +1198,7 @@ export class PrivateMessenger {
|
|
|
989
1198
|
privateChannelReaderPubkey: channel.readerPubkey,
|
|
990
1199
|
receiverPubkeys,
|
|
991
1200
|
...routing,
|
|
992
|
-
expirationSeconds: this.
|
|
1201
|
+
expirationSeconds: this.eventExpirationSecondsFor(channel),
|
|
993
1202
|
temporaryStorageArea: this.temporaryStorageArea,
|
|
994
1203
|
autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
|
|
995
1204
|
code: SEEDER_PRESENCE_CODE,
|
|
@@ -999,6 +1208,7 @@ export class PrivateMessenger {
|
|
|
999
1208
|
}
|
|
1000
1209
|
|
|
1001
1210
|
async startPresencePublisher (channelPubkey) {
|
|
1211
|
+
if (!this.offlineRecoverySecondsFor(channelPubkey)) return
|
|
1002
1212
|
if (this.presenceTimers.has(channelPubkey)) return
|
|
1003
1213
|
try {
|
|
1004
1214
|
await this.publishSeederPresence(channelPubkey)
|
|
@@ -1023,10 +1233,10 @@ export class PrivateMessenger {
|
|
|
1023
1233
|
async reconcilePresencePublishers () {
|
|
1024
1234
|
const starts = []
|
|
1025
1235
|
for (const pubkey of [...this.presenceTimers.keys()]) {
|
|
1026
|
-
if (!storesRecoverySeeds(this.channels.get(pubkey)?.mode)) this.stopPresencePublisher(pubkey)
|
|
1236
|
+
if (!storesRecoverySeeds(this.channels.get(pubkey)?.mode) || !this.offlineRecoverySecondsFor(pubkey)) this.stopPresencePublisher(pubkey)
|
|
1027
1237
|
}
|
|
1028
1238
|
for (const [pubkey, channel] of this.channels) {
|
|
1029
|
-
if (storesRecoverySeeds(channel.mode)) starts.push(this.startPresencePublisher(pubkey))
|
|
1239
|
+
if (storesRecoverySeeds(channel.mode) && this.offlineRecoverySecondsFor(channel)) starts.push(this.startPresencePublisher(pubkey))
|
|
1030
1240
|
else this.stopPresencePublisher(pubkey)
|
|
1031
1241
|
}
|
|
1032
1242
|
await Promise.all(starts)
|
|
@@ -1060,6 +1270,53 @@ export class PrivateMessenger {
|
|
|
1060
1270
|
return channel.autoDeletionCapability ?? this.autoDeletionCapability
|
|
1061
1271
|
}
|
|
1062
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
|
+
|
|
1063
1320
|
requireNymSigner (channel, override) {
|
|
1064
1321
|
const signer = override || channel?.nymSigner || this.nymSigner
|
|
1065
1322
|
if (!signer?.getPublicKey) throw new Error('NYM_SIGNER_REQUIRED')
|
|
@@ -1071,6 +1328,7 @@ export class PrivateMessenger {
|
|
|
1071
1328
|
}
|
|
1072
1329
|
|
|
1073
1330
|
scheduleReloadGap (pubkey) {
|
|
1331
|
+
if (!this.offlineRecoverySecondsFor(pubkey)) return
|
|
1074
1332
|
const current = this.readState().channels[pubkey]
|
|
1075
1333
|
const start = current?.openOfflineStart || current?.lastSeenAt
|
|
1076
1334
|
if (!start) return
|
|
@@ -1100,6 +1358,7 @@ export class PrivateMessenger {
|
|
|
1100
1358
|
const state = this.readState()
|
|
1101
1359
|
const start = Math.max(0, nowSeconds() - this.offlineSkewSeconds)
|
|
1102
1360
|
for (const pubkey of this.channels.keys()) {
|
|
1361
|
+
if (!this.offlineRecoverySecondsFor(pubkey)) continue
|
|
1103
1362
|
const current = state.channels[pubkey] || {}
|
|
1104
1363
|
current.openOfflineStart ||= start
|
|
1105
1364
|
state.channels[pubkey] = current
|
|
@@ -1122,6 +1381,7 @@ export class PrivateMessenger {
|
|
|
1122
1381
|
}
|
|
1123
1382
|
|
|
1124
1383
|
async askSeedersForMissingRange (channelPubkey, since, until) {
|
|
1384
|
+
if (!this.offlineRecoverySecondsFor(channelPubkey)) return []
|
|
1125
1385
|
if (!this.channels.get(channelPubkey)?.signer) return []
|
|
1126
1386
|
const seeders = this.recoverySeeders(channelPubkey)
|
|
1127
1387
|
if (!seeders.length || until < since) return []
|
|
@@ -1158,11 +1418,14 @@ export class PrivateMessenger {
|
|
|
1158
1418
|
question: message.event,
|
|
1159
1419
|
receiverPubkey: message.event?.pubkey,
|
|
1160
1420
|
since,
|
|
1161
|
-
until
|
|
1421
|
+
until,
|
|
1422
|
+
sendEmptyReply: !this.offlineRecoverySecondsFor(channelPubkey)
|
|
1162
1423
|
})
|
|
1163
1424
|
|
|
1164
|
-
|
|
1165
|
-
await
|
|
1425
|
+
if (this.offlineRecoverySecondsFor(channelPubkey)) {
|
|
1426
|
+
for await (const seed of this.seedQueue.storedItemsBy('byChannel', channelPubkey)) {
|
|
1427
|
+
await packer.update(seed)
|
|
1428
|
+
}
|
|
1166
1429
|
}
|
|
1167
1430
|
await packer.finalize()
|
|
1168
1431
|
}
|
|
@@ -1246,12 +1509,14 @@ export class PrivateMessenger {
|
|
|
1246
1509
|
async recoverOfflineRanges (channels = [...this.stopByChannel.keys()]) {
|
|
1247
1510
|
const state = this.readState()
|
|
1248
1511
|
const now = nowSeconds()
|
|
1249
|
-
const minStart = now - this.offlineRecoverySeconds
|
|
1250
1512
|
|
|
1251
1513
|
for (const pubkey of uniq(channels)) {
|
|
1252
1514
|
const channel = this.channels.get(pubkey)
|
|
1253
1515
|
const current = state.channels[pubkey]
|
|
1254
1516
|
if (!channel || !current?.offlineRanges?.length) continue
|
|
1517
|
+
const recoverySeconds = this.offlineRecoverySecondsFor(channel)
|
|
1518
|
+
if (!recoverySeconds) continue
|
|
1519
|
+
const minStart = now - recoverySeconds
|
|
1255
1520
|
|
|
1256
1521
|
const remaining = []
|
|
1257
1522
|
for (const range of current.offlineRanges) {
|
|
@@ -1271,7 +1536,8 @@ export class PrivateMessenger {
|
|
|
1271
1536
|
until: range.end,
|
|
1272
1537
|
mode: channel.mode,
|
|
1273
1538
|
modeByPubkey: { [pubkey]: channel.mode },
|
|
1274
|
-
receivedChunkTtlMs: this.
|
|
1539
|
+
receivedChunkTtlMs: this.receivedChunkTtlMsFor(channel),
|
|
1540
|
+
receivedChunkIndexedDB: this._indexedDB,
|
|
1275
1541
|
onEvent: (event, outer, meta) => this.queueIncoming(() => this.enqueueRumor(eventType(event), pubkey, { event, outer, meta, payload: parseEventContent(event) })),
|
|
1276
1542
|
onNymEvent: (event, outer, meta) => this.queueIncoming(() => this.enqueueRumor('nym', pubkey, { event, outer, meta, payload: parseEventContent(event) })),
|
|
1277
1543
|
onSeedEvent: seed => this.queueIncoming(() => this.enqueueSeed(pubkey, seed)),
|
|
@@ -1295,10 +1561,11 @@ export class PrivateMessenger {
|
|
|
1295
1561
|
|
|
1296
1562
|
async clearChannel (pubkey) {
|
|
1297
1563
|
return this.runQueueOperation(async () => {
|
|
1298
|
-
this.unwatch(pubkey)
|
|
1299
|
-
this._privateMessage.clearChannelState?.(pubkey)
|
|
1564
|
+
await this.unwatch(pubkey)
|
|
1565
|
+
await this._privateMessage.clearChannelState?.(pubkey)
|
|
1300
1566
|
this.channels.delete(pubkey)
|
|
1301
1567
|
this.removeChannelState(pubkey)
|
|
1568
|
+
await this.flushStateWrites()
|
|
1302
1569
|
await this.queue.removeBy('byChannel', pubkey)
|
|
1303
1570
|
await this.seedQueue.removeBy('byChannel', pubkey)
|
|
1304
1571
|
this.ensureRelayListWatcher()
|
|
@@ -1321,29 +1588,45 @@ export class PrivateMessenger {
|
|
|
1321
1588
|
await this.seedQueue?.removeBy('byChannel', pubkey)
|
|
1322
1589
|
}
|
|
1323
1590
|
this.writeState(state)
|
|
1591
|
+
await this.flushStateWrites()
|
|
1324
1592
|
})
|
|
1325
1593
|
}
|
|
1326
1594
|
|
|
1327
1595
|
async pruneStoredSeeds (channelPubkey) {
|
|
1328
1596
|
if (!this.seedQueue) return
|
|
1329
|
-
const cutoff = nowSeconds() - this.offlineRecoverySeconds
|
|
1330
1597
|
const keyRange = globalThis.IDBKeyRange
|
|
1331
|
-
if (channelPubkey
|
|
1332
|
-
|
|
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))
|
|
1602
|
+
return
|
|
1603
|
+
}
|
|
1604
|
+
const recoverySeconds = this.offlineRecoverySecondsFor(channelPubkey)
|
|
1605
|
+
if (!recoverySeconds) {
|
|
1606
|
+
await this.seedQueue.removeBy('byChannel', channelPubkey)
|
|
1333
1607
|
return
|
|
1334
1608
|
}
|
|
1335
|
-
|
|
1336
|
-
|
|
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))
|
|
1337
1613
|
return
|
|
1338
1614
|
}
|
|
1339
1615
|
await this.seedQueue.removeWhere(item => {
|
|
1340
|
-
if (
|
|
1616
|
+
if (item.channelPubkey !== channelPubkey) return false
|
|
1341
1617
|
return (seedRecordTime(item) || item.receivedAt || 0) < cutoff
|
|
1342
1618
|
})
|
|
1343
1619
|
}
|
|
1344
1620
|
|
|
1345
1621
|
close () {
|
|
1346
|
-
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
|
+
}
|
|
1347
1630
|
for (const pubkey of [...this.presenceTimers.keys()]) this.stopPresencePublisher(pubkey)
|
|
1348
1631
|
this.stopRelayListWatcher?.()
|
|
1349
1632
|
this.stopRelayListWatcher = null
|
|
@@ -1352,6 +1635,32 @@ export class PrivateMessenger {
|
|
|
1352
1635
|
this.stopOnline?.()
|
|
1353
1636
|
this.stopOffline = null
|
|
1354
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
|
|
1355
1664
|
}
|
|
1356
1665
|
}
|
|
1357
1666
|
|
|
@@ -1432,11 +1741,9 @@ function nymCarrierSeedKey (record) {
|
|
|
1432
1741
|
|
|
1433
1742
|
function withoutQueueMetadata (item) {
|
|
1434
1743
|
if (!item) return null
|
|
1435
|
-
const {
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
...value
|
|
1439
|
-
} = item
|
|
1744
|
+
const value = { ...item }
|
|
1745
|
+
delete value[SEED_KEY]
|
|
1746
|
+
delete value[SEED_TIME]
|
|
1440
1747
|
return value
|
|
1441
1748
|
}
|
|
1442
1749
|
|