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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libp2r2p",
3
- "version": "0.11.33",
3
+ "version": "0.11.34",
4
4
  "description": "Peer-to-relay-to-peer",
5
5
  "keywords": [
6
6
  "p2r2p",
@@ -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 internal
35
- `recovery: true` scheduling hint to `addPersonalCopy`; row-returning reads keep
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({ ...filter, ...(until === undefined ? {} : { until }), ...(excluded.length ? { '!ids': excluded } : {}), limit: 128, search: 'sort:desc' })
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') stored.set(recordId, undefined)
158
- else stored.set(recordId, { ...record.row, recordId })
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 for (const row of rows) await recoveryStorage.seeds.put(row)
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()