libp2r2p 0.11.33 → 0.11.35
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,15 @@ 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.
|
|
1304
|
+
|
|
1305
|
+
The 0.11.35 session lets self-chat local persistence and its initial encrypted
|
|
1306
|
+
outbox checkpoint run concurrently. The entry remains hidden and ineligible for
|
|
1307
|
+
saved/publication/cleanup until admission; remote entries keep checkpoint-first
|
|
1308
|
+
ordering. A failed local commit after admission remains retryable through the
|
|
1309
|
+
existing outbox path.
|
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) {
|
|
@@ -251,13 +251,19 @@ creates a fresh publication lifetime, without treating the pause as a user abort
|
|
|
251
251
|
## Independent local persistence and publication
|
|
252
252
|
|
|
253
253
|
One encrypted outbox feeds two independent serial processors. Local persistence
|
|
254
|
-
never waits for peer preparation or an in-flight publication.
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
254
|
+
never waits for peer preparation or an in-flight publication. Remote entries
|
|
255
|
+
join those processors only after their initial encrypted intent commits, while
|
|
256
|
+
self entries may start local persistence alongside that checkpoint. An
|
|
257
|
+
in-flight self entry remains internal until admission: it is absent from
|
|
258
|
+
`onOutbox`, cannot publish or clean up, and cannot report saved. Remote
|
|
259
|
+
preparation and publication never wait for event-store persistence. Only the
|
|
260
|
+
exact prepared-envelope checkpoint gates transmission. Text continues passing
|
|
261
|
+
uploads between chunks; its local stage need not have completed. Both stages use
|
|
262
|
+
the admitted inner ID.
|
|
263
|
+
|
|
264
|
+
Self entries emit `status: 'saved'` once local persistence succeeds and the
|
|
265
|
+
initial encrypted checkpoint admits the entry. They do not write a redundant
|
|
266
|
+
localSaved checkpoint before removal. Remote entries
|
|
261
267
|
emit that status after the first valid relay ACK for every required publication,
|
|
262
268
|
without waiting for redundant relays. Outbox cleanup runs independently; failures
|
|
263
269
|
retain saved status and retry only removal with the session backoff, never
|
|
@@ -270,6 +276,10 @@ event/peer identifiers and elapsed milliseconds for admission, local processing,
|
|
|
270
276
|
channel preparation, publication acceptance and cleanup. Callback failures never
|
|
271
277
|
change delivery. Connect only in development; never log events or crypto buffers.
|
|
272
278
|
Custom outbox factories also receive this optional callback for storage timings.
|
|
279
|
+
Recovery archive completion also reports `publicationId`, one aggregate
|
|
280
|
+
`recovery-archive-result` per `putMany`, and sanitized row/unique/existing/
|
|
281
|
+
inserted/blocked/expired/stored/skipped counts. A single-row batch may include
|
|
282
|
+
its opaque `seedRecordId`.
|
|
273
283
|
|
|
274
284
|
## Durable prepared publications
|
|
275
285
|
|
|
@@ -67,6 +67,12 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
67
67
|
const channels = new Map()
|
|
68
68
|
const entries = new Map()
|
|
69
69
|
const cancelled = new Set()
|
|
70
|
+
// Self entries can persist locally while their encrypted checkpoint is in
|
|
71
|
+
// flight, but they are not visible to consumers until that checkpoint wins.
|
|
72
|
+
const unadmitted = new WeakSet()
|
|
73
|
+
const admissionFailed = new WeakSet()
|
|
74
|
+
const localWorks = new WeakMap()
|
|
75
|
+
const localErrors = new WeakMap()
|
|
70
76
|
const attemptStarts = new Map()
|
|
71
77
|
const networkUrgency = new Map()
|
|
72
78
|
const sendControllers = new Map()
|
|
@@ -106,14 +112,14 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
106
112
|
for (const entry of entries.values()) {
|
|
107
113
|
if (entry.peer !== owner && entry.failed && entry.status === 'pending') pausedSends.add(entry.id)
|
|
108
114
|
}
|
|
109
|
-
if (![...entries.values()].some(entry => (entry.localFailed && entry.localRetryable) || (pendingRetryable(entry) && !pausedSends.has(entry.id)))) clearSendRetry()
|
|
115
|
+
if (![...entries.values()].some(entry => !unadmitted.has(entry) && ((entry.localFailed && entry.localRetryable) || (pendingRetryable(entry) && !pausedSends.has(entry.id))))) clearSendRetry()
|
|
110
116
|
watchOfflineSends()
|
|
111
117
|
return
|
|
112
118
|
}
|
|
113
119
|
clearSendRetry()
|
|
114
120
|
for (const id of pausedSends) { const entry = entries.get(id); if (entry) { entry.failed = false; attemptStarts.set(entry.id, performance.now()) } }
|
|
115
121
|
pausedSends.clear()
|
|
116
|
-
if ([...entries.values()].some(entry => entry.localFailed && entry.localRetryable)) scheduleSendRetry()
|
|
122
|
+
if ([...entries.values()].some(entry => !unadmitted.has(entry) && entry.localFailed && entry.localRetryable)) scheduleSendRetry()
|
|
117
123
|
watchOfflineSends()
|
|
118
124
|
// The callback can run inside construction/resume, before configure settles.
|
|
119
125
|
queueMicrotask(() => pump())
|
|
@@ -158,8 +164,8 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
158
164
|
const idleWaiters = new Set()
|
|
159
165
|
const settled = () => { if (!sending && !localSaving && !enqueuing && !draining && !cleanups.size) { for (const resolve of idleWaiters) resolve(); idleWaiters.clear() } }
|
|
160
166
|
const localComplete = entry => entry.localSaved.every(Boolean)
|
|
161
|
-
const completed = entry => localComplete(entry) && (entry.peer === owner || entry.deliveryConfirmed)
|
|
162
|
-
const emit = () => onOutbox([...entries.values()].filter(entry => !entry.deletion && !entry.cancelled).map(entry => ({
|
|
167
|
+
const completed = entry => !unadmitted.has(entry) && localComplete(entry) && (entry.peer === owner || entry.deliveryConfirmed)
|
|
168
|
+
const emit = () => onOutbox([...entries.values()].filter(entry => !unadmitted.has(entry) && !entry.deletion && !entry.cancelled).map(entry => ({
|
|
163
169
|
...entry,
|
|
164
170
|
localPersistence: { state: localComplete(entry) ? 'saved' : entry.localFailed ? 'failed' : 'pending', retryable: Boolean(entry.localFailed && entry.localRetryable && !localRetries.has(entry.id)) }
|
|
165
171
|
})))
|
|
@@ -175,15 +181,40 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
175
181
|
throw error
|
|
176
182
|
}
|
|
177
183
|
if (closed) return
|
|
184
|
+
const archiveStartedAt = Date.now()
|
|
178
185
|
debug(entry, 'recovery-archive-window', waitedAt, { foregroundIdle: idle, rowCount: rows.length })
|
|
186
|
+
let archived
|
|
187
|
+
let diagnosticReported = false
|
|
188
|
+
const onDiagnostic = details => {
|
|
189
|
+
diagnosticReported = true
|
|
190
|
+
debug(entry, 'recovery-archive-result', archiveStartedAt, {
|
|
191
|
+
publicationId: entry.prepared.publication.events[0].id,
|
|
192
|
+
...details
|
|
193
|
+
})
|
|
194
|
+
}
|
|
179
195
|
if (recoveryStorage?.seeds) {
|
|
180
|
-
if (typeof recoveryStorage.seeds.putMany === 'function') await recoveryStorage.seeds.putMany(rows)
|
|
181
|
-
else
|
|
196
|
+
if (typeof recoveryStorage.seeds.putMany === 'function') archived = await recoveryStorage.seeds.putMany(rows, { onDiagnostic })
|
|
197
|
+
else {
|
|
198
|
+
archived = []
|
|
199
|
+
for (const row of rows) archived.push(await recoveryStorage.seeds.put(row))
|
|
200
|
+
}
|
|
182
201
|
} else if (entry.prepared.target.file) {
|
|
183
202
|
for (const seed of entry.prepared.publication.seeds) await fileTransfers.archiveSeed(entry.prepared.target.file, seed)
|
|
184
203
|
} else {
|
|
185
204
|
for (const seed of entry.prepared.publication.seeds) await messenger.enqueueSeed(seed.channelPubkey, seed)
|
|
186
205
|
}
|
|
206
|
+
if (!diagnosticReported) {
|
|
207
|
+
const results = Array.isArray(archived) ? archived : []
|
|
208
|
+
const storedCount = results.filter(Boolean).length
|
|
209
|
+
debug(entry, 'recovery-archive-result', archiveStartedAt, {
|
|
210
|
+
publicationId: entry.prepared.publication.events[0].id,
|
|
211
|
+
rowCount: rows.length,
|
|
212
|
+
uniqueCount: new Set(rows.map(row => row.recordId).filter(Boolean)).size,
|
|
213
|
+
storedCount,
|
|
214
|
+
skippedCount: results.length ? results.length - storedCount : undefined,
|
|
215
|
+
...(rows.length === 1 && rows[0].recordId ? { seedRecordId: rows[0].recordId } : {})
|
|
216
|
+
})
|
|
217
|
+
}
|
|
187
218
|
},
|
|
188
219
|
changed: entry => {
|
|
189
220
|
watchOfflineSends()
|
|
@@ -459,6 +490,7 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
459
490
|
await assertMessagePublished(report, event)
|
|
460
491
|
}
|
|
461
492
|
async function cleanupEntry (entry) {
|
|
493
|
+
if (unadmitted.has(entry)) return
|
|
462
494
|
if (entry.prepared) { journal.wake(); return }
|
|
463
495
|
if (!entry.cancelled && !completed(entry)) return
|
|
464
496
|
if (entry.failed && entry.status === 'saved') return
|
|
@@ -478,10 +510,11 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
478
510
|
return work
|
|
479
511
|
}
|
|
480
512
|
async function saveEntry (entry) {
|
|
481
|
-
if (entry.peer === owner) { entry.status = 'pending'; emit() }
|
|
513
|
+
if (entry.peer === owner && !unadmitted.has(entry)) { entry.status = 'pending'; emit() }
|
|
482
514
|
const releaseForeground = foreground.enter()
|
|
483
515
|
const active = async () => {
|
|
484
|
-
if (
|
|
516
|
+
if (admissionFailed.has(entry)) throw new Error('CHAT_UNAVAILABLE')
|
|
517
|
+
if (!unadmitted.has(entry) && storage.has && !await storage.has(entry.id)) { cancelled.add(entry.id); sendControllers.get(entry.id)?.abort() }
|
|
485
518
|
if (closed || !available || cancelled.has(entry.id) || (entry.peer !== owner && !peers.has(entry.peer))) throw new Error('CHAT_UNAVAILABLE')
|
|
486
519
|
}
|
|
487
520
|
const startedAt = Date.now()
|
|
@@ -499,12 +532,18 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
499
532
|
}
|
|
500
533
|
entry.localFailed = false
|
|
501
534
|
debug(entry, 'local-stored', startedAt)
|
|
535
|
+
if (unadmitted.has(entry)) return
|
|
502
536
|
if (entry.peer === owner) entry.status = 'saved'
|
|
503
537
|
emit()
|
|
504
538
|
if (completed(entry)) cleanupEntry(entry)
|
|
505
539
|
else await journal.write(entry)
|
|
506
540
|
} catch (error) {
|
|
541
|
+
if (admissionFailed.has(entry)) return
|
|
507
542
|
if (cancelled.has(entry.id)) { sendControllers.get(entry.id)?.abort(); entry.cancelled = true; cleanupEntry(entry) } else if (!closed && available) {
|
|
543
|
+
if (unadmitted.has(entry)) {
|
|
544
|
+
entry.localFailed = true; entry.localRetryable = retryable(error); localErrors.set(entry, error)
|
|
545
|
+
return
|
|
546
|
+
}
|
|
508
547
|
entry.localFailed = true; entry.localRetryable = retryable(error)
|
|
509
548
|
if (entry.peer === owner) { entry.status = 'error'; entry.retryable = entry.localRetryable }
|
|
510
549
|
await journal.write(entry).catch(reportError)
|
|
@@ -628,7 +667,7 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
628
667
|
clearSendRetry()
|
|
629
668
|
sendRetryDelay = SEND_RETRY_MIN_MS
|
|
630
669
|
}
|
|
631
|
-
const pendingRetryable = entry => Boolean(!cancelled.has(entry.id) && ((entry.localFailed && entry.localRetryable) || (entry.failed && (entry.retryable || entry.status === 'pending'))))
|
|
670
|
+
const pendingRetryable = entry => Boolean(!unadmitted.has(entry) && !cancelled.has(entry.id) && ((entry.localFailed && entry.localRetryable) || (entry.failed && (entry.retryable || entry.status === 'pending'))))
|
|
632
671
|
function scheduleSendRetry () {
|
|
633
672
|
if (closed || !available || sendRetryTimer) return
|
|
634
673
|
let nextRetryAt = 0
|
|
@@ -716,14 +755,21 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
716
755
|
relocal = false
|
|
717
756
|
for (const entry of entries.values()) {
|
|
718
757
|
if (closed || !available) break
|
|
758
|
+
if (admissionFailed.has(entry)) continue
|
|
719
759
|
if ((entry.localFailed && !localRetries.has(entry.id)) || cancelled.has(entry.id) || (entry.peer !== owner && !peers.has(entry.peer))) continue
|
|
720
|
-
if (localComplete(entry) && !entry.localFailed) {
|
|
721
|
-
|
|
760
|
+
if (localComplete(entry) && !entry.localFailed) {
|
|
761
|
+
if (!unadmitted.has(entry) && entry.peer === owner && entry.status !== 'saved') await saveEntry(entry)
|
|
762
|
+
else if (entry.status === 'saved') cleanupEntry(entry)
|
|
763
|
+
continue
|
|
764
|
+
}
|
|
765
|
+
const work = saveEntry(entry)
|
|
766
|
+
localWorks.set(entry, work)
|
|
767
|
+
try { await work } finally { localWorks.delete(entry) }
|
|
722
768
|
pumpRemote()
|
|
723
769
|
}
|
|
724
770
|
} while (relocal)
|
|
725
771
|
} finally {
|
|
726
|
-
if ([...entries.values()].some(entry => (entry.localFailed && entry.localRetryable) || (entry.status === 'saved' && entry.failed && entry.retryable))) scheduleSendRetry()
|
|
772
|
+
if ([...entries.values()].some(entry => !unadmitted.has(entry) && ((entry.localFailed && entry.localRetryable) || (entry.status === 'saved' && entry.failed && entry.retryable)))) scheduleSendRetry()
|
|
727
773
|
localSaving = false; for (const resolve of localIdleWaiters) resolve(); localIdleWaiters.clear(); settled()
|
|
728
774
|
}
|
|
729
775
|
}
|
|
@@ -736,7 +782,7 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
736
782
|
repump = false
|
|
737
783
|
for (const entry of entries.values()) {
|
|
738
784
|
if (closed || !available) break
|
|
739
|
-
if (entry.prepared?.accepted) { journal.wake(); continue }
|
|
785
|
+
if (unadmitted.has(entry) || entry.prepared?.accepted) { if (entry.prepared?.accepted) journal.wake(); continue }
|
|
740
786
|
if (entry.peer === owner || entry.status === 'saved' || !peers.has(entry.peer) || entry.failed || cancelled.has(entry.id)) continue
|
|
741
787
|
if (!preparation.get(entry.peer) && !preparation.error(entry.peer)) {
|
|
742
788
|
preparation.request(entry.peer).catch(() => {})
|
|
@@ -754,6 +800,12 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
754
800
|
}
|
|
755
801
|
}
|
|
756
802
|
async function pump () { await Promise.all([pumpLocal(), pumpRemote()]) }
|
|
803
|
+
async function discardUnadmitted (entry) {
|
|
804
|
+
admissionFailed.add(entry)
|
|
805
|
+
unadmitted.delete(entry)
|
|
806
|
+
if (entries.get(entry.id) === entry) entries.delete(entry.id)
|
|
807
|
+
await localWorks.get(entry)
|
|
808
|
+
}
|
|
757
809
|
const intentId = file => getEventHash({ kind: 0, pubkey: owner, created_at: 0, tags: [], content: `${file.peer}:${file.root}` })
|
|
758
810
|
async function download (file, options = {}) {
|
|
759
811
|
if (file.peer === owner) return file
|
|
@@ -879,7 +931,7 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
879
931
|
journal.setAvailable(true)
|
|
880
932
|
pumpLocal()
|
|
881
933
|
for (const entry of entries.values()) {
|
|
882
|
-
if (entry.localRetryable) localRetries.add(entry.id)
|
|
934
|
+
if (!unadmitted.has(entry) && entry.localRetryable) localRetries.add(entry.id)
|
|
883
935
|
if (entry.failed && (entry.retryable || entry.status === 'pending')) { entry.failed = false; attemptStarts.set(entry.id, performance.now()) }
|
|
884
936
|
}
|
|
885
937
|
if (storageBlocked) inboxRecovery.start({ immediate: true })
|
|
@@ -901,17 +953,45 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
901
953
|
const files = events.filter(value => value.kind === 1063).map(value => ({ ...decodeFileMetadata(value), optional: !requiredFiles.includes(getEventHash(value)) })).filter(file => file.service === 'irfs').flatMap(file => [file, ...(file.thumbnail?.root ? [{ ...file.thumbnail, optional: file.optional }] : [])])
|
|
902
954
|
const entry = { id, peer, event: { ...main, id }, events, files, index: 0, fileIndex: 0, chunkIndex: 0, localSaved: context.map(() => true).concat(false), status: 'pending', deliveryConfirmed: false, deletion }
|
|
903
955
|
const startedAt = Date.now()
|
|
956
|
+
const earlyLocal = peer === owner
|
|
957
|
+
if (earlyLocal) {
|
|
958
|
+
queuedAt.set(entry, Date.now())
|
|
959
|
+
unadmitted.add(entry)
|
|
960
|
+
entries.set(id, entry)
|
|
961
|
+
pumpLocal()
|
|
962
|
+
}
|
|
904
963
|
debug(entry, 'outbox-start')
|
|
905
|
-
|
|
964
|
+
try {
|
|
965
|
+
await storage.put(entry)
|
|
966
|
+
} catch (error) {
|
|
967
|
+
if (earlyLocal) await discardUnadmitted(entry)
|
|
968
|
+
throw error
|
|
969
|
+
}
|
|
906
970
|
debug(entry, 'outbox-stored', startedAt)
|
|
907
971
|
queuedAt.set(entry, Date.now())
|
|
908
972
|
if (attemptStartedAt !== undefined) attemptStarts.set(id, attemptStartedAt)
|
|
909
|
-
if (cancelled.has(id)) {
|
|
973
|
+
if (cancelled.has(id)) {
|
|
974
|
+
if (earlyLocal) await discardUnadmitted(entry)
|
|
975
|
+
await storage.remove(id)
|
|
976
|
+
throw new Error('CHAT_UNAVAILABLE')
|
|
977
|
+
}
|
|
910
978
|
// An accepted durable intent survives shutdown/locking for replay. Only
|
|
911
979
|
// explicit cancellation removes it; no late callback or work is admitted.
|
|
912
|
-
if (closed)
|
|
913
|
-
|
|
980
|
+
if (closed) {
|
|
981
|
+
if (earlyLocal) await discardUnadmitted(entry)
|
|
982
|
+
return id
|
|
983
|
+
}
|
|
984
|
+
if (earlyLocal) unadmitted.delete(entry)
|
|
985
|
+
else entries.set(id, entry)
|
|
914
986
|
if (peer !== owner && available && lifecycle === urgencyLifecycle) networkUrgency.set(id, urgencyUntil)
|
|
987
|
+
if (earlyLocal && entry.localFailed) {
|
|
988
|
+
entry.status = 'error'; entry.retryable = entry.localRetryable
|
|
989
|
+
const error = localErrors.get(entry)
|
|
990
|
+
if (error) {
|
|
991
|
+
reportError(error)
|
|
992
|
+
if (!entry.deletion) onSendError(error, { id: entry.id, peer: entry.peer })
|
|
993
|
+
}
|
|
994
|
+
}
|
|
915
995
|
watchOfflineSends(); emit(); pump()
|
|
916
996
|
if (available && peer !== owner) preparePeer(peer).catch(() => {})
|
|
917
997
|
return id
|
|
@@ -921,7 +1001,7 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
921
1001
|
if (storageBlocked) inboxRecovery.start({ immediate: true })
|
|
922
1002
|
await ready()
|
|
923
1003
|
const entry = entries.get(id)
|
|
924
|
-
if (!entry) return
|
|
1004
|
+
if (!entry || unadmitted.has(entry)) return
|
|
925
1005
|
journal.retry()
|
|
926
1006
|
if (entry.deliveryConfirmed) {
|
|
927
1007
|
// This action belongs to local recovery, never a fresh network attempt.
|