Source mirror · commit 369512f · 2026-10-26 · MIT · canonical repo: gitlab.com/jiapunk/upgrade-airlock
git clone https://upgrade-airlock.pages.dev/source/upgrade-airlock.git
upgrade-airlock / web / src / offline.ts
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.
4import { blocks } from './chain'
5import { RECORDING } from './chaindata'
6import { blockTs, handleLog, loadImplNames, pickActive, resetSeen, type LogHooks } from './events'
7import { msg } from './i18n'
8import { playbackGate } from './playback'
9import { store } from './store'
10import { isZero32 } from './types'
11
12const NO_NETWORK: LogHooks = { wantReceipt() {}, wantBlockTs() {}, refresh() {}, onLive() {} }
13
14/** Load the recording into the store; returns the number of proposals */
15export 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
42export function startOffline() {
43 loadRecording()
44 blocks.setOffline(RECORDING.head)
45 playbackGate.ready = true
46 store.emit()
47}