libp2r2p 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -13
- package/idb-queue/index.js +37 -11
- package/package.json +1 -1
- package/private-channel/index.js +35 -18
- package/private-channel/services/received-chunks.js +71 -30
- package/private-message/index.js +58 -13
- package/private-messenger/index.js +357 -70
- package/private-messenger/recovery/index.js +3 -1
- package/private-messenger/services/channel-state.js +33 -4
- package/private-messenger/services/storage-maintenance.js +324 -0
|
@@ -295,7 +295,8 @@ export function createMissingMessageReplyPacker ({
|
|
|
295
295
|
receiverPubkey = question?.pubkey,
|
|
296
296
|
since,
|
|
297
297
|
until,
|
|
298
|
-
eventsPerChunk = DEFAULT_EVENTS_PER_CHUNK
|
|
298
|
+
eventsPerChunk = DEFAULT_EVENTS_PER_CHUNK,
|
|
299
|
+
sendEmptyReply = false
|
|
299
300
|
}) {
|
|
300
301
|
if (!messenger?.reply) throw new Error('MESSENGER_REQUIRED')
|
|
301
302
|
if (!question?.id) throw new Error('QUESTION_REQUIRED')
|
|
@@ -311,6 +312,7 @@ export function createMissingMessageReplyPacker ({
|
|
|
311
312
|
code: MISSING_MESSAGES_REPLY_CODE,
|
|
312
313
|
payload: { since: range.since, until: range.until },
|
|
313
314
|
eventsPerChunk,
|
|
315
|
+
sendEmptyReply,
|
|
314
316
|
recordsFromInput: seed => compactRecordsFromSeed(seed, { receiverPubkey, since: range.since, until: range.until })
|
|
315
317
|
})
|
|
316
318
|
}
|
|
@@ -11,7 +11,8 @@ database `${prefix}:state:idb`, version 1
|
|
|
11
11
|
channels, keyPath "pubkey"
|
|
12
12
|
pubkey watched channel public key
|
|
13
13
|
value evolving channel-recovery state, including last-seen/watched times,
|
|
14
|
-
mode, relays, seeders, offline
|
|
14
|
+
mode, relays, seeders, effective offline-recovery retention, offline
|
|
15
|
+
ranges, active offline-range start,
|
|
15
16
|
per-seeder activity, and sent/received content-key usage
|
|
16
17
|
|
|
17
18
|
The complete channels snapshot is replaced atomically on each persisted state
|
|
@@ -79,9 +80,27 @@ function cloneChannels (channels) {
|
|
|
79
80
|
export async function createChannelStateStore ({ prefix, indexedDB = globalThis.indexedDB } = {}) {
|
|
80
81
|
if (!prefix) throw new Error('PRIVATE_MESSENGER_STATE_PREFIX_REQUIRED')
|
|
81
82
|
const db = await openDatabase(indexedDB, `${prefix}:state:idb`)
|
|
83
|
+
let closed = false
|
|
84
|
+
let activeTransactions = 0
|
|
85
|
+
let closePromise = null
|
|
86
|
+
const closeWaiters = new Set()
|
|
87
|
+
|
|
88
|
+
async function runTransaction (mode, work) {
|
|
89
|
+
if (closed) throw new Error('PRIVATE_MESSENGER_STATE_CLOSED')
|
|
90
|
+
activeTransactions++
|
|
91
|
+
try {
|
|
92
|
+
return await transaction(db, mode, work)
|
|
93
|
+
} finally {
|
|
94
|
+
activeTransactions--
|
|
95
|
+
if (!activeTransactions) {
|
|
96
|
+
for (const resolve of closeWaiters) resolve()
|
|
97
|
+
closeWaiters.clear()
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
82
101
|
|
|
83
102
|
async function load () {
|
|
84
|
-
return
|
|
103
|
+
return runTransaction('readonly', async tx => {
|
|
85
104
|
const records = (await run('getAll', [], CHANNELS_STORE, null, { tx })).result
|
|
86
105
|
return Object.fromEntries(records
|
|
87
106
|
.filter(record => typeof record?.pubkey === 'string' && record.pubkey)
|
|
@@ -91,7 +110,7 @@ export async function createChannelStateStore ({ prefix, indexedDB = globalThis.
|
|
|
91
110
|
|
|
92
111
|
async function replace (channels) {
|
|
93
112
|
const snapshot = cloneChannels(channels)
|
|
94
|
-
await
|
|
113
|
+
await runTransaction('readwrite', async tx => {
|
|
95
114
|
await run('clear', [], CHANNELS_STORE, null, { tx })
|
|
96
115
|
for (const [pubkey, value] of Object.entries(snapshot)) {
|
|
97
116
|
await run('put', [{ pubkey, value }], CHANNELS_STORE, null, { tx })
|
|
@@ -99,5 +118,15 @@ export async function createChannelStateStore ({ prefix, indexedDB = globalThis.
|
|
|
99
118
|
})
|
|
100
119
|
}
|
|
101
120
|
|
|
102
|
-
|
|
121
|
+
function close () {
|
|
122
|
+
if (closePromise) return closePromise
|
|
123
|
+
closed = true
|
|
124
|
+
closePromise = (activeTransactions
|
|
125
|
+
? new Promise(resolve => closeWaiters.add(resolve))
|
|
126
|
+
: Promise.resolve())
|
|
127
|
+
.then(() => { db.close() })
|
|
128
|
+
return closePromise
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { load, replace, close }
|
|
103
132
|
}
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { run } from '../../idb/index.js'
|
|
2
|
+
import { cleanupReceivedChunkStorage } from '../../private-channel/services/received-chunks.js'
|
|
3
|
+
import { cleanupTemporaryStorage } from '../../temporary-storage/index.js'
|
|
4
|
+
|
|
5
|
+
const REGISTRY_DATABASE = 'libp2r2p:private-messenger:registry:idb'
|
|
6
|
+
const REGISTRY_VERSION = 1
|
|
7
|
+
const BUNDLES_STORE = 'bundles'
|
|
8
|
+
const LEASES_STORE = 'leases'
|
|
9
|
+
const LOCK_NAME = 'libp2r2p:private-messenger:storage-maintenance'
|
|
10
|
+
const LEASE_MS = 2 * 60 * 60 * 1000 // 2 hours
|
|
11
|
+
const RETENTION_MS = 60 * 24 * 60 * 60 * 1000 // 60 days
|
|
12
|
+
const MAX_RETRY_MS = 5 * 60 * 1000
|
|
13
|
+
|
|
14
|
+
const localLocks = new WeakMap()
|
|
15
|
+
const retryTimers = new WeakMap()
|
|
16
|
+
|
|
17
|
+
/*
|
|
18
|
+
IndexedDB schema, shared by every PrivateMessenger instance on this origin:
|
|
19
|
+
|
|
20
|
+
database "libp2r2p:private-messenger:registry:idb", version 1
|
|
21
|
+
|
|
22
|
+
bundles, keyPath "userPubkey"
|
|
23
|
+
userPubkey primary signer public key and storage-set coordinate
|
|
24
|
+
lastUsedAt most recent durable activity in milliseconds
|
|
25
|
+
leaseUntil active-instance lease expiry in milliseconds
|
|
26
|
+
status "ready" or "delete_pending"
|
|
27
|
+
attempts consecutive failed storage-set deletions
|
|
28
|
+
nextAttemptAt earliest retry time in milliseconds
|
|
29
|
+
|
|
30
|
+
leases, keyPath "key"
|
|
31
|
+
key `${userPubkey}:${leaseId}`
|
|
32
|
+
userPubkey principal signer public key
|
|
33
|
+
leaseId per-PrivateMessenger instance identifier
|
|
34
|
+
leaseUntil instance lease expiry in milliseconds
|
|
35
|
+
|
|
36
|
+
leases indexes
|
|
37
|
+
byUserPubkey userPubkey
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
function transactionDone (tx) {
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
tx.oncomplete = () => resolve()
|
|
43
|
+
tx.onabort = () => reject(tx.error || new Error('IDB_TRANSACTION_ABORTED'))
|
|
44
|
+
tx.onerror = () => reject(tx.error || new Error('IDB_TRANSACTION_FAILED'))
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function openRegistry (indexedDB) {
|
|
49
|
+
if (!indexedDB?.open) return Promise.reject(new Error('IDB_UNAVAILABLE'))
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
const request = indexedDB.open(REGISTRY_DATABASE, REGISTRY_VERSION)
|
|
52
|
+
request.onerror = () => reject(request.error || new Error('IDB_OPEN_FAILED'))
|
|
53
|
+
request.onblocked = () => reject(new Error('IDB_DATABASE_BLOCKED'))
|
|
54
|
+
request.onupgradeneeded = () => {
|
|
55
|
+
const db = request.result
|
|
56
|
+
if (!db.objectStoreNames.contains(BUNDLES_STORE)) {
|
|
57
|
+
db.createObjectStore(BUNDLES_STORE, { keyPath: 'userPubkey' })
|
|
58
|
+
}
|
|
59
|
+
if (!db.objectStoreNames.contains(LEASES_STORE)) {
|
|
60
|
+
const leases = db.createObjectStore(LEASES_STORE, { keyPath: 'key' })
|
|
61
|
+
leases.createIndex('byUserPubkey', 'userPubkey')
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
request.onsuccess = () => {
|
|
65
|
+
const db = request.result
|
|
66
|
+
db.onversionchange = () => db.close()
|
|
67
|
+
resolve(db)
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function withRegistryTransaction (indexedDB, mode, work) {
|
|
73
|
+
const db = await openRegistry(indexedDB)
|
|
74
|
+
try {
|
|
75
|
+
const tx = db.transaction([BUNDLES_STORE, LEASES_STORE], mode)
|
|
76
|
+
const done = transactionDone(tx)
|
|
77
|
+
try {
|
|
78
|
+
const result = await work(tx)
|
|
79
|
+
await done
|
|
80
|
+
return result
|
|
81
|
+
} catch (err) {
|
|
82
|
+
try { tx.abort() } catch {}
|
|
83
|
+
try { await done } catch {}
|
|
84
|
+
throw err
|
|
85
|
+
}
|
|
86
|
+
} finally {
|
|
87
|
+
db.close()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function leaseKey (userPubkey, leaseId) {
|
|
92
|
+
return `${userPubkey}:${leaseId}`
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function readLeases (tx, userPubkey) {
|
|
96
|
+
const keyRange = globalThis.IDBKeyRange
|
|
97
|
+
const records = keyRange?.only
|
|
98
|
+
? (await run('getAll', [keyRange.only(userPubkey)], LEASES_STORE, 'byUserPubkey', { tx })).result
|
|
99
|
+
: (await run('getAll', [], LEASES_STORE, null, { tx })).result
|
|
100
|
+
return records.filter(record => record?.userPubkey === userPubkey)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function refreshLeaseUntil (tx, bundle, now, { removeExpired = false } = {}) {
|
|
104
|
+
const leases = await readLeases(tx, bundle.userPubkey)
|
|
105
|
+
let leaseUntil = 0
|
|
106
|
+
for (const lease of leases) {
|
|
107
|
+
const expiresAt = Math.max(0, Number(lease.leaseUntil) || 0)
|
|
108
|
+
if (expiresAt <= now) {
|
|
109
|
+
if (removeExpired) await run('delete', [lease.key], LEASES_STORE, null, { tx })
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
leaseUntil = Math.max(leaseUntil, expiresAt)
|
|
113
|
+
}
|
|
114
|
+
bundle.leaseUntil = leaseUntil
|
|
115
|
+
return leaseUntil
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function normalizeBundle (record) {
|
|
119
|
+
if (!record?.userPubkey) return null
|
|
120
|
+
return {
|
|
121
|
+
userPubkey: String(record.userPubkey),
|
|
122
|
+
lastUsedAt: Math.max(0, Number(record.lastUsedAt) || 0),
|
|
123
|
+
leaseUntil: Math.max(0, Number(record.leaseUntil) || 0),
|
|
124
|
+
status: record.status === 'delete_pending' ? 'delete_pending' : 'ready',
|
|
125
|
+
attempts: Math.max(0, Math.floor(Number(record.attempts) || 0)),
|
|
126
|
+
nextAttemptAt: Math.max(0, Number(record.nextAttemptAt) || 0)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function withLocalLock (indexedDB, work) {
|
|
131
|
+
const prior = localLocks.get(indexedDB) || Promise.resolve()
|
|
132
|
+
const next = prior.catch(() => {}).then(work)
|
|
133
|
+
localLocks.set(indexedDB, next.catch(() => {}))
|
|
134
|
+
return next
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function withMaintenanceLock (indexedDB, work) {
|
|
138
|
+
if (!indexedDB?.open) return Promise.reject(new Error('IDB_UNAVAILABLE'))
|
|
139
|
+
const locks = globalThis.navigator?.locks
|
|
140
|
+
if (typeof locks?.request === 'function') return locks.request(LOCK_NAME, work)
|
|
141
|
+
return withLocalLock(indexedDB, work)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function storageDatabaseNames (userPubkey) {
|
|
145
|
+
const prefix = `libp2r2p:private-messenger:${userPubkey}`
|
|
146
|
+
return [
|
|
147
|
+
`${prefix}:idb-queue`,
|
|
148
|
+
`${prefix}:seeds:idb-queue`,
|
|
149
|
+
`${prefix}:state:idb`
|
|
150
|
+
]
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function deleteDatabase (indexedDB, name) {
|
|
154
|
+
return new Promise(resolve => {
|
|
155
|
+
let request
|
|
156
|
+
try {
|
|
157
|
+
request = indexedDB.deleteDatabase(name)
|
|
158
|
+
} catch {
|
|
159
|
+
resolve(false)
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
request.onsuccess = () => resolve(true)
|
|
163
|
+
request.onerror = () => resolve(false)
|
|
164
|
+
// A blocked delete request remains live and will continue automatically
|
|
165
|
+
// after every other connection closes. Keep the maintenance lock until
|
|
166
|
+
// that definitive success/error instead of scheduling a second request.
|
|
167
|
+
request.onblocked = () => {}
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function deleteStorageSet (indexedDB, userPubkey) {
|
|
172
|
+
const results = await Promise.all(storageDatabaseNames(userPubkey)
|
|
173
|
+
.map(name => deleteDatabase(indexedDB, name)))
|
|
174
|
+
return results.every(Boolean)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function retryDelay (attempts) {
|
|
178
|
+
return Math.min(MAX_RETRY_MS, 1000 * (2 ** Math.min(9, Math.max(0, attempts - 1))))
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function markAndListDueBundles (indexedDB, now) {
|
|
182
|
+
return withRegistryTransaction(indexedDB, 'readwrite', async tx => {
|
|
183
|
+
const records = (await run('getAll', [], BUNDLES_STORE, null, { tx })).result
|
|
184
|
+
const due = []
|
|
185
|
+
for (const raw of records) {
|
|
186
|
+
const bundle = normalizeBundle(raw)
|
|
187
|
+
if (!bundle) continue
|
|
188
|
+
await refreshLeaseUntil(tx, bundle, now, { removeExpired: true })
|
|
189
|
+
const leaseActive = bundle.leaseUntil > now
|
|
190
|
+
if (bundle.status === 'ready' && !leaseActive && bundle.lastUsedAt + RETENTION_MS <= now) {
|
|
191
|
+
bundle.status = 'delete_pending'
|
|
192
|
+
bundle.attempts = 0
|
|
193
|
+
bundle.nextAttemptAt = now
|
|
194
|
+
}
|
|
195
|
+
await run('put', [bundle], BUNDLES_STORE, null, { tx })
|
|
196
|
+
if (bundle.status === 'delete_pending' && !leaseActive && bundle.nextAttemptAt <= now) {
|
|
197
|
+
due.push(bundle)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return due
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function finishDeleteAttempt (indexedDB, attempted, deleted, now) {
|
|
205
|
+
return withRegistryTransaction(indexedDB, 'readwrite', async tx => {
|
|
206
|
+
const current = normalizeBundle((await run('get', [attempted.userPubkey], BUNDLES_STORE, null, { tx })).result)
|
|
207
|
+
if (!current || current.status !== 'delete_pending' || current.leaseUntil > now) return false
|
|
208
|
+
if (deleted) {
|
|
209
|
+
for (const lease of await readLeases(tx, attempted.userPubkey)) {
|
|
210
|
+
await run('delete', [lease.key], LEASES_STORE, null, { tx })
|
|
211
|
+
}
|
|
212
|
+
await run('delete', [attempted.userPubkey], BUNDLES_STORE, null, { tx })
|
|
213
|
+
return true
|
|
214
|
+
}
|
|
215
|
+
current.attempts++
|
|
216
|
+
current.nextAttemptAt = now + retryDelay(current.attempts)
|
|
217
|
+
await run('put', [current], BUNDLES_STORE, null, { tx })
|
|
218
|
+
return false
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function maintainRegistry (indexedDB, now) {
|
|
223
|
+
const due = await markAndListDueBundles(indexedDB, now)
|
|
224
|
+
for (const bundle of due) {
|
|
225
|
+
const deleted = await deleteStorageSet(indexedDB, bundle.userPubkey)
|
|
226
|
+
await finishDeleteAttempt(indexedDB, bundle, deleted, now)
|
|
227
|
+
}
|
|
228
|
+
const nextAttemptAt = await withRegistryTransaction(indexedDB, 'readonly', async tx => {
|
|
229
|
+
const records = (await run('getAll', [], BUNDLES_STORE, null, { tx })).result
|
|
230
|
+
let earliest = Infinity
|
|
231
|
+
for (const raw of records) {
|
|
232
|
+
const bundle = normalizeBundle(raw)
|
|
233
|
+
if (bundle?.status !== 'delete_pending' || bundle.leaseUntil > now) continue
|
|
234
|
+
earliest = Math.min(earliest, bundle.nextAttemptAt)
|
|
235
|
+
}
|
|
236
|
+
return Number.isFinite(earliest) ? earliest : null
|
|
237
|
+
})
|
|
238
|
+
return { processed: due.length, nextAttemptAt }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function schedulePendingRetry (indexedDB, temporaryStorageArea, nextAttemptAt, now) {
|
|
242
|
+
const previous = retryTimers.get(indexedDB)
|
|
243
|
+
if (previous) clearTimeout(previous)
|
|
244
|
+
if (nextAttemptAt === null) {
|
|
245
|
+
retryTimers.delete(indexedDB)
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
const timer = setTimeout(() => {
|
|
249
|
+
retryTimers.delete(indexedDB)
|
|
250
|
+
maintainPrivateMessengerStorage({ indexedDB, temporaryStorageArea }).catch(() => {})
|
|
251
|
+
}, Math.min(MAX_RETRY_MS, Math.max(0, nextAttemptAt - now)))
|
|
252
|
+
timer?.unref?.()
|
|
253
|
+
retryTimers.set(indexedDB, timer)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export async function maintainPrivateMessengerStorage ({
|
|
257
|
+
indexedDB = globalThis.indexedDB,
|
|
258
|
+
temporaryStorageArea = globalThis.sessionStorage,
|
|
259
|
+
now = Date.now()
|
|
260
|
+
} = {}) {
|
|
261
|
+
if (temporaryStorageArea) cleanupTemporaryStorage({ storageArea: temporaryStorageArea })
|
|
262
|
+
await cleanupReceivedChunkStorage({ indexedDB, now })
|
|
263
|
+
const result = await withMaintenanceLock(indexedDB, () => maintainRegistry(indexedDB, now))
|
|
264
|
+
schedulePendingRetry(indexedDB, temporaryStorageArea, result.nextAttemptAt, now)
|
|
265
|
+
return result.processed
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function activatePrivateMessengerStorage ({
|
|
269
|
+
userPubkey,
|
|
270
|
+
leaseId,
|
|
271
|
+
indexedDB = globalThis.indexedDB,
|
|
272
|
+
now = Date.now()
|
|
273
|
+
} = {}) {
|
|
274
|
+
if (!userPubkey) return Promise.reject(new Error('USER_PUBKEY_REQUIRED'))
|
|
275
|
+
if (!leaseId) return Promise.reject(new Error('PRIVATE_MESSENGER_LEASE_ID_REQUIRED'))
|
|
276
|
+
return withMaintenanceLock(indexedDB, () => withRegistryTransaction(indexedDB, 'readwrite', async tx => {
|
|
277
|
+
userPubkey = String(userPubkey)
|
|
278
|
+
leaseId = String(leaseId)
|
|
279
|
+
const current = normalizeBundle((await run('get', [userPubkey], BUNDLES_STORE, null, { tx })).result)
|
|
280
|
+
await run('put', [{
|
|
281
|
+
key: leaseKey(userPubkey, leaseId),
|
|
282
|
+
userPubkey,
|
|
283
|
+
leaseId,
|
|
284
|
+
leaseUntil: now + LEASE_MS
|
|
285
|
+
}], LEASES_STORE, null, { tx })
|
|
286
|
+
const bundle = {
|
|
287
|
+
userPubkey,
|
|
288
|
+
lastUsedAt: now,
|
|
289
|
+
leaseUntil: Math.max(current?.leaseUntil || 0, now + LEASE_MS),
|
|
290
|
+
status: 'ready',
|
|
291
|
+
attempts: 0,
|
|
292
|
+
nextAttemptAt: 0
|
|
293
|
+
}
|
|
294
|
+
await run('put', [bundle], BUNDLES_STORE, null, { tx })
|
|
295
|
+
}))
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function releasePrivateMessengerStorage ({
|
|
299
|
+
userPubkey,
|
|
300
|
+
leaseId,
|
|
301
|
+
indexedDB = globalThis.indexedDB,
|
|
302
|
+
now = Date.now()
|
|
303
|
+
} = {}) {
|
|
304
|
+
if (!userPubkey) return Promise.resolve(false)
|
|
305
|
+
if (!leaseId) return Promise.reject(new Error('PRIVATE_MESSENGER_LEASE_ID_REQUIRED'))
|
|
306
|
+
return withMaintenanceLock(indexedDB, () => withRegistryTransaction(indexedDB, 'readwrite', async tx => {
|
|
307
|
+
userPubkey = String(userPubkey)
|
|
308
|
+
leaseId = String(leaseId)
|
|
309
|
+
const current = normalizeBundle((await run('get', [userPubkey], BUNDLES_STORE, null, { tx })).result)
|
|
310
|
+
if (!current) return false
|
|
311
|
+
await run('delete', [leaseKey(userPubkey, leaseId)], LEASES_STORE, null, { tx })
|
|
312
|
+
await refreshLeaseUntil(tx, current, now, { removeExpired: true })
|
|
313
|
+
current.lastUsedAt = now
|
|
314
|
+
current.status = 'ready'
|
|
315
|
+
current.attempts = 0
|
|
316
|
+
current.nextAttemptAt = 0
|
|
317
|
+
await run('put', [current], BUNDLES_STORE, null, { tx })
|
|
318
|
+
return true
|
|
319
|
+
}))
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export const PRIVATE_MESSENGER_STORAGE_HEARTBEAT_MS = 60 * 60 * 1000
|
|
323
|
+
export const PRIVATE_MESSENGER_STORAGE_MAINTENANCE_MS = 6 * 60 * 60 * 1000
|
|
324
|
+
export const PRIVATE_MESSENGER_STORAGE_RETENTION_MS = RETENTION_MS
|