From cbc3a5907c55c20c248fa80fa14528cb483688ad Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sat, 1 Aug 2026 18:08:58 -0700 Subject: Split device sync into per-domain slots with bounded, dirty-only publishes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One monolithic snapshot per device outgrew a vanilla relay's 64KB event cap the moment real payment traffic arrived, and sync silently died. Snapshots now fragment into per-domain replaceable events (core wallet, ark, nwc — an extension claims a domain by naming it), published under a new ':x:' d-tag marker that pre-split clients don't match, so they can never mistake a fragment for a full snapshot. Only domains whose content changed republish, a refused publish retries on the next save instead of being marked done, and readers apply full-snapshot semantics only to core-carrying events. The ark projection also caps its history: spent stubs at 200, done actions at 50 — the event size is bounded for life, no special relay rules required. Co-Authored-By: Claude Fable 5 --- src/features/ark.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) (limited to 'src/features/ark.js') diff --git a/src/features/ark.js b/src/features/ark.js index ff0dd13..8456f69 100644 --- a/src/features/ark.js +++ b/src/features/ark.js @@ -130,11 +130,17 @@ export function slimArkForSync(s) { const HEAVY = ['bytes', 'destBytes', 'changeBytes', 'vtxoBytes', 'txHex', 'fundingTxHex', 'outputVtxos']; return { v: s.v, serverPubkey: s.serverPubkey, mailboxCheckpoint: s.mailboxCheckpoint, nextKeyIndex: s.nextKeyIndex, receiveAckTs: s.receiveAckTs, - vtxos: (s.vtxos || []).map((v) => v.state === 'spent' - ? { id: v.id, amountSat: v.amountSat, state: 'spent', keyIndex: v.keyIndex, expiryHeight: v.expiryHeight } - : v), + // Spent stubs and done actions are pure history and grow forever — cap + // them (newest kept) so the sync event's size is bounded for life. + vtxos: (() => { + const live = (s.vtxos || []).filter((v) => v.state !== 'spent'); + const stubs = (s.vtxos || []).filter((v) => v.state === 'spent').slice(-200) + .map((v) => ({ id: v.id, amountSat: v.amountSat, state: 'spent', keyIndex: v.keyIndex, expiryHeight: v.expiryHeight })); + return [...live, ...stubs]; + })(), actions: (s.actions || []) .filter((a) => a.step === 'done' && ['board', 'offboard', 'exit'].includes(a.type)) + .slice(-50) .map((a) => { const c = { ...a }; for (const k of HEAVY) delete c[k]; return c; }), movements: (s.movements || []).slice(-200).map((m, i, arr) => // Older rows only need to render a history line; the bolt11 (~400 chars @@ -199,6 +205,7 @@ export function arkFeature(ctx) { // didn't), re-publish the superset. The slot converges to the union no // matter who publishes when. wallet.registerCacheExtension({ + domain: 'ark', // published as its own sync slot — see splitSnapshotDomains mergeAlways: true, // load() is a commutative merge — apply older snapshots too save: () => { const cfg = getArkConfig(); -- cgit v1.2.3