libp2r2p 0.11.23 → 0.11.24
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/package.json
CHANGED
|
@@ -72,7 +72,7 @@ The coordinator resolves the peer's channel and queues the request until the
|
|
|
72
72
|
channel exists. Priorities expire after `priorityTtlMs` (10 minutes) or once
|
|
73
73
|
covered, and are dropped with recovery state. They are process-local and reset
|
|
74
74
|
on reload.
|
|
75
|
-
Callbacks are `onOutbox`, `onMedia`,
|
|
75
|
+
Callbacks are optional `onDebug`, `onOutbox`, `onMedia`,
|
|
76
76
|
`onError`, and optional `onSendError(error, { id, peer })`. The caller maps its
|
|
77
77
|
account permissions/connectivity/lock state to availability and owns view subscriptions. There are no DOM or UI imports.
|
|
78
78
|
|
|
@@ -226,3 +226,26 @@ closing shared sockets or cancelling another subscriber for the same event ID.
|
|
|
226
226
|
Operations already transmitted cannot be recalled from relays. A messenger pause
|
|
227
227
|
cancels its outstanding publication work and leaves the outbox pending; resume
|
|
228
228
|
creates a fresh publication lifetime, without treating the pause as a user abort.
|
|
229
|
+
|
|
230
|
+
## Independent local persistence and publication
|
|
231
|
+
|
|
232
|
+
One encrypted outbox feeds two independent serial processors. Local persistence
|
|
233
|
+
never waits for peer preparation or an in-flight publication. Remote entries
|
|
234
|
+
become publishable only after their local progress checkpoint commits; failed
|
|
235
|
+
checkpoints are retried even if the copies have already committed. Text may
|
|
236
|
+
continue passing uploads between chunks after its local stage is ready.
|
|
237
|
+
|
|
238
|
+
Self entries emit `status: 'saved'` as soon as local persistence succeeds. They
|
|
239
|
+
do not write a redundant localSaved checkpoint before removal. Remote entries
|
|
240
|
+
emit that status after the first valid relay ACK for every required publication,
|
|
241
|
+
without waiting for redundant relays. Outbox cleanup runs independently; failures
|
|
242
|
+
retain saved status and retry only removal with the session backoff, never
|
|
243
|
+
`onSendError`. Consumers must accept saved entries while removal is pending.
|
|
244
|
+
A crash before removal replays the original event identity into idempotent storage.
|
|
245
|
+
Account shutdown fences late admissions and waits for both processors and cleanup.
|
|
246
|
+
|
|
247
|
+
`onDebug(record)` is disabled by default. Records carry fixed stage names, public
|
|
248
|
+
event/peer identifiers and elapsed milliseconds for admission, local processing,
|
|
249
|
+
channel preparation, publication acceptance and cleanup. Callback failures never
|
|
250
|
+
change delivery. Connect only in development; never log events or crypto buffers.
|
|
251
|
+
Custom outbox factories also receive this optional callback for storage timings.
|
|
@@ -44,7 +44,7 @@ function messengerStatusSnapshot (value) {
|
|
|
44
44
|
return { closed: value.closed, paused: value.paused, pauseReasons: [...value.pauseReasons] }
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
export function createPrivateMessageSession ({ owner, signer, eventStore, messageStorage, chunkStorage, recoveryStorage, fallbackRelays = [], fallbackDelayMs = null, mode = 'seeder', seedersForPeer = peer => [peer], allowedKinds = [5, 9, 1063, 34601], useContentKeys = true, pendingDecryptionReady = true, onMedia = () => {}, onOutbox = () => {}, onError = () => {}, onSendError = () => {}, Messenger = createPrivateMessenger, openOutbox = createChatOutbox, FileTransfer = createPrivateFileTransfer, _onOnline = onOnline, _isOnline = isOnline, _getIykcProofs = getIykcProofs, _setTimeout = setTimeout, _clearTimeout = clearTimeout, _random = Math.random, openDownloads = options => createChatOutbox({ ...options, namespace: 'downloads' }) }) {
|
|
47
|
+
export function createPrivateMessageSession ({ owner, signer, eventStore, messageStorage, chunkStorage, recoveryStorage, fallbackRelays = [], fallbackDelayMs = null, mode = 'seeder', seedersForPeer = peer => [peer], allowedKinds = [5, 9, 1063, 34601], useContentKeys = true, pendingDecryptionReady = true, onMedia = () => {}, onOutbox = () => {}, onError = () => {}, onSendError = () => {}, onDebug, Messenger = createPrivateMessenger, openOutbox = createChatOutbox, FileTransfer = createPrivateFileTransfer, _onOnline = onOnline, _isOnline = isOnline, _getIykcProofs = getIykcProofs, _setTimeout = setTimeout, _clearTimeout = clearTimeout, _random = Math.random, openDownloads = options => createChatOutbox({ ...options, namespace: 'downloads' }) }) {
|
|
48
48
|
fallbackRelays = normalizeFallbackRelays(fallbackRelays)
|
|
49
49
|
fallbackDelayMs = normalizeFallbackDelay(fallbackDelayMs)
|
|
50
50
|
if (typeof pendingDecryptionReady !== 'boolean') throw new ValidationError('INVALID_PENDING_DECRYPTION_READY')
|
|
@@ -66,6 +66,14 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
66
66
|
const entries = new Map()
|
|
67
67
|
const cancelled = new Set()
|
|
68
68
|
const sendControllers = new Map()
|
|
69
|
+
const localReady = new Set()
|
|
70
|
+
const localIdleWaiters = new Set()
|
|
71
|
+
const cleanups = new Map()
|
|
72
|
+
const queuedAt = new WeakMap()
|
|
73
|
+
const debug = (entry, stage, startedAt) => {
|
|
74
|
+
if (!onDebug) return
|
|
75
|
+
try { onDebug({ stage, eventId: entry.id, peer: entry.peer, at: Date.now(), ...(startedAt != null ? { elapsedMs: Date.now() - startedAt } : {}) }) } catch {}
|
|
76
|
+
}
|
|
69
77
|
const offlineSends = new Set()
|
|
70
78
|
const pausedSends = new Set()
|
|
71
79
|
let messengerStatus
|
|
@@ -123,15 +131,22 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
123
131
|
let messengerWork
|
|
124
132
|
let fileSetupWork
|
|
125
133
|
let sending
|
|
134
|
+
let enqueuing = 0
|
|
135
|
+
let localSaving = false
|
|
136
|
+
let relocal = false
|
|
126
137
|
let repump
|
|
127
138
|
let draining
|
|
128
139
|
let redrain = false
|
|
129
140
|
const idleWaiters = new Set()
|
|
130
|
-
const settled = () => { if (!sending && !draining) { for (const resolve of idleWaiters) resolve(); idleWaiters.clear() } }
|
|
141
|
+
const settled = () => { if (!sending && !localSaving && !enqueuing && !draining && !cleanups.size) { for (const resolve of idleWaiters) resolve(); idleWaiters.clear() } }
|
|
131
142
|
const emit = () => onOutbox([...entries.values()].filter(entry => !entry.deletion))
|
|
132
143
|
const ready = () => (initialized ??= (async () => {
|
|
133
|
-
storage = await openOutbox({ owner, signer })
|
|
134
|
-
for (const entry of await storage.list())
|
|
144
|
+
storage = await openOutbox({ owner, signer, onDebug })
|
|
145
|
+
for (const entry of await storage.list()) {
|
|
146
|
+
entries.set(entry.id, entry)
|
|
147
|
+
if (entry.localSaved.every(Boolean)) localReady.add(entry.id)
|
|
148
|
+
queuedAt.set(entry, Date.now())
|
|
149
|
+
}
|
|
135
150
|
emit()
|
|
136
151
|
})().catch(async error => { await storage?.close(); storage = null; initialized = null; throw error }))
|
|
137
152
|
async function createMessenger () {
|
|
@@ -167,6 +182,7 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
167
182
|
|
|
168
183
|
const preparation = createPeerPreparation({
|
|
169
184
|
prepare: async (peer, signal) => {
|
|
185
|
+
const startedAt = Date.now()
|
|
170
186
|
const scoped = messengerSigner(signer.withSharedKey(peer, 'dm'))
|
|
171
187
|
const pubkey = await scoped.getPublicKey()
|
|
172
188
|
signal.throwIfAborted()
|
|
@@ -175,6 +191,7 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
175
191
|
if (peer === pendingDecryptionPeer) messenger?.setPendingDecryptionPriority?.(channel.pubkey)
|
|
176
192
|
await schedule()
|
|
177
193
|
signal.throwIfAborted()
|
|
194
|
+
if (onDebug) { try { onDebug({ stage: 'channel-ready', peer, elapsedMs: Date.now() - startedAt }) } catch {} }
|
|
178
195
|
return channel
|
|
179
196
|
},
|
|
180
197
|
onReady: () => { drain(); pump() }, onError: reportError,
|
|
@@ -365,7 +382,23 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
365
382
|
// Published is relay acceptance, never a peer receipt.
|
|
366
383
|
await assertMessagePublished(report, event)
|
|
367
384
|
}
|
|
368
|
-
async function
|
|
385
|
+
async function cleanupEntry (entry) {
|
|
386
|
+
if (cleanups.has(entry.id)) return cleanups.get(entry.id)
|
|
387
|
+
const startedAt = Date.now()
|
|
388
|
+
const work = Promise.resolve().then(() => storage.remove(entry.id)).then(() => {
|
|
389
|
+
entries.delete(entry.id); localReady.delete(entry.id)
|
|
390
|
+
debug(entry, 'outbox-cleanup', startedAt)
|
|
391
|
+
emit()
|
|
392
|
+
}).catch(error => {
|
|
393
|
+
// Delivery has already succeeded. Cleanup must never become a send error.
|
|
394
|
+
entry.failed = true; entry.retryable = true
|
|
395
|
+
reportError(error)
|
|
396
|
+
if (!closed && available) scheduleSendRetry()
|
|
397
|
+
}).finally(() => { cleanups.delete(entry.id); if (![...entries.values()].some(pendingRetryable) && !cleanups.size) resetSendRetry(); settled() })
|
|
398
|
+
cleanups.set(entry.id, work)
|
|
399
|
+
return work
|
|
400
|
+
}
|
|
401
|
+
async function sendEntry (entry, localOnly = false) {
|
|
369
402
|
const releaseForeground = foreground.enter()
|
|
370
403
|
entry.status = 'pending'; emit()
|
|
371
404
|
const controller = new AbortController()
|
|
@@ -375,19 +408,32 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
375
408
|
if (storage.has && !await storage.has(entry.id)) cancelled.add(entry.id)
|
|
376
409
|
if (closed || !available || cancelled.has(entry.id) || (entry.peer !== owner && !peers.has(entry.peer))) throw new Error('CHAT_UNAVAILABLE')
|
|
377
410
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
411
|
+
if (localOnly) {
|
|
412
|
+
debug(entry, 'local-start', queuedAt.get(entry))
|
|
413
|
+
const startedAt = Date.now()
|
|
414
|
+
for (let index = 0; index < entry.events.length; index++) {
|
|
415
|
+
await active()
|
|
416
|
+
if (entry.localSaved[index]) continue
|
|
417
|
+
const event = entry.events[index]
|
|
384
418
|
const hearsay = !event.sig && event.pubkey !== owner
|
|
385
419
|
const result = await messageStorage.save(event, { peerPubkey: entry.peer, hearsay })
|
|
386
420
|
if (result?.result?.code === 'blocked') { cancelled.add(entry.id); await active() }
|
|
387
421
|
if (!result?.result?.ok) throw Object.assign(new Error('MESSAGE_STORAGE_FAILED'), { code: result?.result?.code?.toUpperCase() })
|
|
388
422
|
entry.localSaved[index] = true
|
|
423
|
+
}
|
|
424
|
+
debug(entry, 'local-stored', startedAt)
|
|
425
|
+
await active()
|
|
426
|
+
if (entry.peer === owner) {
|
|
427
|
+
entry.status = 'saved'; emit()
|
|
428
|
+
cleanupEntry(entry)
|
|
429
|
+
} else {
|
|
430
|
+
// Retry this checkpoint even when a preceding attempt saved all copies
|
|
431
|
+
// but failed to persist progress. Only then admit remote publication.
|
|
389
432
|
await storage.put(entry, { existing: true })
|
|
433
|
+
await active()
|
|
434
|
+
localReady.add(entry.id)
|
|
390
435
|
}
|
|
436
|
+
return
|
|
391
437
|
}
|
|
392
438
|
if (entry.peer !== owner && !preparation.get(entry.peer)) {
|
|
393
439
|
if (preparation.error(entry.peer)) throw preparation.error(entry.peer)
|
|
@@ -414,11 +460,17 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
414
460
|
emit()
|
|
415
461
|
await active()
|
|
416
462
|
await storage.put({ ...entry, chunkIndex: entry.chunkIndex + 1 }, { existing: true })
|
|
463
|
+
// Let the independent local stage finish newly admitted texts before
|
|
464
|
+
// selecting publications at this chunk boundary. It never awaits us.
|
|
465
|
+
if ([...entries.values()].some(pending => !pending.files.length && pending.peer !== owner && !pending.failed && !localReady.has(pending.id))) {
|
|
466
|
+
await pumpLocal()
|
|
467
|
+
if (localSaving) await new Promise(resolve => localIdleWaiters.add(resolve))
|
|
468
|
+
}
|
|
417
469
|
// Newly queued text and deletion controls can pass an in-flight file.
|
|
418
470
|
let admitted = 0
|
|
419
471
|
for (const pending of entries.values()) {
|
|
420
472
|
if (closed || !available || admitted >= 8) break
|
|
421
|
-
if (pending.files.length || pending.failed || cancelled.has(pending.id) || (pending.peer !== owner && !peers.has(pending.peer))) continue
|
|
473
|
+
if (!localReady.has(pending.id) || pending.peer === owner || pending.status === 'saved' || pending.files.length || pending.failed || cancelled.has(pending.id) || (pending.peer !== owner && !peers.has(pending.peer))) continue
|
|
422
474
|
admitted++
|
|
423
475
|
await sendEntry(pending)
|
|
424
476
|
}
|
|
@@ -427,15 +479,16 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
427
479
|
for (; entry.index < entry.events.length; entry.index++) {
|
|
428
480
|
await active()
|
|
429
481
|
const event = entry.events[entry.index]
|
|
430
|
-
|
|
482
|
+
const startedAt = Date.now()
|
|
483
|
+
await publish(entry.peer, event, controller.signal)
|
|
484
|
+
debug(entry, 'publication-accepted', startedAt)
|
|
431
485
|
await active()
|
|
432
486
|
// The final durable operation is removal. Avoid encrypting a checkpoint
|
|
433
487
|
// that would immediately be deleted; earlier context stages still checkpoint.
|
|
434
488
|
if (entry.index + 1 < entry.events.length) await storage.put({ ...entry, index: entry.index + 1 }, { existing: true })
|
|
435
489
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
emit()
|
|
490
|
+
entry.status = 'saved'; emit()
|
|
491
|
+
cleanupEntry(entry)
|
|
439
492
|
} catch (cause) {
|
|
440
493
|
// Pauses can surface at any stage (text publication, file authorization
|
|
441
494
|
// or chunk delivery). Normalize all of them into the retryable path.
|
|
@@ -483,6 +536,7 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
483
536
|
for (const entry of entries.values()) {
|
|
484
537
|
if (!pendingRetryable(entry) || pausedSends.has(entry.id)) continue
|
|
485
538
|
entry.failed = false
|
|
539
|
+
if (entry.status === 'saved') { cleanupEntry(entry); continue }
|
|
486
540
|
retried = true
|
|
487
541
|
}
|
|
488
542
|
if (retried) pump()
|
|
@@ -505,10 +559,26 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
505
559
|
return schedule()
|
|
506
560
|
})
|
|
507
561
|
}
|
|
508
|
-
async function
|
|
562
|
+
async function pumpLocal () {
|
|
509
563
|
if (!available || closed || !storage) return
|
|
510
|
-
|
|
511
|
-
|
|
564
|
+
if (localSaving) { relocal = true; return }
|
|
565
|
+
localSaving = true
|
|
566
|
+
try {
|
|
567
|
+
do {
|
|
568
|
+
relocal = false
|
|
569
|
+
for (const entry of entries.values()) {
|
|
570
|
+
if (closed || !available) break
|
|
571
|
+
if (entry.failed || cancelled.has(entry.id) || (entry.peer !== owner && !peers.has(entry.peer))) continue
|
|
572
|
+
if (entry.status === 'saved') { cleanupEntry(entry); continue }
|
|
573
|
+
if (localReady.has(entry.id) && entry.peer !== owner) continue
|
|
574
|
+
await sendEntry(entry, true)
|
|
575
|
+
pumpRemote()
|
|
576
|
+
}
|
|
577
|
+
} while (relocal)
|
|
578
|
+
} finally { localSaving = false; for (const resolve of localIdleWaiters) resolve(); localIdleWaiters.clear(); settled() }
|
|
579
|
+
}
|
|
580
|
+
async function pumpRemote () {
|
|
581
|
+
if (!available || closed || !storage || !messenger) return
|
|
512
582
|
if (sending) { repump = true; return }
|
|
513
583
|
sending = true
|
|
514
584
|
try {
|
|
@@ -516,22 +586,23 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
516
586
|
repump = false
|
|
517
587
|
for (const entry of entries.values()) {
|
|
518
588
|
if (closed || !available) break
|
|
519
|
-
if (
|
|
520
|
-
if (
|
|
589
|
+
if (entry.peer === owner || entry.status === 'saved' || !localReady.has(entry.id) || !peers.has(entry.peer) || entry.failed || cancelled.has(entry.id)) continue
|
|
590
|
+
if (!preparation.get(entry.peer) && !preparation.error(entry.peer)) {
|
|
521
591
|
preparation.request(entry.peer).catch(() => {})
|
|
522
592
|
continue
|
|
523
593
|
}
|
|
524
|
-
if (entry.
|
|
594
|
+
if (entry.files.length && !fileTransfers) continue
|
|
525
595
|
await sendEntry(entry)
|
|
526
596
|
}
|
|
527
597
|
} while (repump)
|
|
528
598
|
} finally {
|
|
529
599
|
sending = false
|
|
530
600
|
watchOfflineSends()
|
|
531
|
-
if (![...entries.values()].some(pendingRetryable)) resetSendRetry()
|
|
601
|
+
if (![...entries.values()].some(pendingRetryable) && !cleanups.size) resetSendRetry()
|
|
532
602
|
settled()
|
|
533
603
|
}
|
|
534
604
|
}
|
|
605
|
+
async function pump () { await Promise.all([pumpLocal(), pumpRemote()]) }
|
|
535
606
|
const intentId = file => getEventHash({ kind: 0, pubkey: owner, created_at: 0, tags: [], content: `${file.peer}:${file.root}` })
|
|
536
607
|
async function download (file, options = {}) {
|
|
537
608
|
if (file.peer === owner) return file
|
|
@@ -648,6 +719,7 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
648
719
|
inboxRecovery.stop(); await messenger?.pause('signer'); return
|
|
649
720
|
}
|
|
650
721
|
await ready()
|
|
722
|
+
pumpLocal()
|
|
651
723
|
for (const entry of entries.values()) if (pendingRetryable(entry)) entry.failed = false
|
|
652
724
|
if (storageBlocked) inboxRecovery.start({ immediate: true })
|
|
653
725
|
if (!changed && messenger && !configurationFailed) { preparation.setAvailable(true); pump(); return configuring }
|
|
@@ -656,6 +728,7 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
656
728
|
async enqueue ({ peer, event, context = [], requiredFiles = [], deletion = false }) {
|
|
657
729
|
if (closed || !available || (peer !== owner && !peers.has(peer))) throw new Error('CHAT_UNAVAILABLE')
|
|
658
730
|
const releaseForeground = foreground.enter()
|
|
731
|
+
enqueuing++
|
|
659
732
|
try {
|
|
660
733
|
await ready()
|
|
661
734
|
const main = wireEvent(event, owner)
|
|
@@ -663,11 +736,19 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
663
736
|
const events = [...context.map(value => wireEvent(value, owner)), main]
|
|
664
737
|
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 }] : [])])
|
|
665
738
|
const entry = { id, peer, event: { ...main, id }, events, files, index: 0, fileIndex: 0, chunkIndex: 0, localSaved: context.map(() => true).concat(false), status: 'pending', deletion }
|
|
739
|
+
const startedAt = Date.now()
|
|
740
|
+
debug(entry, 'outbox-start')
|
|
666
741
|
await storage.put(entry)
|
|
742
|
+
debug(entry, 'outbox-stored', startedAt)
|
|
743
|
+
queuedAt.set(entry, Date.now())
|
|
744
|
+
if (cancelled.has(id)) { await storage.remove(id); throw new Error('CHAT_UNAVAILABLE') }
|
|
745
|
+
// An accepted durable intent survives shutdown/locking for replay. Only
|
|
746
|
+
// explicit cancellation removes it; no late callback or work is admitted.
|
|
747
|
+
if (closed) return id
|
|
667
748
|
entries.set(id, entry); emit(); pump()
|
|
668
|
-
if (peer !== owner) preparePeer(peer).catch(() => {})
|
|
749
|
+
if (available && peer !== owner) preparePeer(peer).catch(() => {})
|
|
669
750
|
return id
|
|
670
|
-
} finally { releaseForeground() }
|
|
751
|
+
} finally { releaseForeground(); enqueuing--; settled() }
|
|
671
752
|
},
|
|
672
753
|
async retry (id) {
|
|
673
754
|
offlineSends.delete(id); pausedSends.delete(id); watchOfflineSends()
|
|
@@ -681,7 +762,7 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
681
762
|
sendControllers.get(id)?.abort()
|
|
682
763
|
offlineSends.delete(id); pausedSends.delete(id); watchOfflineSends()
|
|
683
764
|
await ready()
|
|
684
|
-
entries.delete(id)
|
|
765
|
+
entries.delete(id); localReady.delete(id)
|
|
685
766
|
await storage.remove(id)
|
|
686
767
|
emit()
|
|
687
768
|
},
|
|
@@ -693,7 +774,7 @@ export function createPrivateMessageSession ({ owner, signer, eventStore, messag
|
|
|
693
774
|
for (const controller of sendControllers.values()) controller.abort()
|
|
694
775
|
await Promise.allSettled([configuring, initialized, messengerWork, fileSetupWork])
|
|
695
776
|
await messenger?.pause('closed')
|
|
696
|
-
if (sending || draining) await new Promise(resolve => idleWaiters.add(resolve))
|
|
777
|
+
if (sending || localSaving || enqueuing || draining || cleanups.size) await new Promise(resolve => idleWaiters.add(resolve))
|
|
697
778
|
await messenger?.close()
|
|
698
779
|
await storage?.close()
|
|
699
780
|
await downloadWriteTail.catch(reportError)
|