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
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
// userSigner,
|
|
4
4
|
// contentKeySigner, // optional when userSigner handles content keys internally
|
|
5
5
|
// nymSigner: optionalDefaultNymSigner,
|
|
6
|
-
//
|
|
6
|
+
// staleChannelSeconds: optionalStaleChannelRetention,
|
|
7
|
+
// identityStorageRetentionSeconds: optionalIdentityStorageRetention,
|
|
8
|
+
// channels: [{ signer: privateChannelSigner, relays, mode: 'leecher', seeders: optionalSeederPubkeys, offlineRecoverySeconds: optionalChannelOverride }],
|
|
7
9
|
// onContentKeyChange: event => reviewContentKeyUse(event),
|
|
8
10
|
// onError: err => reportPrivateMessengerError(err)
|
|
9
11
|
// })
|
|
@@ -29,7 +31,8 @@
|
|
|
29
31
|
// - Each watched channel stores lastSeenAt/lastWatchedAt in IndexedDB.
|
|
30
32
|
// - Re-watching after reload fetches the gap from lastSeenAt to now.
|
|
31
33
|
// - Browser offline/online events add explicit offline ranges with a small skew.
|
|
32
|
-
// -
|
|
34
|
+
// - Recovery defaults to 7 days and can be overridden per channel; zero disables durable recovery.
|
|
35
|
+
// - Channel state not actively leased or watched within the configured stale window is pruned.
|
|
33
36
|
// - Seeders announce presence every 10min and are used for the relay-uncovered left edge of a missed range.
|
|
34
37
|
// - Configured seeders are all asked; auto-discovered seeders are capped to the 8 most recently active.
|
|
35
38
|
// - Seeder/watchtower channels store reconstructed router events in a separate IndexedDB queue and auto-reply to recovery asks.
|
|
@@ -40,10 +43,19 @@ import * as privateMessage from '../private-message/index.js'
|
|
|
40
43
|
import { bytesToBase64 } from '../base64/index.js'
|
|
41
44
|
import { getRelaysByPubkey, pickRelaysForPubkeys, subscribeRelayListUpdates } from '../relay/index.js'
|
|
42
45
|
import * as privateChannel from '../private-channel/index.js'
|
|
43
|
-
import {
|
|
46
|
+
import { DEFAULT_RECEIVED_CHUNK_TTL_MS } from '../private-channel/services/received-chunks.js'
|
|
44
47
|
import { createQueue } from '../idb-queue/index.js'
|
|
45
48
|
import { createChannelStateStore } from './services/channel-state.js'
|
|
46
49
|
import { DEFAULT_STALE_CHANNEL_SECONDS } from './constants/index.js'
|
|
50
|
+
import {
|
|
51
|
+
activatePrivateMessengerStorage,
|
|
52
|
+
DEFAULT_IDENTITY_STORAGE_RETENTION_SECONDS,
|
|
53
|
+
maintainPrivateMessengerStorage,
|
|
54
|
+
PRIVATE_MESSENGER_STORAGE_HEARTBEAT_MS,
|
|
55
|
+
PRIVATE_MESSENGER_STORAGE_MAINTENANCE_MS,
|
|
56
|
+
readPrivateMessengerStorage,
|
|
57
|
+
releasePrivateMessengerStorage
|
|
58
|
+
} from './services/storage-maintenance.js'
|
|
47
59
|
import {
|
|
48
60
|
compactSeedNymCarriers,
|
|
49
61
|
compactSeedRouterRows,
|
|
@@ -58,6 +70,7 @@ import {
|
|
|
58
70
|
} from './recovery/index.js'
|
|
59
71
|
|
|
60
72
|
export { DEFAULT_STALE_CHANNEL_SECONDS } from './constants/index.js'
|
|
73
|
+
export { DEFAULT_IDENTITY_STORAGE_RETENTION_SECONDS } from './services/storage-maintenance.js'
|
|
61
74
|
export {
|
|
62
75
|
compactSeedNymCarriers,
|
|
63
76
|
compactSeedRouterRows,
|
|
@@ -72,6 +85,8 @@ export {
|
|
|
72
85
|
} from './recovery/index.js'
|
|
73
86
|
|
|
74
87
|
const DEFAULT_OFFLINE_RECOVERY_SECONDS = 7 * 24 * 60 * 60
|
|
88
|
+
const MAX_OFFLINE_RECOVERY_SECONDS = Math.floor(Number.MAX_SAFE_INTEGER / 1000)
|
|
89
|
+
const STORAGE_POLICY_BROADCAST_CHANNEL = 'libp2r2p:private-messenger:storage-policy'
|
|
75
90
|
const DEFAULT_OFFLINE_SKEW_SECONDS = 30
|
|
76
91
|
const DEFAULT_RELOAD_GAP_DELAY_MS = 500
|
|
77
92
|
const DEFAULT_SEEDER_PRESENCE_INTERVAL_MS = 10 * 60 * 1000
|
|
@@ -109,6 +124,27 @@ function nowSeconds () {
|
|
|
109
124
|
return Math.floor(Date.now() / 1000)
|
|
110
125
|
}
|
|
111
126
|
|
|
127
|
+
function normalizeOfflineRecoverySeconds (value) {
|
|
128
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > MAX_OFFLINE_RECOVERY_SECONDS) {
|
|
129
|
+
throw new Error('INVALID_OFFLINE_RECOVERY_SECONDS')
|
|
130
|
+
}
|
|
131
|
+
return value
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function normalizeStaleChannelSeconds (value) {
|
|
135
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > MAX_OFFLINE_RECOVERY_SECONDS) {
|
|
136
|
+
throw new Error('INVALID_STALE_CHANNEL_SECONDS')
|
|
137
|
+
}
|
|
138
|
+
return value
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function normalizeIdentityStorageRetentionSeconds (value) {
|
|
142
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > MAX_OFFLINE_RECOVERY_SECONDS) {
|
|
143
|
+
throw new Error('INVALID_IDENTITY_STORAGE_RETENTION_SECONDS')
|
|
144
|
+
}
|
|
145
|
+
return value
|
|
146
|
+
}
|
|
147
|
+
|
|
112
148
|
function uniq (values) {
|
|
113
149
|
return [...new Set((values || []).filter(Boolean))]
|
|
114
150
|
}
|
|
@@ -126,18 +162,33 @@ function parseJson (raw, fallback) {
|
|
|
126
162
|
try { return JSON.parse(raw || '') } catch { return fallback }
|
|
127
163
|
}
|
|
128
164
|
|
|
165
|
+
function sameStateValue (left, right) {
|
|
166
|
+
return JSON.stringify(left) === JSON.stringify(right)
|
|
167
|
+
}
|
|
168
|
+
|
|
129
169
|
function storesRecoverySeeds (mode) {
|
|
130
170
|
return mode === 'seeder' || mode === 'watchtower'
|
|
131
171
|
}
|
|
132
172
|
|
|
173
|
+
function randomStorageLeaseId () {
|
|
174
|
+
if (globalThis.crypto?.randomUUID) return globalThis.crypto.randomUUID()
|
|
175
|
+
const bytes = globalThis.crypto?.getRandomValues?.(new Uint8Array(16))
|
|
176
|
+
if (bytes) return [...bytes].map(value => value.toString(16).padStart(2, '0')).join('')
|
|
177
|
+
return `${Date.now().toString(36)}:${Math.random().toString(36).slice(2)}`
|
|
178
|
+
}
|
|
179
|
+
|
|
133
180
|
export class PrivateMessenger {
|
|
134
|
-
static
|
|
135
|
-
|
|
181
|
+
static maintainStorage ({
|
|
182
|
+
indexedDB = globalThis.indexedDB,
|
|
183
|
+
temporaryStorageArea = globalThis.sessionStorage
|
|
184
|
+
} = {}) {
|
|
185
|
+
return maintainPrivateMessengerStorage({ indexedDB, temporaryStorageArea })
|
|
136
186
|
}
|
|
137
187
|
|
|
138
188
|
constructor ({
|
|
139
189
|
offlineRecoverySeconds = DEFAULT_OFFLINE_RECOVERY_SECONDS,
|
|
140
190
|
staleChannelSeconds = DEFAULT_STALE_CHANNEL_SECONDS,
|
|
191
|
+
identityStorageRetentionSeconds = DEFAULT_IDENTITY_STORAGE_RETENTION_SECONDS,
|
|
141
192
|
offlineSkewSeconds = DEFAULT_OFFLINE_SKEW_SECONDS,
|
|
142
193
|
reloadGapDelayMs = DEFAULT_RELOAD_GAP_DELAY_MS,
|
|
143
194
|
seederPresenceIntervalMs = DEFAULT_SEEDER_PRESENCE_INTERVAL_MS,
|
|
@@ -160,10 +211,14 @@ export class PrivateMessenger {
|
|
|
160
211
|
_subscribeRelayListUpdates = subscribeRelayListUpdates,
|
|
161
212
|
_setTimeout = globalThis.setTimeout.bind(globalThis),
|
|
162
213
|
_setInterval = globalThis.setInterval.bind(globalThis),
|
|
163
|
-
_clearInterval = globalThis.clearInterval.bind(globalThis)
|
|
214
|
+
_clearInterval = globalThis.clearInterval.bind(globalThis),
|
|
215
|
+
_storageSetInterval = globalThis.setInterval.bind(globalThis),
|
|
216
|
+
_storageClearInterval = globalThis.clearInterval.bind(globalThis),
|
|
217
|
+
_BroadcastChannel = _indexedDB === globalThis.indexedDB ? globalThis.BroadcastChannel : undefined
|
|
164
218
|
} = {}) {
|
|
165
|
-
this.offlineRecoverySeconds = offlineRecoverySeconds
|
|
166
|
-
this.staleChannelSeconds = staleChannelSeconds
|
|
219
|
+
this.offlineRecoverySeconds = normalizeOfflineRecoverySeconds(offlineRecoverySeconds)
|
|
220
|
+
this.staleChannelSeconds = normalizeStaleChannelSeconds(staleChannelSeconds)
|
|
221
|
+
this.identityStorageRetentionSeconds = normalizeIdentityStorageRetentionSeconds(identityStorageRetentionSeconds)
|
|
167
222
|
this.offlineSkewSeconds = offlineSkewSeconds
|
|
168
223
|
this.reloadGapDelayMs = reloadGapDelayMs
|
|
169
224
|
this.seederPresenceIntervalMs = seederPresenceIntervalMs
|
|
@@ -187,6 +242,9 @@ export class PrivateMessenger {
|
|
|
187
242
|
this._setTimeout = _setTimeout
|
|
188
243
|
this._setInterval = _setInterval
|
|
189
244
|
this._clearInterval = _clearInterval
|
|
245
|
+
this._storageSetInterval = _storageSetInterval
|
|
246
|
+
this._storageClearInterval = _storageClearInterval
|
|
247
|
+
this._BroadcastChannel = _BroadcastChannel
|
|
190
248
|
|
|
191
249
|
this.userSigner = null
|
|
192
250
|
this.contentKeySigner = null
|
|
@@ -208,42 +266,287 @@ export class PrivateMessenger {
|
|
|
208
266
|
this.stopOnline = null
|
|
209
267
|
this.stopOffline = null
|
|
210
268
|
this.queueOperationTail = Promise.resolve()
|
|
269
|
+
this.storageActive = false
|
|
270
|
+
this.storageLeaseId = randomStorageLeaseId()
|
|
271
|
+
this.lastStorageTouch = 0
|
|
272
|
+
this.storageTouchPromise = null
|
|
273
|
+
this.storageHeartbeatTimer = null
|
|
274
|
+
this.storageMaintenanceTimer = null
|
|
275
|
+
this.storageMaintenancePromise = null
|
|
276
|
+
this.storagePolicyRevision = 0
|
|
277
|
+
this.storagePolicyNeedsApply = false
|
|
278
|
+
this.storagePolicyBroadcast = null
|
|
279
|
+
this.storagePolicyRefreshTail = Promise.resolve()
|
|
280
|
+
this.closePromise = null
|
|
281
|
+
this.initSettledPromise = null
|
|
282
|
+
this.initialized = false
|
|
211
283
|
}
|
|
212
284
|
|
|
213
285
|
async init ({ userSigner, contentKeySigner, nymSigner, channels = [], relays = [], mode = 'leecher' }) {
|
|
214
286
|
if (!userSigner?.getPublicKey) throw new Error('USER_SIGNER_REQUIRED')
|
|
215
|
-
|
|
216
|
-
this.
|
|
217
|
-
this.
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
this.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
287
|
+
this.assertOpen()
|
|
288
|
+
if (this.initSettledPromise) throw new Error('PRIVATE_MESSENGER_INIT_IN_PROGRESS')
|
|
289
|
+
if (this.initialized) throw new Error('PRIVATE_MESSENGER_ALREADY_INITIALIZED')
|
|
290
|
+
let settleInit
|
|
291
|
+
const initSettledPromise = new Promise(resolve => { settleInit = resolve })
|
|
292
|
+
this.initSettledPromise = initSettledPromise
|
|
293
|
+
try {
|
|
294
|
+
this.userSigner = userSigner
|
|
295
|
+
this.contentKeySigner = contentKeySigner || null
|
|
296
|
+
this.nymSigner = nymSigner || null
|
|
297
|
+
this.userPubkey = await userSigner.getPublicKey()
|
|
298
|
+
this.prefix = `libp2r2p:private-messenger:${this.userPubkey}`
|
|
299
|
+
const storageSnapshot = await activatePrivateMessengerStorage({
|
|
300
|
+
userPubkey: this.userPubkey,
|
|
301
|
+
leaseId: this.storageLeaseId,
|
|
302
|
+
activeChannelPubkeys: [],
|
|
303
|
+
storagePolicy: {
|
|
304
|
+
staleChannelSeconds: this.staleChannelSeconds,
|
|
305
|
+
identityStorageRetentionSeconds: this.identityStorageRetentionSeconds
|
|
306
|
+
},
|
|
307
|
+
indexedDB: this._indexedDB
|
|
308
|
+
})
|
|
309
|
+
this.applyStoragePolicySnapshot(storageSnapshot)
|
|
310
|
+
this.storageActive = true
|
|
311
|
+
this.lastStorageTouch = Date.now()
|
|
312
|
+
this.startStoragePolicyBroadcast()
|
|
313
|
+
this.assertOpen()
|
|
314
|
+
await PrivateMessenger.maintainStorage({
|
|
315
|
+
indexedDB: this._indexedDB,
|
|
316
|
+
temporaryStorageArea: this.temporaryStorageArea
|
|
317
|
+
})
|
|
318
|
+
this.assertOpen()
|
|
319
|
+
this.contentKeyPubkey = await this.contentKeySigner?.getPublicKey?.() || ''
|
|
320
|
+
this.assertOpen()
|
|
321
|
+
this.queue = await createQueue({
|
|
322
|
+
prefix: this.prefix,
|
|
323
|
+
indexes: MESSAGE_QUEUE_INDEXES,
|
|
324
|
+
maxBytes: this.messageQueueMaxBytes,
|
|
325
|
+
evictionPolicy: 'fifo',
|
|
326
|
+
indexedDB: this._indexedDB
|
|
327
|
+
})
|
|
328
|
+
this.assertOpen()
|
|
329
|
+
this.seedQueue = await createQueue({
|
|
330
|
+
prefix: `${this.prefix}:seeds`,
|
|
331
|
+
indexes: SEED_QUEUE_INDEXES,
|
|
332
|
+
maxBytes: this.seedQueueMaxBytes,
|
|
333
|
+
evictionPolicy: 'fifo',
|
|
334
|
+
indexedDB: this._indexedDB
|
|
335
|
+
})
|
|
336
|
+
this.assertOpen()
|
|
337
|
+
this.stateStore = await createChannelStateStore({
|
|
338
|
+
prefix: this.prefix,
|
|
339
|
+
indexedDB: this._indexedDB
|
|
340
|
+
})
|
|
341
|
+
this.assertOpen()
|
|
342
|
+
this.startStorageMaintenance()
|
|
343
|
+
this.state = { channels: await this.stateStore.load() }
|
|
344
|
+
await this.update({ userSigner, contentKeySigner, nymSigner: this.nymSigner, channels, relays, mode })
|
|
345
|
+
await this.pruneStoredSeeds()
|
|
346
|
+
this.initialized = true
|
|
347
|
+
await this.refreshAndApplyStoragePolicy()
|
|
348
|
+
this.broadcastStoragePolicyChange()
|
|
349
|
+
return this
|
|
350
|
+
} catch (err) {
|
|
351
|
+
this.stopStorageMaintenance()
|
|
352
|
+
this.stopStoragePolicyBroadcast()
|
|
353
|
+
try { await this.queueOperationTail } catch {}
|
|
354
|
+
try { await this.stateWriteTail } catch {}
|
|
355
|
+
await Promise.allSettled([
|
|
356
|
+
this.queue?.close?.(),
|
|
357
|
+
this.seedQueue?.close?.(),
|
|
358
|
+
this.stateStore?.close?.()
|
|
359
|
+
])
|
|
360
|
+
if (this.storageActive) {
|
|
361
|
+
try {
|
|
362
|
+
await releasePrivateMessengerStorage({
|
|
363
|
+
userPubkey: this.userPubkey,
|
|
364
|
+
leaseId: this.storageLeaseId,
|
|
365
|
+
indexedDB: this._indexedDB
|
|
366
|
+
})
|
|
367
|
+
} catch {}
|
|
368
|
+
}
|
|
369
|
+
this.storageActive = false
|
|
370
|
+
this.initialized = false
|
|
371
|
+
throw err
|
|
372
|
+
} finally {
|
|
373
|
+
settleInit()
|
|
374
|
+
if (this.initSettledPromise === initSettledPromise) this.initSettledPromise = null
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
assertOpen () {
|
|
379
|
+
if (this.closePromise) throw new Error('PRIVATE_MESSENGER_CLOSED')
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
applyStoragePolicySnapshot (snapshot) {
|
|
383
|
+
if (!snapshot) return false
|
|
384
|
+
const staleChannelSeconds = normalizeStaleChannelSeconds(snapshot.staleChannelSeconds)
|
|
385
|
+
const identityStorageRetentionSeconds = normalizeIdentityStorageRetentionSeconds(
|
|
386
|
+
snapshot.identityStorageRetentionSeconds
|
|
387
|
+
)
|
|
388
|
+
const policyRevision = Math.max(0, Number(snapshot.policyRevision) || 0)
|
|
389
|
+
const changed = this.staleChannelSeconds !== staleChannelSeconds ||
|
|
390
|
+
this.identityStorageRetentionSeconds !== identityStorageRetentionSeconds
|
|
391
|
+
this.staleChannelSeconds = staleChannelSeconds
|
|
392
|
+
this.identityStorageRetentionSeconds = identityStorageRetentionSeconds
|
|
393
|
+
this.storagePolicyRevision = policyRevision
|
|
394
|
+
if (changed && this.initialized) this.storagePolicyNeedsApply = true
|
|
395
|
+
return changed
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
startStoragePolicyBroadcast () {
|
|
399
|
+
if (this.storagePolicyBroadcast || typeof this._BroadcastChannel !== 'function') return
|
|
400
|
+
try {
|
|
401
|
+
const channel = new this._BroadcastChannel(STORAGE_POLICY_BROADCAST_CHANNEL)
|
|
402
|
+
channel.unref?.()
|
|
403
|
+
channel.onmessage = event => {
|
|
404
|
+
const message = event?.data
|
|
405
|
+
if (message?.userPubkey !== this.userPubkey) return
|
|
406
|
+
if (!Number.isSafeInteger(message.policyRevision) || message.policyRevision <= (this.storagePolicyRevision || 0)) return
|
|
407
|
+
this.refreshAndApplyStoragePolicy().catch(err => {
|
|
408
|
+
try { this.onError?.(err) } catch {}
|
|
409
|
+
})
|
|
410
|
+
}
|
|
411
|
+
this.storagePolicyBroadcast = channel
|
|
412
|
+
} catch {}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
stopStoragePolicyBroadcast () {
|
|
416
|
+
const channel = this.storagePolicyBroadcast
|
|
417
|
+
this.storagePolicyBroadcast = null
|
|
418
|
+
if (!channel) return
|
|
419
|
+
channel.onmessage = null
|
|
420
|
+
channel.close?.()
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
broadcastStoragePolicyChange () {
|
|
424
|
+
try {
|
|
425
|
+
this.storagePolicyBroadcast?.postMessage({
|
|
426
|
+
userPubkey: this.userPubkey,
|
|
427
|
+
policyRevision: this.storagePolicyRevision || 0
|
|
428
|
+
})
|
|
429
|
+
} catch (err) {
|
|
430
|
+
try { this.onError?.(err) } catch {}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async readStoragePolicySnapshot () {
|
|
435
|
+
if (!this.userPubkey) return null
|
|
436
|
+
const snapshot = await readPrivateMessengerStorage({
|
|
437
|
+
userPubkey: this.userPubkey,
|
|
227
438
|
indexedDB: this._indexedDB
|
|
228
439
|
})
|
|
229
|
-
this.
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
440
|
+
this.applyStoragePolicySnapshot(snapshot)
|
|
441
|
+
return snapshot
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async applyPendingStoragePolicy () {
|
|
445
|
+
if (!this.storagePolicyNeedsApply || !this.initialized || this.closePromise) return false
|
|
446
|
+
this.storagePolicyNeedsApply = false
|
|
447
|
+
try {
|
|
448
|
+
const channels = [...this.channels.values()]
|
|
449
|
+
await this.applyRecoveryPolicies(channels)
|
|
450
|
+
await this.cleanupStaleChannels()
|
|
451
|
+
const pubkeys = [...this.channels.keys()]
|
|
452
|
+
if (pubkeys.length) {
|
|
453
|
+
await this.unwatch(pubkeys)
|
|
454
|
+
await this.watch(pubkeys)
|
|
455
|
+
}
|
|
456
|
+
await this.reconcilePresencePublishers()
|
|
457
|
+
return true
|
|
458
|
+
} catch (err) {
|
|
459
|
+
this.storagePolicyNeedsApply = true
|
|
460
|
+
throw err
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
refreshAndApplyStoragePolicy () {
|
|
465
|
+
const previous = this.storagePolicyRefreshTail || Promise.resolve()
|
|
466
|
+
const refresh = previous.catch(() => {}).then(async () => {
|
|
467
|
+
await this.readStoragePolicySnapshot()
|
|
468
|
+
return this.applyPendingStoragePolicy()
|
|
235
469
|
})
|
|
236
|
-
this.
|
|
237
|
-
|
|
238
|
-
|
|
470
|
+
this.storagePolicyRefreshTail = refresh
|
|
471
|
+
return refresh
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
startStorageMaintenance () {
|
|
475
|
+
if (this.storageHeartbeatTimer || this.storageMaintenanceTimer) return
|
|
476
|
+
this.storageHeartbeatTimer = this._storageSetInterval(() => (
|
|
477
|
+
this.runStorageHeartbeat().catch(err => {
|
|
478
|
+
try { this.onError?.(err) } catch {}
|
|
479
|
+
})
|
|
480
|
+
), PRIVATE_MESSENGER_STORAGE_HEARTBEAT_MS)
|
|
481
|
+
this.storageMaintenanceTimer = this._storageSetInterval(
|
|
482
|
+
() => this.runStorageMaintenance(),
|
|
483
|
+
PRIVATE_MESSENGER_STORAGE_MAINTENANCE_MS
|
|
484
|
+
)
|
|
485
|
+
this.storageHeartbeatTimer?.unref?.()
|
|
486
|
+
this.storageMaintenanceTimer?.unref?.()
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
stopStorageMaintenance () {
|
|
490
|
+
if (this.storageHeartbeatTimer) this._storageClearInterval(this.storageHeartbeatTimer)
|
|
491
|
+
if (this.storageMaintenanceTimer) this._storageClearInterval(this.storageMaintenanceTimer)
|
|
492
|
+
this.storageHeartbeatTimer = null
|
|
493
|
+
this.storageMaintenanceTimer = null
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
runStorageMaintenance () {
|
|
497
|
+
if (this.storageMaintenancePromise) return this.storageMaintenancePromise
|
|
498
|
+
const maintenance = (async () => {
|
|
499
|
+
await PrivateMessenger.maintainStorage({
|
|
500
|
+
indexedDB: this._indexedDB,
|
|
501
|
+
temporaryStorageArea: this.temporaryStorageArea
|
|
502
|
+
})
|
|
503
|
+
await this.refreshAndApplyStoragePolicy()
|
|
504
|
+
await this.cleanupStaleChannels()
|
|
505
|
+
await this.pruneStoredSeeds()
|
|
506
|
+
})().catch(err => {
|
|
507
|
+
try { this.onError?.(err) } catch {}
|
|
508
|
+
}).finally(() => {
|
|
509
|
+
if (this.storageMaintenancePromise === maintenance) this.storageMaintenancePromise = null
|
|
239
510
|
})
|
|
240
|
-
this.
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
511
|
+
this.storageMaintenancePromise = maintenance
|
|
512
|
+
return maintenance
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
async runStorageHeartbeat () {
|
|
516
|
+
await this.stampActiveChannelActivity()
|
|
517
|
+
await this.touchStorageActivity({ force: true })
|
|
518
|
+
await this.applyPendingStoragePolicy()
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
async touchStorageActivity ({ force = false } = {}) {
|
|
522
|
+
if (!this.storageActive || this.closePromise) return false
|
|
523
|
+
if (this.storageTouchPromise) return this.storageTouchPromise
|
|
524
|
+
const now = Date.now()
|
|
525
|
+
if (!force && now - this.lastStorageTouch < PRIVATE_MESSENGER_STORAGE_HEARTBEAT_MS) return false
|
|
526
|
+
const previousTouch = this.lastStorageTouch
|
|
527
|
+
this.lastStorageTouch = now
|
|
528
|
+
const touch = activatePrivateMessengerStorage({
|
|
529
|
+
userPubkey: this.userPubkey,
|
|
530
|
+
leaseId: this.storageLeaseId,
|
|
531
|
+
activeChannelPubkeys: [...this.channels.keys()],
|
|
532
|
+
indexedDB: this._indexedDB,
|
|
533
|
+
now
|
|
534
|
+
}).then(snapshot => {
|
|
535
|
+
this.applyStoragePolicySnapshot(snapshot)
|
|
536
|
+
return true
|
|
537
|
+
}, err => {
|
|
538
|
+
this.lastStorageTouch = previousTouch
|
|
539
|
+
throw err
|
|
540
|
+
}).finally(() => {
|
|
541
|
+
if (this.storageTouchPromise === touch) this.storageTouchPromise = null
|
|
542
|
+
})
|
|
543
|
+
this.storageTouchPromise = touch
|
|
544
|
+
return touch
|
|
244
545
|
}
|
|
245
546
|
|
|
246
547
|
runQueueOperation (operation) {
|
|
548
|
+
if (this.closePromise) return Promise.reject(new Error('PRIVATE_MESSENGER_CLOSED'))
|
|
549
|
+
this.touchStorageActivity().catch(err => this.onError?.(err))
|
|
247
550
|
const run = this.queueOperationTail.then(operation)
|
|
248
551
|
this.queueOperationTail = run.catch(err => {
|
|
249
552
|
try { this.onError?.(err) } catch {}
|
|
@@ -277,7 +580,24 @@ export class PrivateMessenger {
|
|
|
277
580
|
})
|
|
278
581
|
}
|
|
279
582
|
|
|
280
|
-
async update (
|
|
583
|
+
async update (options = {}) {
|
|
584
|
+
this.assertOpen()
|
|
585
|
+
const {
|
|
586
|
+
userSigner = this.userSigner,
|
|
587
|
+
contentKeySigner = this.contentKeySigner,
|
|
588
|
+
nymSigner = this.nymSigner,
|
|
589
|
+
channels = [...this.channels.values()],
|
|
590
|
+
relays = [],
|
|
591
|
+
mode = 'leecher'
|
|
592
|
+
} = options
|
|
593
|
+
const updatesStalePolicy = Object.hasOwn(options, 'staleChannelSeconds')
|
|
594
|
+
const updatesIdentityPolicy = Object.hasOwn(options, 'identityStorageRetentionSeconds')
|
|
595
|
+
let nextStaleChannelSeconds = updatesStalePolicy
|
|
596
|
+
? normalizeStaleChannelSeconds(options.staleChannelSeconds)
|
|
597
|
+
: this.staleChannelSeconds
|
|
598
|
+
let nextIdentityStorageRetentionSeconds = updatesIdentityPolicy
|
|
599
|
+
? normalizeIdentityStorageRetentionSeconds(options.identityStorageRetentionSeconds)
|
|
600
|
+
: this.identityStorageRetentionSeconds
|
|
281
601
|
if (userSigner) {
|
|
282
602
|
const userPubkey = await userSigner.getPublicKey?.()
|
|
283
603
|
if (!userPubkey) throw new Error('USER_SIGNER_REQUIRED')
|
|
@@ -288,19 +608,50 @@ export class PrivateMessenger {
|
|
|
288
608
|
this.nymSigner = nymSigner || null
|
|
289
609
|
this.contentKeyPubkey = await this.contentKeySigner?.getPublicKey?.() || ''
|
|
290
610
|
const nextChannels = await this.normalizeChannels(channels, { relays, mode })
|
|
611
|
+
this.assertOpen()
|
|
291
612
|
const nextPubkeys = new Set(nextChannels.map(channel => channel.pubkey))
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
613
|
+
const removedPubkeys = [...this.channels.keys()].filter(pubkey => !nextPubkeys.has(pubkey))
|
|
614
|
+
const updatesStoragePolicy = updatesStalePolicy || updatesIdentityPolicy
|
|
615
|
+
|
|
616
|
+
if (updatesStoragePolicy) {
|
|
617
|
+
const currentPolicy = await this.readStoragePolicySnapshot()
|
|
618
|
+
if (!updatesStalePolicy) nextStaleChannelSeconds = currentPolicy?.staleChannelSeconds ?? this.staleChannelSeconds
|
|
619
|
+
if (!updatesIdentityPolicy) {
|
|
620
|
+
nextIdentityStorageRetentionSeconds = currentPolicy?.identityStorageRetentionSeconds ?? this.identityStorageRetentionSeconds
|
|
297
621
|
}
|
|
298
622
|
}
|
|
623
|
+
|
|
624
|
+
if (removedPubkeys.length) {
|
|
625
|
+
await this.stampChannelActivity(removedPubkeys)
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const storageSnapshot = await activatePrivateMessengerStorage({
|
|
629
|
+
userPubkey: this.userPubkey,
|
|
630
|
+
leaseId: this.storageLeaseId,
|
|
631
|
+
activeChannelPubkeys: [...nextPubkeys],
|
|
632
|
+
storagePolicy: updatesStoragePolicy
|
|
633
|
+
? {
|
|
634
|
+
staleChannelSeconds: nextStaleChannelSeconds,
|
|
635
|
+
identityStorageRetentionSeconds: nextIdentityStorageRetentionSeconds
|
|
636
|
+
}
|
|
637
|
+
: undefined,
|
|
638
|
+
indexedDB: this._indexedDB
|
|
639
|
+
})
|
|
640
|
+
this.applyStoragePolicySnapshot(storageSnapshot)
|
|
641
|
+
this.lastStorageTouch = Date.now()
|
|
642
|
+
if (updatesStoragePolicy) this.broadcastStoragePolicyChange()
|
|
643
|
+
|
|
644
|
+
await this.unwatch([...this.channels.keys()])
|
|
645
|
+
this.channels.clear()
|
|
299
646
|
for (const channel of nextChannels) this.channels.set(channel.pubkey, channel)
|
|
300
647
|
|
|
301
|
-
await this.cleanupStaleChannels()
|
|
648
|
+
await this.cleanupStaleChannels({ storageSnapshot })
|
|
649
|
+
await this.applyRecoveryPolicies(nextChannels)
|
|
302
650
|
await this.watch()
|
|
303
651
|
await this.reconcilePresencePublishers()
|
|
652
|
+
if (this.storagePolicyRevision === storageSnapshot.policyRevision) {
|
|
653
|
+
this.storagePolicyNeedsApply = false
|
|
654
|
+
}
|
|
304
655
|
return this
|
|
305
656
|
}
|
|
306
657
|
|
|
@@ -323,6 +674,9 @@ export class PrivateMessenger {
|
|
|
323
674
|
const autoDeletionCapability = channel.autoDeletionCapability === undefined
|
|
324
675
|
? undefined
|
|
325
676
|
: normalizeAutoDeletionCapability(channel.autoDeletionCapability)
|
|
677
|
+
const offlineRecoverySeconds = normalizeOfflineRecoverySeconds(
|
|
678
|
+
channel.offlineRecoverySeconds ?? this.offlineRecoverySeconds
|
|
679
|
+
)
|
|
326
680
|
out.push({
|
|
327
681
|
pubkey,
|
|
328
682
|
signer,
|
|
@@ -334,6 +688,7 @@ export class PrivateMessenger {
|
|
|
334
688
|
usesNip65WatchRelays: !hasChannelRelays && !hasDefaultRelays,
|
|
335
689
|
mode,
|
|
336
690
|
seeders: uniq(channel.seeders),
|
|
691
|
+
offlineRecoverySeconds,
|
|
337
692
|
autoDeletionCapability
|
|
338
693
|
})
|
|
339
694
|
}
|
|
@@ -355,6 +710,7 @@ export class PrivateMessenger {
|
|
|
355
710
|
}
|
|
356
711
|
|
|
357
712
|
async recoveryMirrorRelays (channelPubkey) {
|
|
713
|
+
if (!this.offlineRecoverySecondsFor(channelPubkey)) return []
|
|
358
714
|
const seeders = this.recoverySeeders(channelPubkey)
|
|
359
715
|
if (!seeders.length) return []
|
|
360
716
|
try {
|
|
@@ -385,13 +741,19 @@ export class PrivateMessenger {
|
|
|
385
741
|
return structuredClone(this.state)
|
|
386
742
|
}
|
|
387
743
|
|
|
388
|
-
writeState (state) {
|
|
744
|
+
writeState (state, { touchStorage = true } = {}) {
|
|
745
|
+
if (touchStorage) this.touchStorageActivity().catch(err => this.onError?.(err))
|
|
746
|
+
const previous = this.state?.channels || {}
|
|
389
747
|
const next = {
|
|
390
748
|
channels: isPlainObject(state?.channels) ? structuredClone(state.channels) : {}
|
|
391
749
|
}
|
|
750
|
+
const changed = Object.fromEntries(Object.entries(next.channels)
|
|
751
|
+
.filter(([pubkey, value]) => !sameStateValue(previous[pubkey], value)))
|
|
752
|
+
const removed = Object.keys(previous).filter(pubkey => !Object.hasOwn(next.channels, pubkey))
|
|
392
753
|
this.state = next
|
|
393
|
-
|
|
394
|
-
const
|
|
754
|
+
if (!Object.keys(changed).length && !removed.length) return this.stateWriteTail
|
|
755
|
+
const snapshot = structuredClone(changed)
|
|
756
|
+
const write = this.stateWriteTail.then(() => this.stateStore.update(snapshot, removed))
|
|
395
757
|
this.stateWriteTail = write.catch(err => {
|
|
396
758
|
try { this.onError?.(err) } catch {}
|
|
397
759
|
})
|
|
@@ -402,6 +764,29 @@ export class PrivateMessenger {
|
|
|
402
764
|
await this.stateWriteTail
|
|
403
765
|
}
|
|
404
766
|
|
|
767
|
+
async stampChannelActivity (pubkeys) {
|
|
768
|
+
if (!this.stateStore || !this.state) return false
|
|
769
|
+
pubkeys = [...new Set(pubkeys || [])].filter(pubkey => this.state.channels[pubkey])
|
|
770
|
+
if (!pubkeys.length) return false
|
|
771
|
+
const lastWatchedAt = nowSeconds()
|
|
772
|
+
for (const pubkey of pubkeys) {
|
|
773
|
+
this.state.channels[pubkey] = {
|
|
774
|
+
...this.state.channels[pubkey],
|
|
775
|
+
lastWatchedAt
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
const write = this.stateWriteTail.then(() => this.stateStore.touch(pubkeys, lastWatchedAt))
|
|
779
|
+
this.stateWriteTail = write.catch(err => {
|
|
780
|
+
try { this.onError?.(err) } catch {}
|
|
781
|
+
})
|
|
782
|
+
await write
|
|
783
|
+
return true
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
stampActiveChannelActivity () {
|
|
787
|
+
return this.stampChannelActivity([...this.channels.keys()])
|
|
788
|
+
}
|
|
789
|
+
|
|
405
790
|
updateChannelState (pubkey, patch) {
|
|
406
791
|
const state = this.readState()
|
|
407
792
|
const current = state.channels[pubkey] || {}
|
|
@@ -411,9 +796,19 @@ export class PrivateMessenger {
|
|
|
411
796
|
}
|
|
412
797
|
|
|
413
798
|
removeChannelState (pubkey) {
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
799
|
+
this.removeChannelStates([pubkey])
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
removeChannelStates (pubkeys) {
|
|
803
|
+
pubkeys = [...new Set(pubkeys || [])]
|
|
804
|
+
for (const pubkey of pubkeys) delete this.state.channels[pubkey]
|
|
805
|
+
if (!pubkeys.length) return this.stateWriteTail
|
|
806
|
+
this.touchStorageActivity().catch(err => this.onError?.(err))
|
|
807
|
+
const write = this.stateWriteTail.then(() => this.stateStore.update({}, pubkeys))
|
|
808
|
+
this.stateWriteTail = write.catch(err => {
|
|
809
|
+
try { this.onError?.(err) } catch {}
|
|
810
|
+
})
|
|
811
|
+
return write
|
|
417
812
|
}
|
|
418
813
|
|
|
419
814
|
markSeen (pubkey, createdAt = nowSeconds()) {
|
|
@@ -432,6 +827,7 @@ export class PrivateMessenger {
|
|
|
432
827
|
}
|
|
433
828
|
|
|
434
829
|
recoverySeeders (pubkey) {
|
|
830
|
+
if (!this.offlineRecoverySecondsFor(pubkey)) return []
|
|
435
831
|
const channel = this.channels.get(pubkey)
|
|
436
832
|
const configuredSeeders = channel?.seeders || []
|
|
437
833
|
if (configuredSeeders.length) return configuredSeeders.filter(seeder => seeder !== this.userPubkey)
|
|
@@ -446,6 +842,7 @@ export class PrivateMessenger {
|
|
|
446
842
|
}
|
|
447
843
|
|
|
448
844
|
markSeederActive (channelPubkey, seederPubkey, { announced = false, at = nowSeconds() } = {}) {
|
|
845
|
+
if (!this.offlineRecoverySecondsFor(channelPubkey)) return false
|
|
449
846
|
const state = this.readState()
|
|
450
847
|
const current = state.channels[channelPubkey] || {}
|
|
451
848
|
const activity = current.seederActivity || {}
|
|
@@ -459,6 +856,7 @@ export class PrivateMessenger {
|
|
|
459
856
|
current.seederActivity = activity
|
|
460
857
|
state.channels[channelPubkey] = current
|
|
461
858
|
this.writeState(state)
|
|
859
|
+
return true
|
|
462
860
|
}
|
|
463
861
|
|
|
464
862
|
trackSeederActivity (channelPubkey, message) {
|
|
@@ -544,8 +942,10 @@ export class PrivateMessenger {
|
|
|
544
942
|
}
|
|
545
943
|
|
|
546
944
|
addOfflineRange (pubkey, start, end) {
|
|
945
|
+
const recoverySeconds = this.offlineRecoverySecondsFor(pubkey)
|
|
946
|
+
if (!recoverySeconds) return
|
|
547
947
|
const now = nowSeconds()
|
|
548
|
-
const minStart = now -
|
|
948
|
+
const minStart = now - recoverySeconds
|
|
549
949
|
const normalized = {
|
|
550
950
|
start: Math.max(0, Math.floor(start)),
|
|
551
951
|
end: Math.floor(end)
|
|
@@ -567,10 +967,17 @@ export class PrivateMessenger {
|
|
|
567
967
|
closeOpenOfflineRanges () {
|
|
568
968
|
const state = this.readState()
|
|
569
969
|
const end = nowSeconds()
|
|
570
|
-
const minStart = end - this.offlineRecoverySeconds
|
|
571
970
|
for (const pubkey of Object.keys(state.channels)) {
|
|
572
971
|
const current = state.channels[pubkey]
|
|
573
972
|
if (!current.openOfflineStart) continue
|
|
973
|
+
const recoverySeconds = this.offlineRecoverySecondsFor(pubkey)
|
|
974
|
+
if (!recoverySeconds) {
|
|
975
|
+
delete current.openOfflineStart
|
|
976
|
+
current.offlineRanges = []
|
|
977
|
+
state.channels[pubkey] = current
|
|
978
|
+
continue
|
|
979
|
+
}
|
|
980
|
+
const minStart = end - recoverySeconds
|
|
574
981
|
const start = Math.max(minStart, Math.max(0, current.openOfflineStart))
|
|
575
982
|
if (end > start) {
|
|
576
983
|
current.offlineRanges = mergeRanges((current.offlineRanges || []).concat([{ start, end }]))
|
|
@@ -582,11 +989,13 @@ export class PrivateMessenger {
|
|
|
582
989
|
}
|
|
583
990
|
|
|
584
991
|
async watch (channels = [...this.channels.keys()], { scheduleReloadGap = true } = {}) {
|
|
992
|
+
this.assertOpen()
|
|
585
993
|
const channelPubkeys = uniq(channels)
|
|
586
994
|
for (const pubkey of channelPubkeys) {
|
|
587
995
|
const channel = this.channels.get(pubkey)
|
|
588
996
|
if (!channel) throw new Error('UNKNOWN_CHANNEL')
|
|
589
997
|
const watchRelays = await this.resolveWatchRelays(channel)
|
|
998
|
+
this.assertOpen()
|
|
590
999
|
const stop = await this._privateMessage.watch({
|
|
591
1000
|
channels: [pubkey],
|
|
592
1001
|
relays: watchRelays,
|
|
@@ -604,16 +1013,21 @@ export class PrivateMessenger {
|
|
|
604
1013
|
onMessage: message => this.queueIncoming(() => this.handleMessage(pubkey, message)),
|
|
605
1014
|
onSeed: seed => this.queueIncoming(() => this.enqueueSeed(pubkey, seed)),
|
|
606
1015
|
onContentKeyUsage: usage => this.handleContentKeyUsage(pubkey, usage),
|
|
607
|
-
receivedChunkTtlMs: this.
|
|
1016
|
+
receivedChunkTtlMs: this.receivedChunkTtlMsFor(channel),
|
|
608
1017
|
receivedChunkIndexedDB: this._indexedDB,
|
|
609
1018
|
onError: err => this.onError?.(err)
|
|
610
1019
|
})
|
|
1020
|
+
if (this.closePromise) {
|
|
1021
|
+
await stop?.()
|
|
1022
|
+
this.assertOpen()
|
|
1023
|
+
}
|
|
611
1024
|
this.stopByChannel.set(pubkey, stop)
|
|
612
1025
|
this.updateChannelState(pubkey, {
|
|
613
1026
|
lastWatchedAt: nowSeconds(),
|
|
614
1027
|
mode: channel.mode,
|
|
615
1028
|
relays: watchRelays,
|
|
616
|
-
seeders: channel.seeders
|
|
1029
|
+
seeders: channel.seeders,
|
|
1030
|
+
offlineRecoverySeconds: channel.offlineRecoverySeconds
|
|
617
1031
|
})
|
|
618
1032
|
this.debug('watch', {
|
|
619
1033
|
channelPubkey: pubkey,
|
|
@@ -631,12 +1045,15 @@ export class PrivateMessenger {
|
|
|
631
1045
|
|
|
632
1046
|
unwatch (channels) {
|
|
633
1047
|
const channelPubkeys = channels ? uniq(Array.isArray(channels) ? channels : [channels]) : [...this.stopByChannel.keys()]
|
|
1048
|
+
const closing = []
|
|
634
1049
|
for (const pubkey of channelPubkeys) {
|
|
635
|
-
this.stopByChannel.get(pubkey)?.()
|
|
1050
|
+
const close = this.stopByChannel.get(pubkey)?.()
|
|
1051
|
+
if (close && typeof close.then === 'function') closing.push(close)
|
|
636
1052
|
this.stopByChannel.delete(pubkey)
|
|
637
1053
|
this.stopPresencePublisher(pubkey)
|
|
638
1054
|
}
|
|
639
1055
|
this.ensureRelayListWatcher()
|
|
1056
|
+
return Promise.allSettled(closing)
|
|
640
1057
|
}
|
|
641
1058
|
|
|
642
1059
|
nip65WatchChannelPubkeys () {
|
|
@@ -762,6 +1179,7 @@ export class PrivateMessenger {
|
|
|
762
1179
|
}
|
|
763
1180
|
|
|
764
1181
|
async enqueueSeed (channelPubkey, seed) {
|
|
1182
|
+
if (!this.offlineRecoverySecondsFor(channelPubkey)) return
|
|
765
1183
|
const receivedAt = nowSeconds()
|
|
766
1184
|
if (seed.recordType === NYM_CARRIER_SEED_RECORD_TYPE || seed.carriers?.length) {
|
|
767
1185
|
const carriers = compactSeedNymCarriers(seed.carriers)
|
|
@@ -818,6 +1236,7 @@ export class PrivateMessenger {
|
|
|
818
1236
|
|
|
819
1237
|
async nextMessage () {
|
|
820
1238
|
// seedQueue is retained replay material for recovery replies, not an app-message stream.
|
|
1239
|
+
this.touchStorageActivity().catch(err => this.onError?.(err))
|
|
821
1240
|
await this.queueOperationTail
|
|
822
1241
|
return withoutQueueMetadata(await this.queue.shift())
|
|
823
1242
|
}
|
|
@@ -833,7 +1252,7 @@ export class PrivateMessenger {
|
|
|
833
1252
|
privateChannelReaderPubkey: channel.readerPubkey,
|
|
834
1253
|
receiverPubkey,
|
|
835
1254
|
...routing,
|
|
836
|
-
expirationSeconds: this.
|
|
1255
|
+
expirationSeconds: this.eventExpirationSecondsFor(channel),
|
|
837
1256
|
temporaryStorageArea: this.temporaryStorageArea,
|
|
838
1257
|
deletionPubkey,
|
|
839
1258
|
autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
|
|
@@ -859,7 +1278,7 @@ export class PrivateMessenger {
|
|
|
859
1278
|
question,
|
|
860
1279
|
receiverPubkey,
|
|
861
1280
|
...routing,
|
|
862
|
-
expirationSeconds: this.
|
|
1281
|
+
expirationSeconds: this.eventExpirationSecondsFor(channel),
|
|
863
1282
|
temporaryStorageArea: this.temporaryStorageArea,
|
|
864
1283
|
deletionPubkey,
|
|
865
1284
|
autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
|
|
@@ -883,7 +1302,7 @@ export class PrivateMessenger {
|
|
|
883
1302
|
privateChannelReaderPubkey: channel.readerPubkey,
|
|
884
1303
|
receiverPubkey,
|
|
885
1304
|
...routing,
|
|
886
|
-
expirationSeconds: this.
|
|
1305
|
+
expirationSeconds: this.eventExpirationSecondsFor(channel),
|
|
887
1306
|
temporaryStorageArea: this.temporaryStorageArea,
|
|
888
1307
|
deletionPubkey,
|
|
889
1308
|
autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
|
|
@@ -907,7 +1326,7 @@ export class PrivateMessenger {
|
|
|
907
1326
|
privateChannelReaderPubkey: channel.readerPubkey,
|
|
908
1327
|
receiverPubkeys,
|
|
909
1328
|
...routing,
|
|
910
|
-
expirationSeconds: this.
|
|
1329
|
+
expirationSeconds: this.eventExpirationSecondsFor(channel),
|
|
911
1330
|
temporaryStorageArea: this.temporaryStorageArea,
|
|
912
1331
|
deletionPubkey,
|
|
913
1332
|
autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
|
|
@@ -931,7 +1350,7 @@ export class PrivateMessenger {
|
|
|
931
1350
|
privateChannelReaderPubkey: channel.readerPubkey,
|
|
932
1351
|
receiverPubkeys,
|
|
933
1352
|
...routing,
|
|
934
|
-
expirationSeconds: this.
|
|
1353
|
+
expirationSeconds: this.eventExpirationSecondsFor(channel),
|
|
935
1354
|
temporaryStorageArea: this.temporaryStorageArea,
|
|
936
1355
|
deletionPubkey,
|
|
937
1356
|
autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
|
|
@@ -951,7 +1370,7 @@ export class PrivateMessenger {
|
|
|
951
1370
|
privateChannelReaderPubkey: channel.readerPubkey,
|
|
952
1371
|
receiverPubkeys,
|
|
953
1372
|
...routing,
|
|
954
|
-
expirationSeconds: this.
|
|
1373
|
+
expirationSeconds: this.eventExpirationSecondsFor(channel),
|
|
955
1374
|
temporaryStorageArea: this.temporaryStorageArea,
|
|
956
1375
|
deletionPubkey,
|
|
957
1376
|
autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
|
|
@@ -970,7 +1389,7 @@ export class PrivateMessenger {
|
|
|
970
1389
|
privateChannelSigner: channel.signer,
|
|
971
1390
|
privateChannelReaderPubkey: channel.readerPubkey,
|
|
972
1391
|
...routing,
|
|
973
|
-
expirationSeconds: this.
|
|
1392
|
+
expirationSeconds: this.eventExpirationSecondsFor(channel),
|
|
974
1393
|
deletionPubkey,
|
|
975
1394
|
autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
|
|
976
1395
|
rumor
|
|
@@ -987,7 +1406,7 @@ export class PrivateMessenger {
|
|
|
987
1406
|
privateChannelSigner: channel.signer,
|
|
988
1407
|
privateChannelReaderPubkey: channel.readerPubkey,
|
|
989
1408
|
...routing,
|
|
990
|
-
expirationSeconds: this.
|
|
1409
|
+
expirationSeconds: this.eventExpirationSecondsFor(channel),
|
|
991
1410
|
deletionPubkey,
|
|
992
1411
|
autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
|
|
993
1412
|
event
|
|
@@ -996,6 +1415,7 @@ export class PrivateMessenger {
|
|
|
996
1415
|
|
|
997
1416
|
async publishSeederPresence (channelPubkey = this.defaultChannelPubkey()) {
|
|
998
1417
|
const channel = this.requireWritableChannel(channelPubkey)
|
|
1418
|
+
if (!this.offlineRecoverySecondsFor(channel)) return null
|
|
999
1419
|
const receiverPubkeys = uniq([...this.knownSeeders(channelPubkey), this.userPubkey])
|
|
1000
1420
|
const routing = await this.resolveSendRouting({ channel, receiverPubkeys })
|
|
1001
1421
|
this.debugSend('yell', channelPubkey, { code: SEEDER_PRESENCE_CODE, receiverPubkeys })
|
|
@@ -1006,7 +1426,7 @@ export class PrivateMessenger {
|
|
|
1006
1426
|
privateChannelReaderPubkey: channel.readerPubkey,
|
|
1007
1427
|
receiverPubkeys,
|
|
1008
1428
|
...routing,
|
|
1009
|
-
expirationSeconds: this.
|
|
1429
|
+
expirationSeconds: this.eventExpirationSecondsFor(channel),
|
|
1010
1430
|
temporaryStorageArea: this.temporaryStorageArea,
|
|
1011
1431
|
autoDeletionCapability: this.autoDeletionCapabilityFor(channel),
|
|
1012
1432
|
code: SEEDER_PRESENCE_CODE,
|
|
@@ -1016,6 +1436,7 @@ export class PrivateMessenger {
|
|
|
1016
1436
|
}
|
|
1017
1437
|
|
|
1018
1438
|
async startPresencePublisher (channelPubkey) {
|
|
1439
|
+
if (!this.offlineRecoverySecondsFor(channelPubkey)) return
|
|
1019
1440
|
if (this.presenceTimers.has(channelPubkey)) return
|
|
1020
1441
|
try {
|
|
1021
1442
|
await this.publishSeederPresence(channelPubkey)
|
|
@@ -1040,10 +1461,10 @@ export class PrivateMessenger {
|
|
|
1040
1461
|
async reconcilePresencePublishers () {
|
|
1041
1462
|
const starts = []
|
|
1042
1463
|
for (const pubkey of [...this.presenceTimers.keys()]) {
|
|
1043
|
-
if (!storesRecoverySeeds(this.channels.get(pubkey)?.mode)) this.stopPresencePublisher(pubkey)
|
|
1464
|
+
if (!storesRecoverySeeds(this.channels.get(pubkey)?.mode) || !this.offlineRecoverySecondsFor(pubkey)) this.stopPresencePublisher(pubkey)
|
|
1044
1465
|
}
|
|
1045
1466
|
for (const [pubkey, channel] of this.channels) {
|
|
1046
|
-
if (storesRecoverySeeds(channel.mode)) starts.push(this.startPresencePublisher(pubkey))
|
|
1467
|
+
if (storesRecoverySeeds(channel.mode) && this.offlineRecoverySecondsFor(channel)) starts.push(this.startPresencePublisher(pubkey))
|
|
1047
1468
|
else this.stopPresencePublisher(pubkey)
|
|
1048
1469
|
}
|
|
1049
1470
|
await Promise.all(starts)
|
|
@@ -1077,6 +1498,66 @@ export class PrivateMessenger {
|
|
|
1077
1498
|
return channel.autoDeletionCapability ?? this.autoDeletionCapability
|
|
1078
1499
|
}
|
|
1079
1500
|
|
|
1501
|
+
requestedOfflineRecoverySecondsFor (channelOrPubkey) {
|
|
1502
|
+
const channel = typeof channelOrPubkey === 'string'
|
|
1503
|
+
? this.channels.get(channelOrPubkey)
|
|
1504
|
+
: channelOrPubkey
|
|
1505
|
+
if (channel?.offlineRecoverySeconds !== undefined) return channel.offlineRecoverySeconds
|
|
1506
|
+
const pubkey = typeof channelOrPubkey === 'string' ? channelOrPubkey : channelOrPubkey?.pubkey
|
|
1507
|
+
const persisted = pubkey ? this.state.channels[pubkey]?.offlineRecoverySeconds : undefined
|
|
1508
|
+
return persisted === undefined
|
|
1509
|
+
? this.offlineRecoverySeconds
|
|
1510
|
+
: normalizeOfflineRecoverySeconds(persisted)
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
offlineRecoverySecondsFor (channelOrPubkey) {
|
|
1514
|
+
return Math.min(
|
|
1515
|
+
this.requestedOfflineRecoverySecondsFor(channelOrPubkey),
|
|
1516
|
+
this.staleChannelSeconds,
|
|
1517
|
+
this.identityStorageRetentionSeconds
|
|
1518
|
+
)
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
staleChannelSecondsForCleanup () {
|
|
1522
|
+
return Math.min(this.staleChannelSeconds, this.identityStorageRetentionSeconds)
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
eventExpirationSecondsFor (channel) {
|
|
1526
|
+
return this.offlineRecoverySecondsFor(channel) || privateChannel.EXPIRATION_SECONDS
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
receivedChunkTtlMsFor (channel) {
|
|
1530
|
+
const seconds = this.offlineRecoverySecondsFor(channel)
|
|
1531
|
+
return seconds ? seconds * 1000 : DEFAULT_RECEIVED_CHUNK_TTL_MS
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
async applyRecoveryPolicies (channels) {
|
|
1535
|
+
return this.runQueueOperation(async () => {
|
|
1536
|
+
const state = this.readState()
|
|
1537
|
+
const now = nowSeconds()
|
|
1538
|
+
for (const channel of channels) {
|
|
1539
|
+
const current = state.channels[channel.pubkey] || {}
|
|
1540
|
+
const requestedSeconds = channel.offlineRecoverySeconds
|
|
1541
|
+
const effectiveSeconds = this.offlineRecoverySecondsFor(channel)
|
|
1542
|
+
current.offlineRecoverySeconds = requestedSeconds
|
|
1543
|
+
if (!effectiveSeconds) {
|
|
1544
|
+
delete current.openOfflineStart
|
|
1545
|
+
current.offlineRanges = []
|
|
1546
|
+
} else {
|
|
1547
|
+
const cutoff = now - effectiveSeconds
|
|
1548
|
+
current.offlineRanges = mergeRanges((current.offlineRanges || [])
|
|
1549
|
+
.filter(range => range.end >= cutoff)
|
|
1550
|
+
.map(range => ({ ...range, start: Math.max(range.start, cutoff) })))
|
|
1551
|
+
if (current.openOfflineStart) current.openOfflineStart = Math.max(current.openOfflineStart, cutoff)
|
|
1552
|
+
}
|
|
1553
|
+
state.channels[channel.pubkey] = current
|
|
1554
|
+
}
|
|
1555
|
+
this.writeState(state)
|
|
1556
|
+
await this.flushStateWrites()
|
|
1557
|
+
for (const channel of channels) await this.pruneStoredSeeds(channel.pubkey)
|
|
1558
|
+
})
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1080
1561
|
requireNymSigner (channel, override) {
|
|
1081
1562
|
const signer = override || channel?.nymSigner || this.nymSigner
|
|
1082
1563
|
if (!signer?.getPublicKey) throw new Error('NYM_SIGNER_REQUIRED')
|
|
@@ -1088,6 +1569,7 @@ export class PrivateMessenger {
|
|
|
1088
1569
|
}
|
|
1089
1570
|
|
|
1090
1571
|
scheduleReloadGap (pubkey) {
|
|
1572
|
+
if (!this.offlineRecoverySecondsFor(pubkey)) return
|
|
1091
1573
|
const current = this.readState().channels[pubkey]
|
|
1092
1574
|
const start = current?.openOfflineStart || current?.lastSeenAt
|
|
1093
1575
|
if (!start) return
|
|
@@ -1117,6 +1599,7 @@ export class PrivateMessenger {
|
|
|
1117
1599
|
const state = this.readState()
|
|
1118
1600
|
const start = Math.max(0, nowSeconds() - this.offlineSkewSeconds)
|
|
1119
1601
|
for (const pubkey of this.channels.keys()) {
|
|
1602
|
+
if (!this.offlineRecoverySecondsFor(pubkey)) continue
|
|
1120
1603
|
const current = state.channels[pubkey] || {}
|
|
1121
1604
|
current.openOfflineStart ||= start
|
|
1122
1605
|
state.channels[pubkey] = current
|
|
@@ -1139,6 +1622,7 @@ export class PrivateMessenger {
|
|
|
1139
1622
|
}
|
|
1140
1623
|
|
|
1141
1624
|
async askSeedersForMissingRange (channelPubkey, since, until) {
|
|
1625
|
+
if (!this.offlineRecoverySecondsFor(channelPubkey)) return []
|
|
1142
1626
|
if (!this.channels.get(channelPubkey)?.signer) return []
|
|
1143
1627
|
const seeders = this.recoverySeeders(channelPubkey)
|
|
1144
1628
|
if (!seeders.length || until < since) return []
|
|
@@ -1175,11 +1659,14 @@ export class PrivateMessenger {
|
|
|
1175
1659
|
question: message.event,
|
|
1176
1660
|
receiverPubkey: message.event?.pubkey,
|
|
1177
1661
|
since,
|
|
1178
|
-
until
|
|
1662
|
+
until,
|
|
1663
|
+
sendEmptyReply: !this.offlineRecoverySecondsFor(channelPubkey)
|
|
1179
1664
|
})
|
|
1180
1665
|
|
|
1181
|
-
|
|
1182
|
-
await
|
|
1666
|
+
if (this.offlineRecoverySecondsFor(channelPubkey)) {
|
|
1667
|
+
for await (const seed of this.seedQueue.storedItemsBy('byChannel', channelPubkey)) {
|
|
1668
|
+
await packer.update(seed)
|
|
1669
|
+
}
|
|
1183
1670
|
}
|
|
1184
1671
|
await packer.finalize()
|
|
1185
1672
|
}
|
|
@@ -1263,12 +1750,14 @@ export class PrivateMessenger {
|
|
|
1263
1750
|
async recoverOfflineRanges (channels = [...this.stopByChannel.keys()]) {
|
|
1264
1751
|
const state = this.readState()
|
|
1265
1752
|
const now = nowSeconds()
|
|
1266
|
-
const minStart = now - this.offlineRecoverySeconds
|
|
1267
1753
|
|
|
1268
1754
|
for (const pubkey of uniq(channels)) {
|
|
1269
1755
|
const channel = this.channels.get(pubkey)
|
|
1270
1756
|
const current = state.channels[pubkey]
|
|
1271
1757
|
if (!channel || !current?.offlineRanges?.length) continue
|
|
1758
|
+
const recoverySeconds = this.offlineRecoverySecondsFor(channel)
|
|
1759
|
+
if (!recoverySeconds) continue
|
|
1760
|
+
const minStart = now - recoverySeconds
|
|
1272
1761
|
|
|
1273
1762
|
const remaining = []
|
|
1274
1763
|
for (const range of current.offlineRanges) {
|
|
@@ -1288,7 +1777,7 @@ export class PrivateMessenger {
|
|
|
1288
1777
|
until: range.end,
|
|
1289
1778
|
mode: channel.mode,
|
|
1290
1779
|
modeByPubkey: { [pubkey]: channel.mode },
|
|
1291
|
-
receivedChunkTtlMs: this.
|
|
1780
|
+
receivedChunkTtlMs: this.receivedChunkTtlMsFor(channel),
|
|
1292
1781
|
receivedChunkIndexedDB: this._indexedDB,
|
|
1293
1782
|
onEvent: (event, outer, meta) => this.queueIncoming(() => this.enqueueRumor(eventType(event), pubkey, { event, outer, meta, payload: parseEventContent(event) })),
|
|
1294
1783
|
onNymEvent: (event, outer, meta) => this.queueIncoming(() => this.enqueueRumor('nym', pubkey, { event, outer, meta, payload: parseEventContent(event) })),
|
|
@@ -1313,13 +1802,14 @@ export class PrivateMessenger {
|
|
|
1313
1802
|
|
|
1314
1803
|
async clearChannel (pubkey) {
|
|
1315
1804
|
return this.runQueueOperation(async () => {
|
|
1316
|
-
this.unwatch(pubkey)
|
|
1317
|
-
this._privateMessage.clearChannelState?.(pubkey)
|
|
1805
|
+
await this.unwatch(pubkey)
|
|
1806
|
+
await this._privateMessage.clearChannelState?.(pubkey)
|
|
1318
1807
|
this.channels.delete(pubkey)
|
|
1319
1808
|
this.removeChannelState(pubkey)
|
|
1320
1809
|
await this.flushStateWrites()
|
|
1321
1810
|
await this.queue.removeBy('byChannel', pubkey)
|
|
1322
1811
|
await this.seedQueue.removeBy('byChannel', pubkey)
|
|
1812
|
+
await this.touchStorageActivity({ force: true })
|
|
1323
1813
|
this.ensureRelayListWatcher()
|
|
1324
1814
|
})
|
|
1325
1815
|
}
|
|
@@ -1328,42 +1818,64 @@ export class PrivateMessenger {
|
|
|
1328
1818
|
return this.runQueueOperation(() => this.queue.clear())
|
|
1329
1819
|
}
|
|
1330
1820
|
|
|
1331
|
-
async cleanupStaleChannels () {
|
|
1821
|
+
async cleanupStaleChannels ({ storageSnapshot } = {}) {
|
|
1332
1822
|
if (!this.prefix) return
|
|
1823
|
+
storageSnapshot ||= await this.readStoragePolicySnapshot()
|
|
1824
|
+
if (!storageSnapshot) return
|
|
1825
|
+
const activeChannelPubkeys = new Set(storageSnapshot.activeChannelPubkeys || [])
|
|
1333
1826
|
return this.runQueueOperation(async () => {
|
|
1334
|
-
|
|
1335
|
-
const
|
|
1827
|
+
await this.flushStateWrites()
|
|
1828
|
+
const state = { channels: await this.stateStore.load() }
|
|
1829
|
+
const cutoff = nowSeconds() - this.staleChannelSecondsForCleanup()
|
|
1830
|
+
const stalePubkeys = []
|
|
1336
1831
|
for (const [pubkey, channel] of Object.entries(state.channels)) {
|
|
1832
|
+
if (activeChannelPubkeys.has(pubkey)) continue
|
|
1337
1833
|
if ((channel.lastWatchedAt || 0) >= cutoff) continue
|
|
1338
1834
|
delete state.channels[pubkey]
|
|
1835
|
+
stalePubkeys.push(pubkey)
|
|
1339
1836
|
await this.queue?.removeBy('byChannel', pubkey)
|
|
1340
1837
|
await this.seedQueue?.removeBy('byChannel', pubkey)
|
|
1341
1838
|
}
|
|
1342
|
-
this.
|
|
1343
|
-
await this.
|
|
1839
|
+
this.state = state
|
|
1840
|
+
if (stalePubkeys.length) await this.removeChannelStates(stalePubkeys)
|
|
1344
1841
|
})
|
|
1345
1842
|
}
|
|
1346
1843
|
|
|
1347
1844
|
async pruneStoredSeeds (channelPubkey) {
|
|
1348
1845
|
if (!this.seedQueue) return
|
|
1349
|
-
const cutoff = nowSeconds() - this.offlineRecoverySeconds
|
|
1350
1846
|
const keyRange = globalThis.IDBKeyRange
|
|
1351
|
-
if (channelPubkey
|
|
1352
|
-
|
|
1847
|
+
if (!channelPubkey) {
|
|
1848
|
+
const pubkeys = new Set([...Object.keys(this.state.channels), ...this.channels.keys()])
|
|
1849
|
+
for (const pubkey of pubkeys) await this.pruneStoredSeeds(pubkey)
|
|
1850
|
+
await this.seedQueue.removeWhere(item => !pubkeys.has(item.channelPubkey))
|
|
1353
1851
|
return
|
|
1354
1852
|
}
|
|
1355
|
-
|
|
1356
|
-
|
|
1853
|
+
const recoverySeconds = this.offlineRecoverySecondsFor(channelPubkey)
|
|
1854
|
+
if (!recoverySeconds) {
|
|
1855
|
+
await this.seedQueue.removeBy('byChannel', channelPubkey)
|
|
1856
|
+
return
|
|
1857
|
+
}
|
|
1858
|
+
const cutoff = nowSeconds() - recoverySeconds
|
|
1859
|
+
if (cutoff <= 0) return
|
|
1860
|
+
if (keyRange?.bound) {
|
|
1861
|
+
await this.seedQueue.removeBy('byChannelTime', keyRange.bound([channelPubkey, 0], [channelPubkey, cutoff], false, true))
|
|
1357
1862
|
return
|
|
1358
1863
|
}
|
|
1359
1864
|
await this.seedQueue.removeWhere(item => {
|
|
1360
|
-
if (
|
|
1865
|
+
if (item.channelPubkey !== channelPubkey) return false
|
|
1361
1866
|
return (seedRecordTime(item) || item.receivedAt || 0) < cutoff
|
|
1362
1867
|
})
|
|
1363
1868
|
}
|
|
1364
1869
|
|
|
1365
1870
|
close () {
|
|
1366
|
-
this.
|
|
1871
|
+
if (this.closePromise) return this.closePromise
|
|
1872
|
+
const initSettledPromise = this.initSettledPromise
|
|
1873
|
+
let unwatchPromise
|
|
1874
|
+
try {
|
|
1875
|
+
unwatchPromise = Promise.resolve(this.unwatch())
|
|
1876
|
+
} catch (err) {
|
|
1877
|
+
unwatchPromise = Promise.reject(err)
|
|
1878
|
+
}
|
|
1367
1879
|
for (const pubkey of [...this.presenceTimers.keys()]) this.stopPresencePublisher(pubkey)
|
|
1368
1880
|
this.stopRelayListWatcher?.()
|
|
1369
1881
|
this.stopRelayListWatcher = null
|
|
@@ -1372,6 +1884,41 @@ export class PrivateMessenger {
|
|
|
1372
1884
|
this.stopOnline?.()
|
|
1373
1885
|
this.stopOffline = null
|
|
1374
1886
|
this.stopOnline = null
|
|
1887
|
+
this.stopStorageMaintenance()
|
|
1888
|
+
this.stopStoragePolicyBroadcast()
|
|
1889
|
+
|
|
1890
|
+
this.closePromise = (async () => {
|
|
1891
|
+
let unwatchError
|
|
1892
|
+
try { await unwatchPromise } catch (err) { unwatchError = err }
|
|
1893
|
+
await initSettledPromise
|
|
1894
|
+
await this.stampActiveChannelActivity()
|
|
1895
|
+
await this.queueOperationTail
|
|
1896
|
+
await this.stateWriteTail
|
|
1897
|
+
try { await this.storageTouchPromise } catch {}
|
|
1898
|
+
await this.storageMaintenancePromise
|
|
1899
|
+
try { await this.storagePolicyRefreshTail } catch {}
|
|
1900
|
+
await Promise.all([
|
|
1901
|
+
this.queue?.close?.(),
|
|
1902
|
+
this.seedQueue?.close?.(),
|
|
1903
|
+
this.stateStore?.close?.()
|
|
1904
|
+
])
|
|
1905
|
+
if (this.storageActive) {
|
|
1906
|
+
await releasePrivateMessengerStorage({
|
|
1907
|
+
userPubkey: this.userPubkey,
|
|
1908
|
+
leaseId: this.storageLeaseId,
|
|
1909
|
+
indexedDB: this._indexedDB
|
|
1910
|
+
})
|
|
1911
|
+
}
|
|
1912
|
+
this.storageActive = false
|
|
1913
|
+
if (this.identityStorageRetentionSeconds === 0) {
|
|
1914
|
+
await PrivateMessenger.maintainStorage({
|
|
1915
|
+
indexedDB: this._indexedDB,
|
|
1916
|
+
temporaryStorageArea: this.temporaryStorageArea
|
|
1917
|
+
})
|
|
1918
|
+
}
|
|
1919
|
+
if (unwatchError) throw unwatchError
|
|
1920
|
+
})()
|
|
1921
|
+
return this.closePromise
|
|
1375
1922
|
}
|
|
1376
1923
|
}
|
|
1377
1924
|
|
|
@@ -1452,11 +1999,9 @@ function nymCarrierSeedKey (record) {
|
|
|
1452
1999
|
|
|
1453
2000
|
function withoutQueueMetadata (item) {
|
|
1454
2001
|
if (!item) return null
|
|
1455
|
-
const {
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
...value
|
|
1459
|
-
} = item
|
|
2002
|
+
const value = { ...item }
|
|
2003
|
+
delete value[SEED_KEY]
|
|
2004
|
+
delete value[SEED_TIME]
|
|
1460
2005
|
return value
|
|
1461
2006
|
}
|
|
1462
2007
|
|