web/src/offline.ts47 lines · 2327 bytes
| 1 | // Offline mode (?mode=offline): plays back src/recorded-run.json (scripts/record-run.mjs) for a venue whose network |
| 2 | // fails. Nothing here opens a connection: the logs, block timestamps, receipts and final proposal states were all |
| 3 | // recorded, and they go through the same log handler as the live page. |
| 4 | import { blocks } from './chain' |
| 5 | import { RECORDING } from './chaindata' |
| 6 | import { blockTs, handleLog, loadImplNames, pickActive, resetSeen, type LogHooks } from './events' |
| 7 | import { msg } from './i18n' |
| 8 | import { playbackGate } from './playback' |
| 9 | import { store } from './store' |
| 10 | import { isZero32 } from './types' |
| 11 | |
| 12 | const NO_NETWORK: LogHooks = { wantReceipt() {}, wantBlockTs() {}, refresh() {}, onLive() {} } |
| 13 | |
| 14 | /** Load the recording into the store; returns the number of proposals */ |
| 15 | export function loadRecording(): number { |
| 16 | const R = RECORDING |
| 17 | const d = R.deployment |
| 18 | store.deploy = { airlockAdmin: d.airlockAdmin, proxy: d.proxy, delayBlocks: d.delayBlocks, chainId: d.chainId } |
| 19 | loadImplNames() |
| 20 | for (const [n, t] of Object.entries(R.blocks)) blockTs.set(Number(n), t) |
| 21 | for (const [tx, r] of Object.entries(R.receipts)) |
| 22 | store.receipts.set(tx.toLowerCase(), { status: r.status === 'success' ? 'success' : 'reverted', gasUsed: r.gasUsed, effectiveGasPrice: r.effectiveGasPrice }) |
| 23 | resetSeen() |
| 24 | const logs = [...R.logs].sort((a, b) => parseInt(a.blockNumber, 16) - parseInt(b.blockNumber, 16) || parseInt(a.logIndex, 16) - parseInt(b.logIndex, 16)) |
| 25 | for (const l of logs) handleLog(l, false, NO_NETWORK) |
| 26 | for (const [id, cp] of Object.entries(R.proposals)) { |
| 27 | if (!cp.status) continue |
| 28 | const p = store.ensure(id) |
| 29 | p.chain = { status: cp.status, vetoCount: cp.vetoCount, proposer: cp.proposer, clearanceOf: isZero32(cp.clearanceOf) ? null : cp.clearanceOf } |
| 30 | p.proposeBlock = p.proposeBlock ?? cp.proposedBlock |
| 31 | p.readyBlock = p.readyBlock ?? cp.readyBlock |
| 32 | if (!p.target) p.target = cp.target |
| 33 | } |
| 34 | pickActive() |
| 35 | // the load itself is not news: the playback writes its own lines as it goes |
| 36 | store.stream = [] |
| 37 | const n = Object.keys(R.proposals).length |
| 38 | store.logT('chain', 'load', msg('off.loaded', { n, d: R.recordedOn || '—' }), 'info') |
| 39 | return n |
| 40 | } |
| 41 | |
| 42 | export function startOffline() { |
| 43 | loadRecording() |
| 44 | blocks.setOffline(RECORDING.head) |
| 45 | playbackGate.ready = true |
| 46 | store.emit() |
| 47 | } |