libp2r2p 0.11.33 → 0.11.34
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
|
@@ -1295,3 +1295,9 @@ The 0.11.33 recovery adapter checks immutable wrapper mirrors without decrypting
|
|
|
1295
1295
|
snapshots and serializes recovery mutations per event store. Session archives
|
|
1296
1296
|
wait for foreground quiet for up to five seconds, execute one batch, then
|
|
1297
1297
|
re-evaluate so pending recovery cannot monopolize local persistence.
|
|
1298
|
+
|
|
1299
|
+
The 0.11.34 recovery adapter marks paged event-store reads with the internal
|
|
1300
|
+
`recovery: true` hint so a compatible launcher can prioritize foreground writes
|
|
1301
|
+
over new background read transactions. `putMany` also accepts an optional
|
|
1302
|
+
development diagnostic callback with aggregate existing/inserted/skipped counts;
|
|
1303
|
+
it never exposes ciphertext or record payloads.
|
package/package.json
CHANGED
|
@@ -30,10 +30,18 @@ without decrypting the archive; the store may also purge expired wrappers itself
|
|
|
30
30
|
filter and the record coordinates as an OR filter. Existence checks compare the
|
|
31
31
|
immutable wrapper `o` mirrors to the requested record IDs and do not decrypt or
|
|
32
32
|
decode snapshots. Duplicate `putMany` inputs return their supplied immutable row.
|
|
33
|
+
- Recovery pages pass the optional internal `{ recovery: true }` query hint.
|
|
34
|
+
Launchers may use it to admit the readonly transaction as background without
|
|
35
|
+
granting an app elevated priority. Stores that do not recognize the hint keep
|
|
36
|
+
the previous query behavior.
|
|
37
|
+
- `putMany(rows, { onDiagnostic })` accepts an optional development callback. One
|
|
38
|
+
sanitized report identifies row, unique, existing, inserted, blocked, expired,
|
|
39
|
+
stored and skipped counts after each batch. It never includes ciphertext,
|
|
40
|
+
decoded rows, record payloads or signer material.
|
|
33
41
|
- 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
|
|
35
|
-
`recovery: true` scheduling hint to
|
|
36
|
-
normal decrypt-and-validate behavior.
|
|
42
|
+
grant writes cannot overlap for the same store. Writes and paged reads pass the
|
|
43
|
+
internal `recovery: true` scheduling hint to the corresponding event-store
|
|
44
|
+
methods; row-returning reads keep normal decrypt-and-validate behavior.
|
|
37
45
|
- `iterate` is an async generator. Queries can name `recordId`, `channelPubkey`,
|
|
38
46
|
`receiverPubkey`, `since`/`until`, or the file tuple `controlChannelPubkey`,
|
|
39
47
|
`fileChannelPubkey`, `receiverPubkey`, and `indices`. `signal` cancels reads.
|
|
@@ -7,6 +7,7 @@ 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 RECOVERY_QUERY_OPTIONS = Object.freeze({ recovery: true })
|
|
10
11
|
const mutationTails = new WeakMap()
|
|
11
12
|
const chunks = values => Array.from({ length: Math.ceil(values.length / RECOVERY_BATCH_SIZE) }, (_, index) =>
|
|
12
13
|
values.slice(index * RECOVERY_BATCH_SIZE, (index + 1) * RECOVERY_BATCH_SIZE))
|
|
@@ -32,7 +33,10 @@ async function * pages (eventStore, filter, signal) {
|
|
|
32
33
|
let until = filter.until, excluded = []
|
|
33
34
|
while (true) {
|
|
34
35
|
signal?.throwIfAborted()
|
|
35
|
-
const { results } = await eventStore.query(
|
|
36
|
+
const { results } = await eventStore.query(
|
|
37
|
+
{ ...filter, ...(until === undefined ? {} : { until }), ...(excluded.length ? { '!ids': excluded } : {}), limit: 128, search: 'sort:desc' },
|
|
38
|
+
RECOVERY_QUERY_OPTIONS
|
|
39
|
+
)
|
|
36
40
|
if (!results.length) return
|
|
37
41
|
for (const event of results) { signal?.throwIfAborted(); yield event }
|
|
38
42
|
const boundary = results.at(-1).created_at
|
|
@@ -131,10 +135,10 @@ export function createPersonalCopyRecoveryStorage ({ eventStore, signer, now = n
|
|
|
131
135
|
}
|
|
132
136
|
}
|
|
133
137
|
function store (type) {
|
|
134
|
-
async function putMany (rows) {
|
|
135
|
-
return serializeMutation(eventStore, () => putManyNow(rows))
|
|
138
|
+
async function putMany (rows, options = {}) {
|
|
139
|
+
return serializeMutation(eventStore, () => putManyNow(rows, options))
|
|
136
140
|
}
|
|
137
|
-
async function putManyNow (rows) {
|
|
141
|
+
async function putManyNow (rows, { onDiagnostic } = {}) {
|
|
138
142
|
check()
|
|
139
143
|
if (!Array.isArray(rows)) throw new ValidationError('INVALID_RECOVERY_RECORDS')
|
|
140
144
|
const encoded = rows.map(row => {
|
|
@@ -150,13 +154,35 @@ export function createPersonalCopyRecoveryStorage ({ eventStore, signer, now = n
|
|
|
150
154
|
if (record) stored.set(recordId, { ...record.row, recordId })
|
|
151
155
|
}
|
|
152
156
|
}
|
|
157
|
+
const existingCount = stored.size
|
|
158
|
+
let insertedCount = 0
|
|
159
|
+
let blockedCount = 0
|
|
153
160
|
for (const [recordId, record] of unique) {
|
|
154
161
|
if (stored.has(recordId)) continue
|
|
155
162
|
const saved = committed(await eventStore.addPersonalCopy(record.event, { context: '', recovery: true }))
|
|
156
163
|
// A pending kind-5 tombstone can reject the insertion; do not cache it.
|
|
157
|
-
if (saved.result.stored === false && saved.result.code === 'blocked')
|
|
158
|
-
|
|
164
|
+
if (saved.result.stored === false && saved.result.code === 'blocked') {
|
|
165
|
+
stored.set(recordId, undefined)
|
|
166
|
+
blockedCount++
|
|
167
|
+
} else {
|
|
168
|
+
stored.set(recordId, { ...record.row, recordId })
|
|
169
|
+
insertedCount++
|
|
170
|
+
}
|
|
159
171
|
}
|
|
172
|
+
const expiredCount = encoded.filter(record => record === null).length
|
|
173
|
+
try {
|
|
174
|
+
onDiagnostic?.({
|
|
175
|
+
type,
|
|
176
|
+
rowCount: rows.length,
|
|
177
|
+
uniqueCount: unique.size,
|
|
178
|
+
existingCount,
|
|
179
|
+
insertedCount,
|
|
180
|
+
blockedCount,
|
|
181
|
+
expiredCount,
|
|
182
|
+
storedCount: existingCount + insertedCount,
|
|
183
|
+
skippedCount: existingCount + blockedCount + expiredCount
|
|
184
|
+
})
|
|
185
|
+
} catch {}
|
|
160
186
|
return encoded.map(record => record ? stored.get(record.recordId) : undefined)
|
|
161
187
|
}
|
|
162
188
|
async function hasMany (rows) {
|
|
@@ -270,6 +270,10 @@ event/peer identifiers and elapsed milliseconds for admission, local processing,
|
|
|
270
270
|
channel preparation, publication acceptance and cleanup. Callback failures never
|
|
271
271
|
change delivery. Connect only in development; never log events or crypto buffers.
|
|
272
272
|
Custom outbox factories also receive this optional callback for storage timings.
|
|
273
|
+
Recovery archive completion also reports `publicationId`, one aggregate
|
|
274
|
+
`recovery-archive-result` per `putMany`, and sanitized row/unique/existing/
|
|
275
|
+
inserted/blocked/expired/stored/skipped counts. A single-row batch may include
|
|
276
|
+
its opaque `seedRecordId`.
|
|
273
277
|
|
|
274
278
|
## Durable prepared publications
|
|
275
279
|
|
|
@@ -175,15 +175,40 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
175
175
|
throw error
|
|
176
176
|
}
|
|
177
177
|
if (closed) return
|
|
178
|
+
const archiveStartedAt = Date.now()
|
|
178
179
|
debug(entry, 'recovery-archive-window', waitedAt, { foregroundIdle: idle, rowCount: rows.length })
|
|
180
|
+
let archived
|
|
181
|
+
let diagnosticReported = false
|
|
182
|
+
const onDiagnostic = details => {
|
|
183
|
+
diagnosticReported = true
|
|
184
|
+
debug(entry, 'recovery-archive-result', archiveStartedAt, {
|
|
185
|
+
publicationId: entry.prepared.publication.events[0].id,
|
|
186
|
+
...details
|
|
187
|
+
})
|
|
188
|
+
}
|
|
179
189
|
if (recoveryStorage?.seeds) {
|
|
180
|
-
if (typeof recoveryStorage.seeds.putMany === 'function') await recoveryStorage.seeds.putMany(rows)
|
|
181
|
-
else
|
|
190
|
+
if (typeof recoveryStorage.seeds.putMany === 'function') archived = await recoveryStorage.seeds.putMany(rows, { onDiagnostic })
|
|
191
|
+
else {
|
|
192
|
+
archived = []
|
|
193
|
+
for (const row of rows) archived.push(await recoveryStorage.seeds.put(row))
|
|
194
|
+
}
|
|
182
195
|
} else if (entry.prepared.target.file) {
|
|
183
196
|
for (const seed of entry.prepared.publication.seeds) await fileTransfers.archiveSeed(entry.prepared.target.file, seed)
|
|
184
197
|
} else {
|
|
185
198
|
for (const seed of entry.prepared.publication.seeds) await messenger.enqueueSeed(seed.channelPubkey, seed)
|
|
186
199
|
}
|
|
200
|
+
if (!diagnosticReported) {
|
|
201
|
+
const results = Array.isArray(archived) ? archived : []
|
|
202
|
+
const storedCount = results.filter(Boolean).length
|
|
203
|
+
debug(entry, 'recovery-archive-result', archiveStartedAt, {
|
|
204
|
+
publicationId: entry.prepared.publication.events[0].id,
|
|
205
|
+
rowCount: rows.length,
|
|
206
|
+
uniqueCount: new Set(rows.map(row => row.recordId).filter(Boolean)).size,
|
|
207
|
+
storedCount,
|
|
208
|
+
skippedCount: results.length ? results.length - storedCount : undefined,
|
|
209
|
+
...(rows.length === 1 && rows[0].recordId ? { seedRecordId: rows[0].recordId } : {})
|
|
210
|
+
})
|
|
211
|
+
}
|
|
187
212
|
},
|
|
188
213
|
changed: entry => {
|
|
189
214
|
watchOfflineSends()
|