libp2r2p 0.11.28 → 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
CHANGED
|
@@ -1264,3 +1264,9 @@ and range, plus report and relay classifications; arbitrary relay text is not
|
|
|
1264
1264
|
logged. Native report causes remain available privately to recovery retry
|
|
1265
1265
|
policy. Required publication coverage, first-ACK semantics and range recovery
|
|
1266
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
|
},
|
|
@@ -106,6 +106,7 @@ export {
|
|
|
106
106
|
|
|
107
107
|
const DEFAULT_OFFLINE_RECOVERY_SECONDS = 7 * 24 * 60 * 60
|
|
108
108
|
const MAX_OFFLINE_RECOVERY_SECONDS = Math.floor(Number.MAX_SAFE_INTEGER / 1000)
|
|
109
|
+
const RECOVERY_STORAGE_BATCH_SIZE = 100
|
|
109
110
|
const STORAGE_POLICY_BROADCAST_CHANNEL = 'libp2r2p:private-messenger:storage-policy'
|
|
110
111
|
const DEFAULT_OFFLINE_SKEW_SECONDS = 30
|
|
111
112
|
const DEFAULT_RELOAD_GAP_DELAY_MS = 500
|
|
@@ -2148,9 +2149,13 @@ export class PrivateMessenger {
|
|
|
2148
2149
|
const rows = seed.recordType === NYM_CARRIER_SEED_RECORD_TYPE || seed.carriers?.length
|
|
2149
2150
|
? [{ recordType: NYM_CARRIER_SEED_RECORD_TYPE, carriers: compactSeedNymCarriers(seed.carriers) }]
|
|
2150
2151
|
: compactSeedRouterRows(seed)
|
|
2151
|
-
|
|
2152
|
+
const records = rows.map(row => {
|
|
2152
2153
|
const time = seedRecordTime(row) || receivedAt
|
|
2153
|
-
|
|
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)
|
|
2154
2159
|
}
|
|
2155
2160
|
return
|
|
2156
2161
|
}
|
|
@@ -2799,6 +2804,16 @@ export class PrivateMessenger {
|
|
|
2799
2804
|
if (this.offlineRecoverySecondsFor(channelPubkey)) {
|
|
2800
2805
|
const seen = new Set()
|
|
2801
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
|
+
}
|
|
2802
2817
|
const pending = this._pendingSeedRecords({ channelPubkey, receiverPubkey: message.event?.pubkey, since, until })
|
|
2803
2818
|
for (const seed of pending) {
|
|
2804
2819
|
if (seed.channelPubkey !== channelPubkey || (seed.receiverPubkey && seed.receiverPubkey !== message.event?.pubkey) || seed.expiresAt <= nowSeconds()) continue
|
|
@@ -2806,11 +2821,17 @@ export class PrivateMessenger {
|
|
|
2806
2821
|
if (until !== undefined && (seed.firstSeenAt || seed.router.created_at) > until) continue
|
|
2807
2822
|
seen.add(key(seed)); await packer.update(seed)
|
|
2808
2823
|
}
|
|
2824
|
+
const stored = []
|
|
2809
2825
|
for await (const seed of this.seedStorage ? this.seedStorage.iterate({ channelPubkey, receiverPubkey: message.event?.pubkey, since, until }) : this.seedQueue.storedItemsBy('byChannel', channelPubkey)) {
|
|
2810
2826
|
if (seen.has(key(seed)) || seedRecordTime(seed) < nowSeconds() - this.offlineRecoverySecondsFor(channelPubkey)) continue
|
|
2811
|
-
if (
|
|
2812
|
-
|
|
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))
|
|
2813
2833
|
}
|
|
2834
|
+
if (stored.length) await sendStored(stored)
|
|
2814
2835
|
}
|
|
2815
2836
|
await packer.finalize()
|
|
2816
2837
|
}
|