libp2r2p 0.11.34 → 0.11.36
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 +10 -0
- package/kind/index.js +2 -0
- package/package.json +1 -1
- package/private-messenger/session/README.md +13 -7
- package/private-messenger/session/index.js +72 -17
package/README.md
CHANGED
|
@@ -1301,3 +1301,13 @@ The 0.11.34 recovery adapter marks paged event-store reads with the internal
|
|
|
1301
1301
|
over new background read transactions. `putMany` also accepts an optional
|
|
1302
1302
|
development diagnostic callback with aggregate existing/inserted/skipped counts;
|
|
1303
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.
|
|
1310
|
+
|
|
1311
|
+
The 0.11.36 kind registry adds `CONTACT_LIST = 13300` as a replaceable,
|
|
1312
|
+
project-specific contact list used only inside private personal copies.
|
|
1313
|
+
`FOLLOW_SET = 30000` remains the addressable NIP-02-style custom follow set.
|
package/kind/index.js
CHANGED
|
@@ -28,6 +28,7 @@ export const COMMENT = 1111
|
|
|
28
28
|
export const VOICE_MESSAGE = 1222
|
|
29
29
|
export const VOICE_MESSAGE_REPLY = 1244
|
|
30
30
|
export const LIVE_CHAT_MESSAGE = 1311
|
|
31
|
+
export const CONTACT_LIST = 13300
|
|
31
32
|
export const PROBLEM_TRACKER = 1971
|
|
32
33
|
export const REPORT = 1984
|
|
33
34
|
export const LABEL = 1985
|
|
@@ -169,6 +170,7 @@ export const eventKinds = /* @__PURE__ */ Object.freeze({
|
|
|
169
170
|
VOICE_MESSAGE,
|
|
170
171
|
VOICE_MESSAGE_REPLY,
|
|
171
172
|
LIVE_CHAT_MESSAGE,
|
|
173
|
+
CONTACT_LIST,
|
|
172
174
|
PROBLEM_TRACKER,
|
|
173
175
|
REPORT,
|
|
174
176
|
LABEL,
|
package/package.json
CHANGED
|
@@ -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
|
|
@@ -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
|
})))
|
|
@@ -484,6 +490,7 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
484
490
|
await assertMessagePublished(report, event)
|
|
485
491
|
}
|
|
486
492
|
async function cleanupEntry (entry) {
|
|
493
|
+
if (unadmitted.has(entry)) return
|
|
487
494
|
if (entry.prepared) { journal.wake(); return }
|
|
488
495
|
if (!entry.cancelled && !completed(entry)) return
|
|
489
496
|
if (entry.failed && entry.status === 'saved') return
|
|
@@ -503,10 +510,11 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
503
510
|
return work
|
|
504
511
|
}
|
|
505
512
|
async function saveEntry (entry) {
|
|
506
|
-
if (entry.peer === owner) { entry.status = 'pending'; emit() }
|
|
513
|
+
if (entry.peer === owner && !unadmitted.has(entry)) { entry.status = 'pending'; emit() }
|
|
507
514
|
const releaseForeground = foreground.enter()
|
|
508
515
|
const active = async () => {
|
|
509
|
-
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() }
|
|
510
518
|
if (closed || !available || cancelled.has(entry.id) || (entry.peer !== owner && !peers.has(entry.peer))) throw new Error('CHAT_UNAVAILABLE')
|
|
511
519
|
}
|
|
512
520
|
const startedAt = Date.now()
|
|
@@ -524,12 +532,18 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
524
532
|
}
|
|
525
533
|
entry.localFailed = false
|
|
526
534
|
debug(entry, 'local-stored', startedAt)
|
|
535
|
+
if (unadmitted.has(entry)) return
|
|
527
536
|
if (entry.peer === owner) entry.status = 'saved'
|
|
528
537
|
emit()
|
|
529
538
|
if (completed(entry)) cleanupEntry(entry)
|
|
530
539
|
else await journal.write(entry)
|
|
531
540
|
} catch (error) {
|
|
541
|
+
if (admissionFailed.has(entry)) return
|
|
532
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
|
+
}
|
|
533
547
|
entry.localFailed = true; entry.localRetryable = retryable(error)
|
|
534
548
|
if (entry.peer === owner) { entry.status = 'error'; entry.retryable = entry.localRetryable }
|
|
535
549
|
await journal.write(entry).catch(reportError)
|
|
@@ -653,7 +667,7 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
653
667
|
clearSendRetry()
|
|
654
668
|
sendRetryDelay = SEND_RETRY_MIN_MS
|
|
655
669
|
}
|
|
656
|
-
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'))))
|
|
657
671
|
function scheduleSendRetry () {
|
|
658
672
|
if (closed || !available || sendRetryTimer) return
|
|
659
673
|
let nextRetryAt = 0
|
|
@@ -741,14 +755,21 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
741
755
|
relocal = false
|
|
742
756
|
for (const entry of entries.values()) {
|
|
743
757
|
if (closed || !available) break
|
|
758
|
+
if (admissionFailed.has(entry)) continue
|
|
744
759
|
if ((entry.localFailed && !localRetries.has(entry.id)) || cancelled.has(entry.id) || (entry.peer !== owner && !peers.has(entry.peer))) continue
|
|
745
|
-
if (localComplete(entry) && !entry.localFailed) {
|
|
746
|
-
|
|
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) }
|
|
747
768
|
pumpRemote()
|
|
748
769
|
}
|
|
749
770
|
} while (relocal)
|
|
750
771
|
} finally {
|
|
751
|
-
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()
|
|
752
773
|
localSaving = false; for (const resolve of localIdleWaiters) resolve(); localIdleWaiters.clear(); settled()
|
|
753
774
|
}
|
|
754
775
|
}
|
|
@@ -761,7 +782,7 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
761
782
|
repump = false
|
|
762
783
|
for (const entry of entries.values()) {
|
|
763
784
|
if (closed || !available) break
|
|
764
|
-
if (entry.prepared?.accepted) { journal.wake(); continue }
|
|
785
|
+
if (unadmitted.has(entry) || entry.prepared?.accepted) { if (entry.prepared?.accepted) journal.wake(); continue }
|
|
765
786
|
if (entry.peer === owner || entry.status === 'saved' || !peers.has(entry.peer) || entry.failed || cancelled.has(entry.id)) continue
|
|
766
787
|
if (!preparation.get(entry.peer) && !preparation.error(entry.peer)) {
|
|
767
788
|
preparation.request(entry.peer).catch(() => {})
|
|
@@ -779,6 +800,12 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
779
800
|
}
|
|
780
801
|
}
|
|
781
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
|
+
}
|
|
782
809
|
const intentId = file => getEventHash({ kind: 0, pubkey: owner, created_at: 0, tags: [], content: `${file.peer}:${file.root}` })
|
|
783
810
|
async function download (file, options = {}) {
|
|
784
811
|
if (file.peer === owner) return file
|
|
@@ -904,7 +931,7 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
904
931
|
journal.setAvailable(true)
|
|
905
932
|
pumpLocal()
|
|
906
933
|
for (const entry of entries.values()) {
|
|
907
|
-
if (entry.localRetryable) localRetries.add(entry.id)
|
|
934
|
+
if (!unadmitted.has(entry) && entry.localRetryable) localRetries.add(entry.id)
|
|
908
935
|
if (entry.failed && (entry.retryable || entry.status === 'pending')) { entry.failed = false; attemptStarts.set(entry.id, performance.now()) }
|
|
909
936
|
}
|
|
910
937
|
if (storageBlocked) inboxRecovery.start({ immediate: true })
|
|
@@ -926,17 +953,45 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
926
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 }] : [])])
|
|
927
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 }
|
|
928
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
|
+
}
|
|
929
963
|
debug(entry, 'outbox-start')
|
|
930
|
-
|
|
964
|
+
try {
|
|
965
|
+
await storage.put(entry)
|
|
966
|
+
} catch (error) {
|
|
967
|
+
if (earlyLocal) await discardUnadmitted(entry)
|
|
968
|
+
throw error
|
|
969
|
+
}
|
|
931
970
|
debug(entry, 'outbox-stored', startedAt)
|
|
932
971
|
queuedAt.set(entry, Date.now())
|
|
933
972
|
if (attemptStartedAt !== undefined) attemptStarts.set(id, attemptStartedAt)
|
|
934
|
-
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
|
+
}
|
|
935
978
|
// An accepted durable intent survives shutdown/locking for replay. Only
|
|
936
979
|
// explicit cancellation removes it; no late callback or work is admitted.
|
|
937
|
-
if (closed)
|
|
938
|
-
|
|
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)
|
|
939
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
|
+
}
|
|
940
995
|
watchOfflineSends(); emit(); pump()
|
|
941
996
|
if (available && peer !== owner) preparePeer(peer).catch(() => {})
|
|
942
997
|
return id
|
|
@@ -946,7 +1001,7 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
946
1001
|
if (storageBlocked) inboxRecovery.start({ immediate: true })
|
|
947
1002
|
await ready()
|
|
948
1003
|
const entry = entries.get(id)
|
|
949
|
-
if (!entry) return
|
|
1004
|
+
if (!entry || unadmitted.has(entry)) return
|
|
950
1005
|
journal.retry()
|
|
951
1006
|
if (entry.deliveryConfirmed) {
|
|
952
1007
|
// This action belongs to local recovery, never a fresh network attempt.
|