libp2r2p 0.11.31 → 0.11.33
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 +11 -0
- package/package.json +1 -1
- package/private-messenger/event-store/README.md +7 -2
- package/private-messenger/event-store/index.js +51 -16
- package/private-messenger/index.js +59 -27
- package/private-messenger/session/README.md +2 -0
- package/private-messenger/session/helpers/foreground-work.js +30 -1
- package/private-messenger/session/index.js +12 -1
package/README.md
CHANGED
|
@@ -1284,3 +1284,14 @@ The 0.11.31 pending-decryption retry groups records by
|
|
|
1284
1284
|
remaining records only when the local signer confirms `CONTENT_KEY_NOT_FOUND`
|
|
1285
1285
|
with `hasOwnContentKey: false`. MAC, proof, parsing and operational failures are
|
|
1286
1286
|
never generalized to the group, and every deferred record remains persisted.
|
|
1287
|
+
|
|
1288
|
+
The 0.11.32 local recovery-seed queue serializes incoming and outgoing writes
|
|
1289
|
+
within one PrivateMessenger. Duplicate `bySeedKey` races retry up to five times:
|
|
1290
|
+
NYM rows remain idempotent and router rows re-read the winning timestamps before
|
|
1291
|
+
replacement. External event-store seed adapters retain their independent batched
|
|
1292
|
+
writes.
|
|
1293
|
+
|
|
1294
|
+
The 0.11.33 recovery adapter checks immutable wrapper mirrors without decrypting
|
|
1295
|
+
snapshots and serializes recovery mutations per event store. Session archives
|
|
1296
|
+
wait for foreground quiet for up to five seconds, execute one batch, then
|
|
1297
|
+
re-evaluate so pending recovery cannot monopolize local persistence.
|
package/package.json
CHANGED
|
@@ -27,8 +27,13 @@ without decrypting the archive; the store may also purge expired wrappers itself
|
|
|
27
27
|
query. Identical inputs share one immutable result. `putMany` returns
|
|
28
28
|
`undefined` for an expired input without querying or writing; `hasMany` returns
|
|
29
29
|
`false` for expired snapshots. Batched checks use the recovery type as an AND
|
|
30
|
-
filter and the record coordinates as an OR filter
|
|
31
|
-
|
|
30
|
+
filter and the record coordinates as an OR filter. Existence checks compare the
|
|
31
|
+
immutable wrapper `o` mirrors to the requested record IDs and do not decrypt or
|
|
32
|
+
decode snapshots. Duplicate `putMany` inputs return their supplied immutable row.
|
|
33
|
+
- Mutating recovery stores share one operation tail per event store, so seed and
|
|
34
|
+
grant writes cannot overlap for the same store. Writes pass the internal
|
|
35
|
+
`recovery: true` scheduling hint to `addPersonalCopy`; row-returning reads keep
|
|
36
|
+
normal decrypt-and-validate behavior.
|
|
32
37
|
- `iterate` is an async generator. Queries can name `recordId`, `channelPubkey`,
|
|
33
38
|
`receiverPubkey`, `since`/`until`, or the file tuple `controlChannelPubkey`,
|
|
34
39
|
`fileChannelPubkey`, `receiverPubkey`, and `indices`. `signal` cancels reads.
|
|
@@ -7,8 +7,19 @@ export { encodeRecord as encodeRecoveryRecord, decodeRecord as decodeRecoveryRec
|
|
|
7
7
|
|
|
8
8
|
const nowSeconds = () => Math.floor(Date.now() / 1000)
|
|
9
9
|
const RECOVERY_BATCH_SIZE = 100
|
|
10
|
+
const mutationTails = new WeakMap()
|
|
10
11
|
const chunks = values => Array.from({ length: Math.ceil(values.length / RECOVERY_BATCH_SIZE) }, (_, index) =>
|
|
11
12
|
values.slice(index * RECOVERY_BATCH_SIZE, (index + 1) * RECOVERY_BATCH_SIZE))
|
|
13
|
+
function serializeMutation (eventStore, operation) {
|
|
14
|
+
const previous = mutationTails.get(eventStore) ?? Promise.resolve()
|
|
15
|
+
const run = previous.catch(() => {}).then(operation)
|
|
16
|
+
const tail = run.catch(() => {})
|
|
17
|
+
mutationTails.set(eventStore, tail)
|
|
18
|
+
tail.then(() => {
|
|
19
|
+
if (mutationTails.get(eventStore) === tail) mutationTails.delete(eventStore)
|
|
20
|
+
})
|
|
21
|
+
return run
|
|
22
|
+
}
|
|
12
23
|
function assertOptions (eventStore, signer) {
|
|
13
24
|
if (!eventStore?.query || !eventStore?.addPersonalCopy || !signer?.getPublicKey || !signer?.obfuscate || !signer?.nip44v3?.decrypt) throw new ValidationError('INVALID_EVENT_STORE_OPTIONS')
|
|
14
25
|
}
|
|
@@ -103,8 +114,27 @@ export function createPersonalCopyRecoveryStorage ({ eventStore, signer, now = n
|
|
|
103
114
|
yield { row, wrapper }
|
|
104
115
|
}
|
|
105
116
|
}
|
|
117
|
+
async function * existingRecordIds (type, values, { includeExpired = false } = {}) {
|
|
118
|
+
const ids = [...new Set(values)]
|
|
119
|
+
if (!ids.length) return
|
|
120
|
+
const filter = await exactFilterFor(type)
|
|
121
|
+
const mirrorsById = new Map(await Promise.all(ids.map(async recordId => [await mirror('d', recordId), recordId])))
|
|
122
|
+
filter['#o'] = [...mirrorsById.keys()]
|
|
123
|
+
for await (const wrapper of pages(eventStore, filter)) {
|
|
124
|
+
const expiry = Number(wrapper.tags?.find(tag => Array.isArray(tag) && tag[0] === 'expiration')?.[1])
|
|
125
|
+
if (!includeExpired && (!Number.isSafeInteger(expiry) || expiry <= now())) continue
|
|
126
|
+
for (const tag of wrapper.tags || []) {
|
|
127
|
+
if (!Array.isArray(tag) || tag[0] !== 'o') continue
|
|
128
|
+
const recordId = mirrorsById.get(tag[1])
|
|
129
|
+
if (recordId) yield recordId
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
106
133
|
function store (type) {
|
|
107
134
|
async function putMany (rows) {
|
|
135
|
+
return serializeMutation(eventStore, () => putManyNow(rows))
|
|
136
|
+
}
|
|
137
|
+
async function putManyNow (rows) {
|
|
108
138
|
check()
|
|
109
139
|
if (!Array.isArray(rows)) throw new ValidationError('INVALID_RECOVERY_RECORDS')
|
|
110
140
|
const encoded = rows.map(row => {
|
|
@@ -115,11 +145,14 @@ export function createPersonalCopyRecoveryStorage ({ eventStore, signer, now = n
|
|
|
115
145
|
const unique = new Map(encoded.filter(Boolean).map(record => [record.recordId, record]))
|
|
116
146
|
const stored = new Map()
|
|
117
147
|
for (const recordIds of chunks([...unique.keys()])) {
|
|
118
|
-
for await (const
|
|
148
|
+
for await (const recordId of existingRecordIds(type, recordIds, { includeExpired: true })) {
|
|
149
|
+
const record = unique.get(recordId)
|
|
150
|
+
if (record) stored.set(recordId, { ...record.row, recordId })
|
|
151
|
+
}
|
|
119
152
|
}
|
|
120
153
|
for (const [recordId, record] of unique) {
|
|
121
154
|
if (stored.has(recordId)) continue
|
|
122
|
-
const saved = committed(await eventStore.addPersonalCopy(record.event, { context: '' }))
|
|
155
|
+
const saved = committed(await eventStore.addPersonalCopy(record.event, { context: '', recovery: true }))
|
|
123
156
|
// A pending kind-5 tombstone can reject the insertion; do not cache it.
|
|
124
157
|
if (saved.result.stored === false && saved.result.code === 'blocked') stored.set(recordId, undefined)
|
|
125
158
|
else stored.set(recordId, { ...record.row, recordId })
|
|
@@ -132,7 +165,7 @@ export function createPersonalCopyRecoveryStorage ({ eventStore, signer, now = n
|
|
|
132
165
|
const recordIds = rows.map(row => row.recordId || encodeRecord(type, row).tags[0][1])
|
|
133
166
|
const present = new Set()
|
|
134
167
|
for (const batch of chunks([...new Set(recordIds)])) {
|
|
135
|
-
for await (const
|
|
168
|
+
for await (const recordId of existingRecordIds(type, batch)) present.add(recordId)
|
|
136
169
|
}
|
|
137
170
|
return recordIds.map(recordId => present.has(recordId))
|
|
138
171
|
}
|
|
@@ -154,20 +187,22 @@ export function createPersonalCopyRecoveryStorage ({ eventStore, signer, now = n
|
|
|
154
187
|
}
|
|
155
188
|
},
|
|
156
189
|
async revoke (rows) {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
190
|
+
return serializeMutation(eventStore, async () => {
|
|
191
|
+
check()
|
|
192
|
+
const [owner] = await ready()
|
|
193
|
+
for (let i = 0; i < rows.length; i += 100) {
|
|
194
|
+
const group = rows.slice(i, i + 100).filter(row => row.expiresAt > now())
|
|
195
|
+
if (!group.length) continue
|
|
196
|
+
const tags = []; let createdAt = now(), expiresAt = 0
|
|
197
|
+
for (const row of group) {
|
|
198
|
+
const recordId = row.recordId || encodeRecord(type, row).tags[0][1]
|
|
199
|
+
for await (const { wrapper } of records(type, { recordId })) createdAt = Math.max(createdAt, wrapper.created_at)
|
|
200
|
+
tags.push(['a', `30078:${owner}:${recordId}`]); expiresAt = Math.max(expiresAt, row.expiresAt)
|
|
201
|
+
}
|
|
202
|
+
if (expiresAt <= createdAt) continue
|
|
203
|
+
committed(await eventStore.addPersonalCopy({ kind: 5, created_at: createdAt, tags: [...tags, ['k', '30078'], ['expiration', String(expiresAt)]], content: '' }, { context: '', recovery: true }))
|
|
167
204
|
}
|
|
168
|
-
|
|
169
|
-
committed(await eventStore.addPersonalCopy({ kind: 5, created_at: createdAt, tags: [...tags, ['k', '30078'], ['expiration', String(expiresAt)]], content: '' }, { context: '' }))
|
|
170
|
-
}
|
|
205
|
+
})
|
|
171
206
|
}
|
|
172
207
|
}
|
|
173
208
|
}
|
|
@@ -108,6 +108,7 @@ export {
|
|
|
108
108
|
const DEFAULT_OFFLINE_RECOVERY_SECONDS = 7 * 24 * 60 * 60
|
|
109
109
|
const MAX_OFFLINE_RECOVERY_SECONDS = Math.floor(Number.MAX_SAFE_INTEGER / 1000)
|
|
110
110
|
const RECOVERY_STORAGE_BATCH_SIZE = 100
|
|
111
|
+
const SEED_QUEUE_UPSERT_ATTEMPTS = 5
|
|
111
112
|
const STORAGE_POLICY_BROADCAST_CHANNEL = 'libp2r2p:private-messenger:storage-policy'
|
|
112
113
|
const DEFAULT_OFFLINE_SKEW_SECONDS = 30
|
|
113
114
|
const DEFAULT_RELOAD_GAP_DELAY_MS = 500
|
|
@@ -374,6 +375,7 @@ export class PrivateMessenger {
|
|
|
374
375
|
this.watchRevisionByChannel = new Map()
|
|
375
376
|
this.presenceTimers = new Map()
|
|
376
377
|
this.presenceJobs = new Map()
|
|
378
|
+
this.seedQueueOperationTail = Promise.resolve()
|
|
377
379
|
this.stopRelayListWatcher = null
|
|
378
380
|
this.relayListWatcherPubkey = ''
|
|
379
381
|
this.relayListRefreshPromise = null
|
|
@@ -686,6 +688,13 @@ export class PrivateMessenger {
|
|
|
686
688
|
return this.runQueueOperation(operation)
|
|
687
689
|
}
|
|
688
690
|
|
|
691
|
+
runSeedQueueOperation (operation) {
|
|
692
|
+
if (this.closePromise) return Promise.reject(new Error('PRIVATE_MESSENGER_CLOSED'))
|
|
693
|
+
const run = this.seedQueueOperationTail.then(operation)
|
|
694
|
+
this.seedQueueOperationTail = run.catch(() => {})
|
|
695
|
+
return run
|
|
696
|
+
}
|
|
697
|
+
|
|
689
698
|
debug (action, detail = {}) {
|
|
690
699
|
try {
|
|
691
700
|
this.onDebug?.({ source: 'private-messenger', action, ...detail })
|
|
@@ -2140,8 +2149,8 @@ export class PrivateMessenger {
|
|
|
2140
2149
|
|
|
2141
2150
|
async enqueueSeed (channelPubkey, seed) {
|
|
2142
2151
|
if (!this.offlineRecoverySecondsFor(channelPubkey)) return
|
|
2143
|
-
const receivedAt = nowSeconds()
|
|
2144
2152
|
if (this.seedStorage) {
|
|
2153
|
+
const receivedAt = nowSeconds()
|
|
2145
2154
|
const rows = seed.recordType === NYM_CARRIER_SEED_RECORD_TYPE || seed.carriers?.length
|
|
2146
2155
|
? [{ recordType: NYM_CARRIER_SEED_RECORD_TYPE, carriers: compactSeedNymCarriers(seed.carriers) }]
|
|
2147
2156
|
: compactSeedRouterRows(seed)
|
|
@@ -2155,22 +2164,37 @@ export class PrivateMessenger {
|
|
|
2155
2164
|
}
|
|
2156
2165
|
return
|
|
2157
2166
|
}
|
|
2167
|
+
return this.runSeedQueueOperation(() => this.enqueueQueueSeed(channelPubkey, seed))
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
async enqueueQueueSeed (channelPubkey, seed) {
|
|
2171
|
+
this.assertOpen()
|
|
2172
|
+
if (!this.channels.has(channelPubkey) || !this.offlineRecoverySecondsFor(channelPubkey)) return
|
|
2173
|
+
const receivedAt = nowSeconds()
|
|
2158
2174
|
if (seed.recordType === NYM_CARRIER_SEED_RECORD_TYPE || seed.carriers?.length) {
|
|
2159
2175
|
const carriers = compactSeedNymCarriers(seed.carriers)
|
|
2160
2176
|
const recordTime = nymCarrierRecordTime({ carriers }) || seed.outer?.created_at || receivedAt
|
|
2161
2177
|
const key = nymCarrierSeedKey({ channelPubkey, carriers })
|
|
2162
2178
|
const seedKey = key ? `nym:${key}` : ''
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2179
|
+
for (let attempt = 0; attempt < SEED_QUEUE_UPSERT_ATTEMPTS; attempt++) {
|
|
2180
|
+
if (seedKey && await this.seedQueue.someBy('bySeedKey', seedKey)) break
|
|
2181
|
+
try {
|
|
2182
|
+
await this.seedQueue.enqueue({
|
|
2183
|
+
type: 'seed',
|
|
2184
|
+
recordType: NYM_CARRIER_SEED_RECORD_TYPE,
|
|
2185
|
+
channelPubkey,
|
|
2186
|
+
receivedAt,
|
|
2187
|
+
carriers,
|
|
2188
|
+
meta: { channelPubkey: seed.channelPubkey },
|
|
2189
|
+
[SEED_KEY]: seedKey || undefined,
|
|
2190
|
+
[SEED_TIME]: recordTime
|
|
2191
|
+
})
|
|
2192
|
+
break
|
|
2193
|
+
} catch (error) {
|
|
2194
|
+
if (error?.name !== 'ConstraintError') throw error
|
|
2195
|
+
if (attempt === SEED_QUEUE_UPSERT_ATTEMPTS - 1) throw error
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2174
2198
|
await this.pruneStoredSeeds(channelPubkey)
|
|
2175
2199
|
return
|
|
2176
2200
|
}
|
|
@@ -2180,21 +2204,29 @@ export class PrivateMessenger {
|
|
|
2180
2204
|
const rowTime = row.lastSeenAt || row.router?.created_at || receivedAt
|
|
2181
2205
|
const rowKey = routerSeedRowKey({ ...row, channelPubkey })
|
|
2182
2206
|
const seedKey = `router:${rowKey}`
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2207
|
+
for (let attempt = 0; attempt < SEED_QUEUE_UPSERT_ATTEMPTS; attempt++) {
|
|
2208
|
+
const [previous] = await this.seedQueue.removeBy('bySeedKey', seedKey)
|
|
2209
|
+
const firstSeenAt = Math.min(previous?.firstSeenAt ?? rowTime, row.firstSeenAt ?? rowTime)
|
|
2210
|
+
const lastSeenAt = Math.max(previous?.lastSeenAt ?? rowTime, row.lastSeenAt ?? rowTime)
|
|
2211
|
+
try {
|
|
2212
|
+
await this.seedQueue.enqueue({
|
|
2213
|
+
...row,
|
|
2214
|
+
type: 'seed',
|
|
2215
|
+
recordType: ROUTER_SEED_RECORD_TYPE,
|
|
2216
|
+
channelPubkey,
|
|
2217
|
+
receivedAt,
|
|
2218
|
+
firstSeenAt,
|
|
2219
|
+
lastSeenAt,
|
|
2220
|
+
meta: { channelPubkey: seed.channelPubkey },
|
|
2221
|
+
[SEED_KEY]: seedKey,
|
|
2222
|
+
[SEED_TIME]: lastSeenAt || rowTime
|
|
2223
|
+
})
|
|
2224
|
+
break
|
|
2225
|
+
} catch (error) {
|
|
2226
|
+
if (error?.name !== 'ConstraintError') throw error
|
|
2227
|
+
if (attempt === SEED_QUEUE_UPSERT_ATTEMPTS - 1) throw error
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2198
2230
|
}
|
|
2199
2231
|
await this.pruneStoredSeeds(channelPubkey)
|
|
2200
2232
|
}
|
|
@@ -130,6 +130,8 @@ reevaluate a local failure without clearing another owner's storage pause.
|
|
|
130
130
|
|
|
131
131
|
`enqueue({ peer, event, context, requiredFiles, deletion })` preserves prepared
|
|
132
132
|
identities. Local message commits and remote stages have independent checkpoints.
|
|
133
|
+
Recovery archival waits up to five seconds for foreground work, then runs one
|
|
134
|
+
durable batch and re-evaluates; postponement never drops packet rows.
|
|
133
135
|
It sends original/thumbnail chunks before announcements, yields for small messages,
|
|
134
136
|
and never interprets relay rejection text as signer permission denial. Inbox ACK
|
|
135
137
|
follows durable storage; failed storage nacks and pauses ingestion. Hearsay cannot
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Local outbox persistence and user publications take precedence over the next
|
|
2
2
|
// optional presence signer call. Already-started signer calls are not recalled.
|
|
3
|
-
export function createForegroundWork () {
|
|
3
|
+
export function createForegroundWork ({ setTimer = setTimeout, clearTimer = clearTimeout } = {}) {
|
|
4
4
|
let active = 0
|
|
5
5
|
const waiters = new Set()
|
|
6
6
|
const observers = new Set()
|
|
@@ -28,6 +28,35 @@ export function createForegroundWork () {
|
|
|
28
28
|
combined.addEventListener('abort', finish, { once: true })
|
|
29
29
|
})
|
|
30
30
|
},
|
|
31
|
+
waitUntilIdle ({ maxWaitMs = 5000, signal } = {}) {
|
|
32
|
+
if (!Number.isFinite(maxWaitMs) || maxWaitMs < 0) throw new TypeError('INVALID_FOREGROUND_MAX_WAIT')
|
|
33
|
+
const combined = signal ? AbortSignal.any([signal, lifetime.signal]) : lifetime.signal
|
|
34
|
+
combined.throwIfAborted()
|
|
35
|
+
if (!active) return Promise.resolve(true)
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const timeout = { timer: undefined }
|
|
38
|
+
const cleanup = () => {
|
|
39
|
+
waiters.delete(finishIdle)
|
|
40
|
+
if (timeout.timer !== undefined) clearTimer(timeout.timer)
|
|
41
|
+
combined.removeEventListener('abort', finishAbort)
|
|
42
|
+
}
|
|
43
|
+
const finishIdle = () => {
|
|
44
|
+
cleanup()
|
|
45
|
+
resolve(true)
|
|
46
|
+
}
|
|
47
|
+
const finishAbort = () => {
|
|
48
|
+
cleanup()
|
|
49
|
+
reject(combined.reason)
|
|
50
|
+
}
|
|
51
|
+
waiters.add(finishIdle)
|
|
52
|
+
combined.addEventListener('abort', finishAbort, { once: true })
|
|
53
|
+
timeout.timer = setTimer(() => {
|
|
54
|
+
cleanup()
|
|
55
|
+
resolve(false)
|
|
56
|
+
}, maxWaitMs)
|
|
57
|
+
timeout.timer?.unref?.()
|
|
58
|
+
})
|
|
59
|
+
},
|
|
31
60
|
observe (listener) { observers.add(listener); listener(active > 0); return () => observers.delete(listener) },
|
|
32
61
|
close () { lifetime.abort(); waiters.clear(); observers.clear() }
|
|
33
62
|
}
|
|
@@ -28,6 +28,7 @@ const unavailableSend = /PRIVATE_MESSENGER_PAUSED|CHAT_UNAVAILABLE|MESSENGER_UNA
|
|
|
28
28
|
const isUnavailableSend = error => unavailableSend.test(`${error?.code || ''} ${error?.message || ''}`)
|
|
29
29
|
const SEND_RETRY_MIN_MS = 1000
|
|
30
30
|
const SEND_RETRY_MAX_MS = 30000
|
|
31
|
+
const ARCHIVE_FOREGROUND_WAIT_MS = 5000
|
|
31
32
|
// A paused or unavailable messenger must keep the outbox entry retryable
|
|
32
33
|
// instead of surfacing a send failure the user has to retry by hand.
|
|
33
34
|
const unavailableError = (error, signal) => Object.assign(new Error(error?.message || 'CHAT_UNAVAILABLE'), {
|
|
@@ -59,7 +60,7 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
59
60
|
onError(error)
|
|
60
61
|
}
|
|
61
62
|
const userSigner = messengerSigner(signer)
|
|
62
|
-
const foreground = createForegroundWork()
|
|
63
|
+
const foreground = createForegroundWork({ setTimer: _setTimeout, clearTimer: _clearTimeout })
|
|
63
64
|
messageStorage ||= createEventStoreMessageStorage({ eventStore })
|
|
64
65
|
chunkStorage ||= eventStore ? createEventStoreChunkStorage({ eventStore }) : undefined
|
|
65
66
|
const peers = new Set()
|
|
@@ -165,6 +166,16 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
165
166
|
const journal = createPublicationJournal({
|
|
166
167
|
entries, store: () => storage, onError: reportError, debug, setTimer: _setTimeout, clearTimer: _clearTimeout, random: _random,
|
|
167
168
|
archive: async (entry, rows) => {
|
|
169
|
+
const waitedAt = Date.now()
|
|
170
|
+
let idle
|
|
171
|
+
try {
|
|
172
|
+
idle = await foreground.waitUntilIdle({ maxWaitMs: ARCHIVE_FOREGROUND_WAIT_MS })
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (closed) return
|
|
175
|
+
throw error
|
|
176
|
+
}
|
|
177
|
+
if (closed) return
|
|
178
|
+
debug(entry, 'recovery-archive-window', waitedAt, { foregroundIdle: idle, rowCount: rows.length })
|
|
168
179
|
if (recoveryStorage?.seeds) {
|
|
169
180
|
if (typeof recoveryStorage.seeds.putMany === 'function') await recoveryStorage.seeds.putMany(rows)
|
|
170
181
|
else for (const row of rows) await recoveryStorage.seeds.put(row)
|