libp2r2p 0.11.27 → 0.11.29
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 +13 -0
- package/package.json +1 -1
- package/private-messenger/event-store/README.md +8 -2
- package/private-messenger/event-store/index.js +50 -18
- package/private-messenger/helpers/publication.js +91 -0
- package/private-messenger/helpers/recovery-retry.js +9 -0
- package/private-messenger/index.js +36 -10
package/README.md
CHANGED
|
@@ -1257,3 +1257,16 @@ The 0.11.27 session publishes independently of local event-store persistence aft
|
|
|
1257
1257
|
its initial durable intent and exact-envelope checkpoint. `deliveryConfirmed`
|
|
1258
1258
|
and the copied `localPersistence` summary distinguish relay acceptance from
|
|
1259
1259
|
local storage. See `private-messenger/session/README.md` for retry and cleanup.
|
|
1260
|
+
|
|
1261
|
+
The 0.11.28 private messenger preserves classified publication failures for
|
|
1262
|
+
seeder recovery asks. Diagnostics include the logical ask ID, channel, seeder
|
|
1263
|
+
and range, plus report and relay classifications; arbitrary relay text is not
|
|
1264
|
+
logged. Native report causes remain available privately to recovery retry
|
|
1265
|
+
policy. Required publication coverage, first-ACK semantics and range recovery
|
|
1266
|
+
behavior are unchanged.
|
|
1267
|
+
|
|
1268
|
+
The 0.11.29 event-store recovery adapter batches immutable seed existence checks
|
|
1269
|
+
and writes in groups of at most 100. Batched checks combine one recovery-type
|
|
1270
|
+
AND filter with coordinate OR filters, preserve input order and continue to
|
|
1271
|
+
decode and validate every returned snapshot. Custom adapters without batched
|
|
1272
|
+
methods retain the sequential fallback.
|
package/package.json
CHANGED
|
@@ -18,10 +18,16 @@ without decrypting the archive; the store may also purge expired wrappers itself
|
|
|
18
18
|
|
|
19
19
|
## Contracts and ownership
|
|
20
20
|
|
|
21
|
-
- Seeds: `put(record)`, `
|
|
22
|
-
`prune({ now })`, and
|
|
21
|
+
- Seeds: `put(record)`, `putMany(records)`, `iterate(query)`, `has(record)`,
|
|
22
|
+
`hasMany(records)`, `removeLocal(query)`, `prune({ now })`, and
|
|
23
|
+
`revoke(records)`.
|
|
23
24
|
- Authorizations: the same operations plus `find(query)`, choosing the newest
|
|
24
25
|
valid share, then the shorter expiry and lexicographically smaller record ID.
|
|
26
|
+
- Batched methods preserve input order and process at most 100 coordinates per
|
|
27
|
+
query. Identical inputs share one immutable result; expired inputs return
|
|
28
|
+
`undefined` without a query or write. Batched checks use the recovery type
|
|
29
|
+
as an AND filter and the record coordinates as an OR filter, then still decode
|
|
30
|
+
and validate every returned snapshot.
|
|
25
31
|
- `iterate` is an async generator. Queries can name `recordId`, `channelPubkey`,
|
|
26
32
|
`receiverPubkey`, `since`/`until`, or the file tuple `controlChannelPubkey`,
|
|
27
33
|
`fileChannelPubkey`, `receiverPubkey`, and `indices`. `signal` cancels reads.
|
|
@@ -6,6 +6,9 @@ import { encodeRecord, decodeRecord, selector, days, PREFIX } from './helpers/co
|
|
|
6
6
|
export { encodeRecord as encodeRecoveryRecord, decodeRecord as decodeRecoveryRecord } from './helpers/codec.js'
|
|
7
7
|
|
|
8
8
|
const nowSeconds = () => Math.floor(Date.now() / 1000)
|
|
9
|
+
const RECOVERY_BATCH_SIZE = 100
|
|
10
|
+
const chunks = values => Array.from({ length: Math.ceil(values.length / RECOVERY_BATCH_SIZE) }, (_, index) =>
|
|
11
|
+
values.slice(index * RECOVERY_BATCH_SIZE, (index + 1) * RECOVERY_BATCH_SIZE))
|
|
9
12
|
function assertOptions (eventStore, signer) {
|
|
10
13
|
if (!eventStore?.query || !eventStore?.addPersonalCopy || !signer?.getPublicKey || !signer?.obfuscate || !signer?.nip44v3?.decrypt) throw new ValidationError('INVALID_EVENT_STORE_OPTIONS')
|
|
11
14
|
}
|
|
@@ -49,6 +52,11 @@ export function createPersonalCopyRecoveryStorage ({ eventStore, signer, now = n
|
|
|
49
52
|
const [owner, context] = await ready()
|
|
50
53
|
return { kinds: [1006], authors: [owner], '#k': ['30078'], '#c': [context], '#v': ['0', '1'], '&o': [await mirror('t', `${PREFIX}${type}`)] }
|
|
51
54
|
}
|
|
55
|
+
async function exactFilterFor (type) {
|
|
56
|
+
check()
|
|
57
|
+
const [owner] = await ready()
|
|
58
|
+
return { kinds: [1006], authors: [owner], '&o': [await mirror('t', `${PREFIX}${type}`)] }
|
|
59
|
+
}
|
|
52
60
|
async function read (wrapper) {
|
|
53
61
|
check()
|
|
54
62
|
// Query the store before every use; only decryption of an immutable wrapper
|
|
@@ -65,9 +73,13 @@ export function createPersonalCopyRecoveryStorage ({ eventStore, signer, now = n
|
|
|
65
73
|
return { ...decoded, wrapper }
|
|
66
74
|
}
|
|
67
75
|
async function * records (type, query = {}) {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
76
|
+
const exact = typeof query.recordId === 'string' || Array.isArray(query.recordIds)
|
|
77
|
+
const filter = exact ? await exactFilterFor(type) : await filterFor(type)
|
|
78
|
+
if (query.recordId) filter['#o'] = [await mirror('d', query.recordId)]
|
|
79
|
+
else if (Array.isArray(query.recordIds)) {
|
|
80
|
+
if (!query.recordIds.length) return
|
|
81
|
+
filter['#o'] = await Promise.all([...new Set(query.recordIds)].map(recordId => mirror('d', recordId)))
|
|
82
|
+
} else if (query.fileChannelPubkey && query.receiverPubkey) {
|
|
71
83
|
const indices = type === 'seed' ? query.indices : [undefined]
|
|
72
84
|
if (indices?.length) filter['#o'] = await Promise.all(indices.map(chunkIndex => mirror('s', selector(type, { ...query, chunkIndex }))))
|
|
73
85
|
} else if (query.channelPubkey) {
|
|
@@ -92,24 +104,44 @@ export function createPersonalCopyRecoveryStorage ({ eventStore, signer, now = n
|
|
|
92
104
|
}
|
|
93
105
|
}
|
|
94
106
|
function store (type) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
107
|
+
async function putMany (rows) {
|
|
108
|
+
check()
|
|
109
|
+
if (!Array.isArray(rows)) throw new ValidationError('INVALID_RECOVERY_RECORDS')
|
|
110
|
+
const encoded = rows.map(row => {
|
|
111
|
+
if (row.expiresAt <= now()) return null
|
|
98
112
|
const event = encodeRecord(type, row)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
113
|
+
return { event, recordId: event.tags[0][1], row }
|
|
114
|
+
})
|
|
115
|
+
const unique = new Map(encoded.filter(Boolean).map(record => [record.recordId, record]))
|
|
116
|
+
const stored = new Map()
|
|
117
|
+
for (const recordIds of chunks([...unique.keys()])) {
|
|
118
|
+
for await (const { row } of records(type, { recordIds, includeExpired: true })) stored.set(row.recordId, row)
|
|
119
|
+
}
|
|
120
|
+
for (const [recordId, record] of unique) {
|
|
121
|
+
if (stored.has(recordId)) continue
|
|
122
|
+
const saved = committed(await eventStore.addPersonalCopy(record.event, { context: '' }))
|
|
104
123
|
// A pending kind-5 tombstone can reject the insertion; do not cache it.
|
|
105
|
-
if (saved.result.stored === false && saved.result.code === 'blocked')
|
|
106
|
-
|
|
107
|
-
}
|
|
124
|
+
if (saved.result.stored === false && saved.result.code === 'blocked') stored.set(recordId, undefined)
|
|
125
|
+
else stored.set(recordId, { ...record.row, recordId })
|
|
126
|
+
}
|
|
127
|
+
return encoded.map(record => record ? stored.get(record.recordId) : undefined)
|
|
128
|
+
}
|
|
129
|
+
async function hasMany (rows) {
|
|
130
|
+
check()
|
|
131
|
+
if (!Array.isArray(rows)) throw new ValidationError('INVALID_RECOVERY_RECORDS')
|
|
132
|
+
const recordIds = rows.map(row => row.recordId || encodeRecord(type, row).tags[0][1])
|
|
133
|
+
const present = new Set()
|
|
134
|
+
for (const batch of chunks([...new Set(recordIds)])) {
|
|
135
|
+
for await (const { row } of records(type, { recordIds: batch, includeExpired: true })) present.add(row.recordId)
|
|
136
|
+
}
|
|
137
|
+
return recordIds.map(recordId => present.has(recordId))
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
async put (row) { return (await putMany([row]))[0] },
|
|
141
|
+
putMany,
|
|
108
142
|
async * iterate (query = {}) { for await (const { row } of records(type, query)) yield row },
|
|
109
|
-
async has (row) {
|
|
110
|
-
|
|
111
|
-
try { return !(await iterator.next()).done } finally { await iterator.return() }
|
|
112
|
-
},
|
|
143
|
+
async has (row) { return (await hasMany([row]))[0] },
|
|
144
|
+
hasMany,
|
|
113
145
|
async removeLocal (query = {}) {
|
|
114
146
|
for await (const { wrapper } of records(type, { ...query, includeExpired: true })) await eventStore.remove([['e', wrapper.id]])
|
|
115
147
|
},
|
|
@@ -1,5 +1,96 @@
|
|
|
1
1
|
import { getEventHash } from '../../event/index.js'
|
|
2
2
|
|
|
3
|
+
const safeTokenPattern = /^[A-Za-z0-9][A-Za-z0-9:_-]{0,159}$/
|
|
4
|
+
const safeToken = value => typeof value === 'string' && safeTokenPattern.test(value) ? value : undefined
|
|
5
|
+
const safeRelay = value => {
|
|
6
|
+
if (typeof value !== 'string') return undefined
|
|
7
|
+
try {
|
|
8
|
+
const url = new URL(value)
|
|
9
|
+
if (url.protocol !== 'ws:' && url.protocol !== 'wss:') return undefined
|
|
10
|
+
return `${url.protocol}//${url.host}`
|
|
11
|
+
} catch {
|
|
12
|
+
return undefined
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function reasonDiagnostic (reason) {
|
|
17
|
+
if (typeof reason === 'string') {
|
|
18
|
+
const prefix = /^([a-z][a-z0-9-]{0,31}):/i.exec(reason)?.[1]
|
|
19
|
+
return { code: safeToken(reason) ?? safeToken(prefix ? `${prefix}:` : undefined) ?? 'UNKNOWN' }
|
|
20
|
+
}
|
|
21
|
+
const prefix = typeof reason?.message === 'string' && /^([a-z][a-z0-9-]{0,31}):/i.exec(reason.message)?.[1]
|
|
22
|
+
const category = safeToken(reason?.category)
|
|
23
|
+
return {
|
|
24
|
+
code: safeToken(reason?.code) ?? safeToken(prefix ? `${prefix}:` : undefined) ?? category ?? 'UNKNOWN',
|
|
25
|
+
...(category ? { category } : {}),
|
|
26
|
+
...(safeToken(reason?.name) ? { name: safeToken(reason.name) } : {}),
|
|
27
|
+
...(Number.isSafeInteger(reason?.retryAt) ? { retryAt: reason.retryAt } : {}),
|
|
28
|
+
...(Number.isSafeInteger(reason?.retryAfterMs) ? { retryAfterMs: reason.retryAfterMs } : {})
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function sanitizedReport (report, index) {
|
|
33
|
+
return {
|
|
34
|
+
index,
|
|
35
|
+
...(Number.isSafeInteger(report?.total) ? { total: report.total } : {}),
|
|
36
|
+
...(Number.isSafeInteger(report?.fulfilled) ? { fulfilled: report.fulfilled } : {}),
|
|
37
|
+
...(report?.retryWhenAvailable === true ? { retryWhenAvailable: true } : {}),
|
|
38
|
+
...(report?.retryWhenOnline === true ? { retryWhenOnline: true } : {}),
|
|
39
|
+
errors: (Array.isArray(report?.errors) ? report.errors : []).map(entry => {
|
|
40
|
+
const relay = safeRelay(entry?.relay)
|
|
41
|
+
const detail = reasonDiagnostic(entry?.reason ?? entry)
|
|
42
|
+
return {
|
|
43
|
+
...(relay ? { relay } : {}),
|
|
44
|
+
...(safeToken(entry?.code) ? { ...detail, code: entry.code } : detail)
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const contextFields = ['operation', 'channelPubkey', 'seederPubkey', 'receiverPubkey', 'eventId']
|
|
51
|
+
const contextNumbers = ['eventKind', 'since', 'until']
|
|
52
|
+
|
|
53
|
+
export function sanitizePublicationError (error, context = {}) {
|
|
54
|
+
const source = error instanceof Error ? error : new Error('PRIVATE_MESSAGE_NOT_PUBLISHED')
|
|
55
|
+
const reports = Array.isArray(source.reports) ? source.reports : []
|
|
56
|
+
const retryReasons = reports.flatMap(report => (Array.isArray(report?.errors) ? report.errors : [])
|
|
57
|
+
.map(entry => entry?.reason).filter(Boolean))
|
|
58
|
+
if (source.code !== 'MESSAGE_NOT_PUBLISHED' && !reports.length) {
|
|
59
|
+
for (const key of contextFields) if (safeToken(context[key])) source[key] = context[key]
|
|
60
|
+
for (const key of contextNumbers) if (Number.isSafeInteger(context[key])) source[key] = context[key]
|
|
61
|
+
return source
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const sanitized = new Error('PRIVATE_MESSAGE_NOT_PUBLISHED')
|
|
65
|
+
if (source.stack) sanitized.stack = source.stack
|
|
66
|
+
Object.assign(sanitized, {
|
|
67
|
+
code: safeToken(source.code) ?? 'MESSAGE_NOT_PUBLISHED',
|
|
68
|
+
...(safeToken(source.reason) ? { reason: source.reason } : {}),
|
|
69
|
+
...(safeToken(source.eventId) ? { eventId: source.eventId } : {}),
|
|
70
|
+
...(Number.isSafeInteger(source.eventKind) ? { eventKind: source.eventKind } : {}),
|
|
71
|
+
...(source.retryWhenAvailable === true ? { retryWhenAvailable: true } : {}),
|
|
72
|
+
...(source.retryWhenOnline === true ? { retryWhenOnline: true } : {}),
|
|
73
|
+
reports: reports.map(sanitizedReport)
|
|
74
|
+
})
|
|
75
|
+
for (const key of contextFields) if (safeToken(context[key])) sanitized[key] = context[key]
|
|
76
|
+
for (const key of contextNumbers) if (Number.isSafeInteger(context[key])) sanitized[key] = context[key]
|
|
77
|
+
Object.defineProperty(sanitized, 'retryReasons', { value: retryReasons })
|
|
78
|
+
return sanitized
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function publicationFailureDiagnostic (error) {
|
|
82
|
+
const result = {}
|
|
83
|
+
for (const key of [...contextFields, ...contextNumbers]) {
|
|
84
|
+
const value = error?.[key]
|
|
85
|
+
if (safeToken(value) && contextFields.includes(key)) result[key] = value
|
|
86
|
+
else if (Number.isSafeInteger(value) && contextNumbers.includes(key)) result[key] = value
|
|
87
|
+
}
|
|
88
|
+
for (const key of ['code', 'reason', 'eventId']) if (safeToken(error?.[key])) result[key] = error[key]
|
|
89
|
+
for (const key of ['retryWhenAvailable', 'retryWhenOnline']) if (error?.[key] === true) result[key] = true
|
|
90
|
+
result.reports = (Array.isArray(error?.reports) ? error.reports : []).map(sanitizedReport)
|
|
91
|
+
return result
|
|
92
|
+
}
|
|
93
|
+
|
|
3
94
|
// A report describes one outer event, not one relay. Each outer event needs
|
|
4
95
|
// accepted publication covering its routed recipients. Keep the successful fast
|
|
5
96
|
// path immediate; a partially covered multi-recipient report remains a failure.
|
|
@@ -19,6 +19,11 @@ export function recoveryRetryDelay (errors, delay) {
|
|
|
19
19
|
if (['timeout', 'cutoff', 'closed'].includes(entry.status)) retry = true
|
|
20
20
|
visit(entry.error)
|
|
21
21
|
}
|
|
22
|
+
for (const report of error.reports || []) {
|
|
23
|
+
if (report.retryWhenAvailable || report.retryWhenOnline) retry = true
|
|
24
|
+
for (const entry of report.errors || []) visit(entry.reason ?? entry.error)
|
|
25
|
+
}
|
|
26
|
+
for (const reason of error.retryReasons || []) visit(reason)
|
|
22
27
|
visit(error.cause)
|
|
23
28
|
}
|
|
24
29
|
errors.forEach(visit)
|
|
@@ -36,6 +41,10 @@ export function isPermanentRecoveryError (error) {
|
|
|
36
41
|
/^(auth-required:|restricted:|blocked:|invalid:|pow:)/.test(value.message || '')) return true
|
|
37
42
|
for (const child of value.errors || []) if (visit(child)) return true
|
|
38
43
|
for (const entry of value.relays || []) if (visit(entry.error)) return true
|
|
44
|
+
for (const report of value.reports || []) {
|
|
45
|
+
for (const entry of report.errors || []) if (visit(entry.reason ?? entry.error)) return true
|
|
46
|
+
}
|
|
47
|
+
for (const reason of value.retryReasons || []) if (visit(reason)) return true
|
|
39
48
|
return visit(value.cause)
|
|
40
49
|
}
|
|
41
50
|
return visit(error)
|
|
@@ -52,6 +52,7 @@ import { backgroundSigner } from './helpers/background-signer.js'
|
|
|
52
52
|
import { createSendRelayRouting, normalizeFallbackRelays, normalizeFallbackDelay } from './helpers/send-routing.js'
|
|
53
53
|
import { incompleteFetchError } from '../private-channel/helpers/fetch-error.js'
|
|
54
54
|
import { recoveryRetryDelay, isPermanentRecoveryError } from './helpers/recovery-retry.js'
|
|
55
|
+
import { assertMessagePublished, publicationFailureDiagnostic, sanitizePublicationError } from './helpers/publication.js'
|
|
55
56
|
import { normalizeRelayUrl } from '../url/index.js'
|
|
56
57
|
import { deliveryInfo } from '../private-channel/helpers/rumor.js'
|
|
57
58
|
import { migrateMessageQueue } from './services/migrate-message-queue.js'
|
|
@@ -105,6 +106,7 @@ export {
|
|
|
105
106
|
|
|
106
107
|
const DEFAULT_OFFLINE_RECOVERY_SECONDS = 7 * 24 * 60 * 60
|
|
107
108
|
const MAX_OFFLINE_RECOVERY_SECONDS = Math.floor(Number.MAX_SAFE_INTEGER / 1000)
|
|
109
|
+
const RECOVERY_STORAGE_BATCH_SIZE = 100
|
|
108
110
|
const STORAGE_POLICY_BROADCAST_CHANNEL = 'libp2r2p:private-messenger:storage-policy'
|
|
109
111
|
const DEFAULT_OFFLINE_SKEW_SECONDS = 30
|
|
110
112
|
const DEFAULT_RELOAD_GAP_DELAY_MS = 500
|
|
@@ -2147,9 +2149,13 @@ export class PrivateMessenger {
|
|
|
2147
2149
|
const rows = seed.recordType === NYM_CARRIER_SEED_RECORD_TYPE || seed.carriers?.length
|
|
2148
2150
|
? [{ recordType: NYM_CARRIER_SEED_RECORD_TYPE, carriers: compactSeedNymCarriers(seed.carriers) }]
|
|
2149
2151
|
: compactSeedRouterRows(seed)
|
|
2150
|
-
|
|
2152
|
+
const records = rows.map(row => {
|
|
2151
2153
|
const time = seedRecordTime(row) || receivedAt
|
|
2152
|
-
|
|
2154
|
+
return { ...row, channelPubkey, receivedAt, expiresAt: time + this.offlineRecoverySecondsFor(channelPubkey) }
|
|
2155
|
+
})
|
|
2156
|
+
if (typeof this.seedStorage.putMany === 'function') await this.seedStorage.putMany(records)
|
|
2157
|
+
else {
|
|
2158
|
+
for (const row of records) await this.seedStorage.put(row)
|
|
2153
2159
|
}
|
|
2154
2160
|
return
|
|
2155
2161
|
}
|
|
@@ -2755,13 +2761,17 @@ export class PrivateMessenger {
|
|
|
2755
2761
|
payload: { since, until }
|
|
2756
2762
|
})
|
|
2757
2763
|
asks.push(ask)
|
|
2758
|
-
|
|
2759
|
-
if (!Array.isArray(reports) || !reports.length || reports.some(report => report?.success !== true)) {
|
|
2760
|
-
throw new Error('PRIVATE_MESSAGE_NOT_PUBLISHED')
|
|
2761
|
-
}
|
|
2764
|
+
await assertMessagePublished(ask, ask.question)
|
|
2762
2765
|
} catch (err) {
|
|
2763
|
-
|
|
2764
|
-
|
|
2766
|
+
const error = sanitizePublicationError(err, {
|
|
2767
|
+
operation: 'seeder-recovery-ask',
|
|
2768
|
+
channelPubkey,
|
|
2769
|
+
seederPubkey: seeder,
|
|
2770
|
+
since,
|
|
2771
|
+
until
|
|
2772
|
+
})
|
|
2773
|
+
failures.push({ seeder, error })
|
|
2774
|
+
console.warn('private-messenger seeder recovery ask failed', JSON.stringify(publicationFailureDiagnostic(error)))
|
|
2765
2775
|
}
|
|
2766
2776
|
}
|
|
2767
2777
|
return { asks, failures }
|
|
@@ -2794,6 +2804,16 @@ export class PrivateMessenger {
|
|
|
2794
2804
|
if (this.offlineRecoverySecondsFor(channelPubkey)) {
|
|
2795
2805
|
const seen = new Set()
|
|
2796
2806
|
const key = seed => seed.router ? routerSeedRowKey(seed) : nymCarrierSeedKey(seed)
|
|
2807
|
+
const sendStored = async seeds => {
|
|
2808
|
+
let present
|
|
2809
|
+
if (typeof this.seedStorage?.hasMany === 'function') present = await this.seedStorage.hasMany(seeds)
|
|
2810
|
+
else {
|
|
2811
|
+
present = []
|
|
2812
|
+
for (const seed of seeds) present.push(await this.seedStorage.has(seed))
|
|
2813
|
+
}
|
|
2814
|
+
if (!Array.isArray(present) || present.length !== seeds.length) throw new ValidationError('INVALID_RECOVERY_STORAGE_RESULT')
|
|
2815
|
+
for (let index = 0; index < seeds.length; index++) if (present[index]) await packer.update(seeds[index])
|
|
2816
|
+
}
|
|
2797
2817
|
const pending = this._pendingSeedRecords({ channelPubkey, receiverPubkey: message.event?.pubkey, since, until })
|
|
2798
2818
|
for (const seed of pending) {
|
|
2799
2819
|
if (seed.channelPubkey !== channelPubkey || (seed.receiverPubkey && seed.receiverPubkey !== message.event?.pubkey) || seed.expiresAt <= nowSeconds()) continue
|
|
@@ -2801,11 +2821,17 @@ export class PrivateMessenger {
|
|
|
2801
2821
|
if (until !== undefined && (seed.firstSeenAt || seed.router.created_at) > until) continue
|
|
2802
2822
|
seen.add(key(seed)); await packer.update(seed)
|
|
2803
2823
|
}
|
|
2824
|
+
const stored = []
|
|
2804
2825
|
for await (const seed of this.seedStorage ? this.seedStorage.iterate({ channelPubkey, receiverPubkey: message.event?.pubkey, since, until }) : this.seedQueue.storedItemsBy('byChannel', channelPubkey)) {
|
|
2805
2826
|
if (seen.has(key(seed)) || seedRecordTime(seed) < nowSeconds() - this.offlineRecoverySecondsFor(channelPubkey)) continue
|
|
2806
|
-
if (
|
|
2807
|
-
|
|
2827
|
+
if (!this.seedStorage) {
|
|
2828
|
+
await packer.update(seed)
|
|
2829
|
+
continue
|
|
2830
|
+
}
|
|
2831
|
+
stored.push(seed)
|
|
2832
|
+
if (stored.length === RECOVERY_STORAGE_BATCH_SIZE) await sendStored(stored.splice(0))
|
|
2808
2833
|
}
|
|
2834
|
+
if (stored.length) await sendStored(stored)
|
|
2809
2835
|
}
|
|
2810
2836
|
await packer.finalize()
|
|
2811
2837
|
}
|