@agfpd/iapeer 0.4.36 → 0.4.37
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 +1 -1
- package/src/daemon/main.ts +19 -0
- package/src/lifecycle/index.ts +6 -0
- package/src/registry/index.ts +7 -0
- package/src/transport/index.ts +24 -2
package/package.json
CHANGED
package/src/daemon/main.ts
CHANGED
|
@@ -547,6 +547,25 @@ if (import.meta.main) {
|
|
|
547
547
|
}
|
|
548
548
|
process.on('SIGTERM', shutdown)
|
|
549
549
|
process.on('SIGINT', shutdown)
|
|
550
|
+
// В5 — a NON-signal crash (an unhandledRejection, or any uncaught error) would otherwise kill the
|
|
551
|
+
// daemon WITHOUT running handle.close() → failAll, so in-memory composer-queued messages (the sender
|
|
552
|
+
// already got queued:true) would vanish SILENTLY — the forbidden loss class. Run failAll on these
|
|
553
|
+
// paths too so senders are notified, then exit(1) so launchd KeepAlive respawns a clean daemon. A
|
|
554
|
+
// bounded timeout guarantees the exit even if close() hangs. (Hard SIGKILL/OOM cannot be caught — a
|
|
555
|
+
// durable on-disk composer queue would be needed for that, a deliberate follow-up.)
|
|
556
|
+
let crashing = false
|
|
557
|
+
const crashExit = (kind: string) => (errObj: unknown) => {
|
|
558
|
+
if (crashing) return
|
|
559
|
+
crashing = true
|
|
560
|
+
const detail = errObj instanceof Error ? (errObj.stack ?? errObj.message) : String(errObj)
|
|
561
|
+
process.stderr.write(`[iapeer-daemon] ${kind} — failing pending queued deliveries then exiting: ${detail}\n`)
|
|
562
|
+
const done = (): never => process.exit(1)
|
|
563
|
+
const t = setTimeout(done, 3000)
|
|
564
|
+
if (typeof t.unref === 'function') t.unref()
|
|
565
|
+
void handle.close().then(done, done)
|
|
566
|
+
}
|
|
567
|
+
process.on('uncaughtException', crashExit('uncaughtException'))
|
|
568
|
+
process.on('unhandledRejection', crashExit('unhandledRejection'))
|
|
550
569
|
await new Promise(() => {}) // launchd KeepAlive holds this process; block forever
|
|
551
570
|
}
|
|
552
571
|
}
|
package/src/lifecycle/index.ts
CHANGED
|
@@ -727,6 +727,12 @@ export async function withWakeLock<T>(
|
|
|
727
727
|
stale: 60_000,
|
|
728
728
|
update: 5_000,
|
|
729
729
|
retries: { retries: cfg.wakeLockRetries, factor: 1.3, minTimeout: 100, maxTimeout: 1_000 },
|
|
730
|
+
// В5 — a compromised wake-lock (mtime-refresh timer fired late, e.g. after system sleep >60s) MUST
|
|
731
|
+
// NOT rethrow from proper-lockfile's timer: unhandled it crashes the whole router and silently drops
|
|
732
|
+
// the in-memory composer queue. Log and continue — a controlled degrade beats killing the daemon.
|
|
733
|
+
onCompromised: (err: Error) => {
|
|
734
|
+
process.stderr.write(`[iapeer] WARN wake-lock compromised for ${identity} (continuing, not crashing): ${err.message}\n`)
|
|
735
|
+
},
|
|
730
736
|
})
|
|
731
737
|
try {
|
|
732
738
|
return await fn()
|
package/src/registry/index.ts
CHANGED
|
@@ -297,6 +297,13 @@ export async function withPeersLock<T>(
|
|
|
297
297
|
stale: 10_000,
|
|
298
298
|
update: 1_000,
|
|
299
299
|
retries: { retries: 13, factor: 1.4, minTimeout: 50, maxTimeout: 500 },
|
|
300
|
+
// В5 — a compromised lock (e.g. the mtime-refresh timer fired late after system sleep, so the lock
|
|
301
|
+
// read as stale) MUST NOT rethrow from proper-lockfile's timer: unhandled, it would crash the whole
|
|
302
|
+
// router process and silently drop the in-memory composer queue. Log loudly and let the current
|
|
303
|
+
// (near-done) critical section finish — a controlled degrade beats taking the daemon down.
|
|
304
|
+
onCompromised: (err: Error) => {
|
|
305
|
+
process.stderr.write(`[iapeer] WARN peers-lock compromised (continuing, not crashing): ${err.message}\n`)
|
|
306
|
+
},
|
|
300
307
|
})
|
|
301
308
|
try {
|
|
302
309
|
return await fn(paths)
|
package/src/transport/index.ts
CHANGED
|
@@ -1025,7 +1025,17 @@ export function createComposerDeliveryQueue(opts: ComposerDeliveryQueueOptions):
|
|
|
1025
1025
|
function kick(identity: string): void {
|
|
1026
1026
|
if (draining.has(identity)) return
|
|
1027
1027
|
draining.add(identity)
|
|
1028
|
-
void drain(identity)
|
|
1028
|
+
void drain(identity)
|
|
1029
|
+
.catch(() => {
|
|
1030
|
+
/* drain is internally best-effort; a stray throw must not surface as an unhandled rejection */
|
|
1031
|
+
})
|
|
1032
|
+
.finally(() => {
|
|
1033
|
+
draining.delete(identity)
|
|
1034
|
+
// В9 — close the enqueue-during-teardown race: a job pushed AFTER drain's empty-return but
|
|
1035
|
+
// BEFORE this finally cleared `draining` saw kick() no-op (draining still held), leaving it
|
|
1036
|
+
// undrained until some unrelated later enqueue. Re-kick if work remains so it drains promptly.
|
|
1037
|
+
if (queues.get(identity)?.length) kick(identity)
|
|
1038
|
+
})
|
|
1029
1039
|
}
|
|
1030
1040
|
|
|
1031
1041
|
async function drain(identity: string): Promise<void> {
|
|
@@ -1050,8 +1060,20 @@ export function createComposerDeliveryQueue(opts: ComposerDeliveryQueueOptions):
|
|
|
1050
1060
|
continue
|
|
1051
1061
|
}
|
|
1052
1062
|
|
|
1053
|
-
|
|
1063
|
+
// В8 — take the head OUT of the queue BEFORE committing to deliver. A concurrent failAll snapshots
|
|
1064
|
+
// only what remains queued, so an IN-FLIGHT job is never both delivered AND failed (the dup / false-
|
|
1065
|
+
// failure the exactly-once contract forbids). The head stayed queued during the composer poll-wait
|
|
1066
|
+
// above (still undelivered → failAll correctly fails it); it leaves the queue only at this commit.
|
|
1054
1067
|
q.shift()
|
|
1068
|
+
let delivered: Result<void>
|
|
1069
|
+
try {
|
|
1070
|
+
delivered = await deliver(job.target, job.envelope, job.peer.cwd)
|
|
1071
|
+
} catch (e) {
|
|
1072
|
+
// deliverWarm normally returns a Result, but a defensive catch keeps drain from throwing (which,
|
|
1073
|
+
// with the В9 re-kick, could otherwise spin) — an unexpected throw is a delivery failure.
|
|
1074
|
+
await notifyFailed(job, `queued delivery to ${job.target.personality} (${job.target.runtime}) failed: ${e instanceof Error ? e.message : String(e)}`)
|
|
1075
|
+
continue
|
|
1076
|
+
}
|
|
1055
1077
|
if (!delivered.ok) {
|
|
1056
1078
|
await notifyFailed(job, `queued delivery to ${job.target.personality} (${job.target.runtime}) failed: ${delivered.error.message}`)
|
|
1057
1079
|
} else {
|