Repository navigation
feat: pinBlock — run-start block resolution with onPin (F8) - #23
Merged
Merged
Conversation
Fill the two seams T9 reserved in src/core/internal.ts: - validatePinCapability(options, executor): pre-consumption. Throws when pinBlock is requested against an executor lacking getBlockNumber (always, regardless of block shape — a predictable capability contract), and when block.blockTag is 'pending' (no stable number). - resolvePinnedBlock(options, executor): post-consumption, pre-execute. No-op when pinBlock is false. When true: resolves an absent block or a latest/safe/finalized tag via one executor.getBlockNumber() call (+1 RT) to a concrete blockNumber used for every step; an explicit blockNumber/blockHash is used as-is with no extra RPC. Reports the resolved PinnedBlock to onPin synchronously, exactly once, before any multicall batch is dispatched; a throwing onPin rejects the run with tasks left consumed. Adds StepExecutor.getBlockNumber (optional, additive) and its Eip1193Executor implementation (eth_getBlockByNumber; blockNumber short-circuits with no RPC; blockHash throws — pinning never calls it for hashes). Adds BatchOptions.pinBlock/onPin and the PinnedBlock type, exported from index.ts. Both runners now thread the resolved block through their executeBatch policy instead of options.block directly. Docs: new "Atomicity" section in docs/api-reference.md (within-batch vs cross-step consistency, the resolution/no-op mapping table, and a pinBlock + Presets.throughput snippet); API Surface entries updated for the new fields/method/type. README bundle-size badge refreshed for the measured gzip delta.
… review) Three P2 findings from external review, same branch: 1. runMultistepTasks's empty-tasks shortcut used to bypass F8 entirely, so onPin never fired for a zero-task pinBlock:true run; runSettled had the opposite bug — it always ran prepareRun (and validatePinCapability) for an empty submission but returned [] before ever reaching resolvePinnedBlock, so onPin never fired there either. Both fixed: onPin's "exactly once per run when pinBlock: true" contract now holds for zero-task runs in both runners. runMultistepTasks's 1.0-pinned quirk (invalid batchSize with zero tasks silently resolves to []) stays intact — only pinBlock's own two seams are exempted from that shortcut, nothing else. 2. Added the missing consumed-state test: executor.getBlockNumber rejecting (the post-consumption seam) rejects the run with that exact error, dispatches zero multicall batches, and leaves the task consumed — proven by a resubmission throwing DominoTaskReuseError instead of another transport failure. Covers both runners. 3. Docs: the getBlockNumber entry in api-reference.md now states its exact contract (absent/tag resolves via eth_getBlockByNumber; explicit blockNumber returns as-is with no RPC; blockHash rejects). Atomicity table and the matching source doc comments now correctly note that 'earliest' resolves the same way as 'latest'/'safe'/'finalized' (any stable tag does) — only 'pending' throws. Adds 7 tests to src/__tests__/pinBlock.test.ts (24 total in that file).
Merged
halaprix
added a commit
that referenced
this pull request
Jul 24, 2026
* feat: pinBlock — run-start block resolution with onPin (F8) Fill the two seams T9 reserved in src/core/internal.ts: - validatePinCapability(options, executor): pre-consumption. Throws when pinBlock is requested against an executor lacking getBlockNumber (always, regardless of block shape — a predictable capability contract), and when block.blockTag is 'pending' (no stable number). - resolvePinnedBlock(options, executor): post-consumption, pre-execute. No-op when pinBlock is false. When true: resolves an absent block or a latest/safe/finalized tag via one executor.getBlockNumber() call (+1 RT) to a concrete blockNumber used for every step; an explicit blockNumber/blockHash is used as-is with no extra RPC. Reports the resolved PinnedBlock to onPin synchronously, exactly once, before any multicall batch is dispatched; a throwing onPin rejects the run with tasks left consumed. Adds StepExecutor.getBlockNumber (optional, additive) and its Eip1193Executor implementation (eth_getBlockByNumber; blockNumber short-circuits with no RPC; blockHash throws — pinning never calls it for hashes). Adds BatchOptions.pinBlock/onPin and the PinnedBlock type, exported from index.ts. Both runners now thread the resolved block through their executeBatch policy instead of options.block directly. Docs: new "Atomicity" section in docs/api-reference.md (within-batch vs cross-step consistency, the resolution/no-op mapping table, and a pinBlock + Presets.throughput snippet); API Surface entries updated for the new fields/method/type. README bundle-size badge refreshed for the measured gzip delta. * fix: F8 pinBlock — onPin fires on empty runs, doc precision (external review) Three P2 findings from external review, same branch: 1. runMultistepTasks's empty-tasks shortcut used to bypass F8 entirely, so onPin never fired for a zero-task pinBlock:true run; runSettled had the opposite bug — it always ran prepareRun (and validatePinCapability) for an empty submission but returned [] before ever reaching resolvePinnedBlock, so onPin never fired there either. Both fixed: onPin's "exactly once per run when pinBlock: true" contract now holds for zero-task runs in both runners. runMultistepTasks's 1.0-pinned quirk (invalid batchSize with zero tasks silently resolves to []) stays intact — only pinBlock's own two seams are exempted from that shortcut, nothing else. 2. Added the missing consumed-state test: executor.getBlockNumber rejecting (the post-consumption seam) rejects the run with that exact error, dispatches zero multicall batches, and leaves the task consumed — proven by a resubmission throwing DominoTaskReuseError instead of another transport failure. Covers both runners. 3. Docs: the getBlockNumber entry in api-reference.md now states its exact contract (absent/tag resolves via eth_getBlockByNumber; explicit blockNumber returns as-is with no RPC; blockHash rejects). Atomicity table and the matching source doc comments now correctly note that 'earliest' resolves the same way as 'latest'/'safe'/'finalized' (any stable tag does) — only 'pending' throws. Adds 7 tests to src/__tests__/pinBlock.test.ts (24 total in that file). --------- Co-authored-by: halaprix <halaprix@users.noreply.github.com>
halaprix
added a commit
that referenced
this pull request
Jul 24, 2026
* feat: pinBlock — run-start block resolution with onPin (F8) Fill the two seams T9 reserved in src/core/internal.ts: - validatePinCapability(options, executor): pre-consumption. Throws when pinBlock is requested against an executor lacking getBlockNumber (always, regardless of block shape — a predictable capability contract), and when block.blockTag is 'pending' (no stable number). - resolvePinnedBlock(options, executor): post-consumption, pre-execute. No-op when pinBlock is false. When true: resolves an absent block or a latest/safe/finalized tag via one executor.getBlockNumber() call (+1 RT) to a concrete blockNumber used for every step; an explicit blockNumber/blockHash is used as-is with no extra RPC. Reports the resolved PinnedBlock to onPin synchronously, exactly once, before any multicall batch is dispatched; a throwing onPin rejects the run with tasks left consumed. Adds StepExecutor.getBlockNumber (optional, additive) and its Eip1193Executor implementation (eth_getBlockByNumber; blockNumber short-circuits with no RPC; blockHash throws — pinning never calls it for hashes). Adds BatchOptions.pinBlock/onPin and the PinnedBlock type, exported from index.ts. Both runners now thread the resolved block through their executeBatch policy instead of options.block directly. Docs: new "Atomicity" section in docs/api-reference.md (within-batch vs cross-step consistency, the resolution/no-op mapping table, and a pinBlock + Presets.throughput snippet); API Surface entries updated for the new fields/method/type. README bundle-size badge refreshed for the measured gzip delta. * fix: F8 pinBlock — onPin fires on empty runs, doc precision (external review) Three P2 findings from external review, same branch: 1. runMultistepTasks's empty-tasks shortcut used to bypass F8 entirely, so onPin never fired for a zero-task pinBlock:true run; runSettled had the opposite bug — it always ran prepareRun (and validatePinCapability) for an empty submission but returned [] before ever reaching resolvePinnedBlock, so onPin never fired there either. Both fixed: onPin's "exactly once per run when pinBlock: true" contract now holds for zero-task runs in both runners. runMultistepTasks's 1.0-pinned quirk (invalid batchSize with zero tasks silently resolves to []) stays intact — only pinBlock's own two seams are exempted from that shortcut, nothing else. 2. Added the missing consumed-state test: executor.getBlockNumber rejecting (the post-consumption seam) rejects the run with that exact error, dispatches zero multicall batches, and leaves the task consumed — proven by a resubmission throwing DominoTaskReuseError instead of another transport failure. Covers both runners. 3. Docs: the getBlockNumber entry in api-reference.md now states its exact contract (absent/tag resolves via eth_getBlockByNumber; explicit blockNumber returns as-is with no RPC; blockHash rejects). Atomicity table and the matching source doc comments now correctly note that 'earliest' resolves the same way as 'latest'/'safe'/'finalized' (any stable tag does) — only 'pending' throws. Adds 7 tests to src/__tests__/pinBlock.test.ts (24 total in that file). --------- Co-authored-by: halaprix <halaprix@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Completes 1.2.0's execution controls (spec F8):
pinBlock(opt-in):latest/safe/finalized/earliesttags resolve ONCE at run start (+1 round-trip) via the new optionalStepExecutor.getBlockNumber(block?)(Eip1193Executor implements overeth_getBlockByNumber); the resolved number pins every step's multicall — cross-step atomicity documented in the new api-reference "Atomicity" section.pending+ pinBlock throws. ExplicitblockNumber/blockHashare no-ops (requireCanonicaluntouched, no extra RPC).onPin(pinnedBlock): synchronous, exactly once per run — including empty submissions (both runners reconciled; run()'s legacy empty-shortcut quirk for batchSize validation preserved) — only whenpinBlock: true. AnonPinthrow rejects the run with tasks consumed and zero batches dispatched.pendingfailures throw BEFORE consumption (tasks stay fresh — tested);getBlockNumberrejection after consumption leaves tasks consumed with zero dispatches (regression-tested against seam reordering).PinnedBlockexported;exactOptionalPropertyTypes-correct construction (requireCanonicalkey absent when unset).Presets.throughputspread;pinBlock: falsepaths byte-identical (compat-pinned).Test evidence
280/280 (24 pinBlock tests incl. once-per-run accounting, empty-run parity, consumed-state boundaries in both directions), compat-vs-dist 38/38, full gate build-first. Gzip 13.9KB (<15KB).
Spec:
spec.md§ 1.2.0 F8.