Skip to content

feat: MultichainResolver — parallel per-chain execution (F9) - #25

Merged
halaprix merged 2 commits into
mainfrom
feat/f9-multichain
Jul 23, 2026
Merged

halaprix merged 2 commits into
mainfrom
feat/f9-multichain

Conversation

@halaprix

Copy link
Copy Markdown
Owner

Summary

Implements spec F9 (1.3.0):

  • MultichainResolver (src/engine/multichain.ts): Record<chainId, provider | executor> constructor with lazy, cached Eip1193Executor wrapping; chain(id) cached resolvers; snapshot() — parallel block map with all-chains capability check before any RPC (and rejection handlers attached before anything can abort — a synchronously-throwing custom executor can no longer leak an earlier chain's rejection); runAll/runAllSettled with per-chain blocks overrides; chains execute in parallel; single-T generic (mixed shapes → per-chain calls, documented).
  • [v5] flattened duplicate-instance validation over the entire plan BEFORE any chain executes — the same branded instance under two chain IDs (or twice in one array) throws DominoTaskReuseError consuming nothing, killing the cross-chain consumption race; brand semantics shared with the runners via one isSingleUseTask predicate (no drift).
  • runAll rejects deterministically with the lowest-chainId rejection (others attached — no unhandled rejections; no cross-chain cancellation); onPin fires once per chain against each chain's own resolution.
  • Gzip ceiling consciously raised 15→18KB for the new feature class (measured F9 layer ~2.7KB; whole-artifact budget — sideEffects: false tree-shaking spares non-users); measured 17.12KB.

Test evidence

307/307 (27 multichain tests incl. flattened-duplicate pre-execution rejection with zero dispatches, parallel-overlap assertions, per-chain block routing, lowest-chainId determinism, snapshot sync-throw regression), compat-vs-dist 38/38, full gate build-first.

Spec: spec.md § 1.3.0 F9.

halaprix added 2 commits July 23, 2026 21:20
Runs task-shaped plans across several chains at once, fanning out to the
existing single-chain runners (runMultistepTasks/runSettled) per chain:
constructor discriminates StepExecutor vs. Eip1193Provider entries (the
latter lazily wrapped + cached), chain()/snapshot()/runAll()/runAllSettled()
as pinned, per-chain `blocks` overrides, deterministic lowest-chainId error
selection on runAll failure, and onPin firing once per chain.

[v5]: duplicate-instance validation now scans the whole flattened plan
(every chain's task array) before any chain begins execution, rejecting a
branded task instance reused across two chains up front instead of letting
two chains race to consume it.

Raises the bundle-size gzip ceiling 15KB -> 18KB (measured delta: +2.72KB
gzip) — a whole-library budget, not per-consumer cost, since
"sideEffects": false lets tree-shaking consumers drop this entirely if
unused. Updates the README bundle-size badge to match.
…ion, shared brand check

P1: snapshot() invoked executor.getBlockNumber() directly inside .map();
a custom executor throwing SYNCHRONOUSLY (instead of rejecting a promise)
aborted the mapping mid-way, leaving an earlier chain's already-created
promise with no handler attached — a genuine unhandled rejection once that
promise later rejected. Each call is now deferred through
Promise.resolve().then(...) so .map() can never throw, guaranteeing every
promise gets its no-op catch attached before any of them actually run.

P2: the flattened cross-chain duplicate scan re-implemented the
SINGLE_USE brand-truthiness check inline instead of sharing it with the
single-chain runners. Extracted isSingleUseTask() in src/core/internal.ts
as the one shared predicate, now used by rejectDuplicateInstances,
markTasksConsumed, and the multichain flattened scan alike.

Bundle-size ceiling stays at 18KB (measured +0.46KB gzip for this round);
README badge updated to match.
@halaprix
halaprix merged commit 17cd30f into main Jul 23, 2026
2 checks passed
@halaprix
halaprix deleted the feat/f9-multichain branch July 23, 2026 19:38
halaprix added a commit that referenced this pull request Jul 23, 2026
* chore: release v1.3.0

- Version bump 1.2.0 → 1.3.0 in package.json and npm-lock
- CHANGELOG.md: [1.3.0] entry with facts verified against git log v1.2.0..HEAD (PRs #25, #26, #27):
  * Added: MultichainResolver, examples/refinance.ts
  * Changed: handlers reimplemented on defineTask (parity-gated, observable deltas documented)
  * Bundle size 15→18KB (current 16.8KB measured)
- README.md: updated bundle badge, added MultichainResolver to API table, added Multichain section with compiling example and refinance.ts pointer
- MIGRATION.md: appended v1.2.x → v1.3.0 section (additive, no consumer action required)
- SUMMARY.md: updated TOC with new sections
- docs/benchmarks.md: updated size figures (13.9 → 16.8 KB)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: fix API accuracy — multichain, pinBlock, ethers

- README multichain example: replace task instance reuse with factory pattern (flattened duplicate validation throws before any execution)
- api-reference MultichainResolver.chain(): returns MulticallResolver (cached), throws on unknown chainId (not undefined)
- Remove fake per-chain onPin Record — onPin is single inherited callback from BatchOptions; each chain reports once
- Remove fake chainId < 1 validation rule — real rule: lowest-chainId rejection on concurrent failures
- docs/index.html: Fix ethers claim — "requires an adapter" (domino doesn't depend on ethers)
- pinBlock performance: Clarify +1 RPC only for absent/tag blocks; explicit blockNumber/blockHash add none
- CHANGELOG: Correct MultichainResolver description (blocks map, not per-chain onPin; deterministic rejection)
- Verify all snippets compile and bundle badge matches measured size

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: halaprix <halaprix@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
halaprix added a commit that referenced this pull request Jul 24, 2026
* feat: MultichainResolver — parallel per-chain execution (F9)

Runs task-shaped plans across several chains at once, fanning out to the
existing single-chain runners (runMultistepTasks/runSettled) per chain:
constructor discriminates StepExecutor vs. Eip1193Provider entries (the
latter lazily wrapped + cached), chain()/snapshot()/runAll()/runAllSettled()
as pinned, per-chain `blocks` overrides, deterministic lowest-chainId error
selection on runAll failure, and onPin firing once per chain.

[v5]: duplicate-instance validation now scans the whole flattened plan
(every chain's task array) before any chain begins execution, rejecting a
branded task instance reused across two chains up front instead of letting
two chains race to consume it.

Raises the bundle-size gzip ceiling 15KB -> 18KB (measured delta: +2.72KB
gzip) — a whole-library budget, not per-consumer cost, since
"sideEffects": false lets tree-shaking consumers drop this entirely if
unused. Updates the README bundle-size badge to match.

* fix: MultichainResolver external review — snapshot() unhandled rejection, shared brand check

P1: snapshot() invoked executor.getBlockNumber() directly inside .map();
a custom executor throwing SYNCHRONOUSLY (instead of rejecting a promise)
aborted the mapping mid-way, leaving an earlier chain's already-created
promise with no handler attached — a genuine unhandled rejection once that
promise later rejected. Each call is now deferred through
Promise.resolve().then(...) so .map() can never throw, guaranteeing every
promise gets its no-op catch attached before any of them actually run.

P2: the flattened cross-chain duplicate scan re-implemented the
SINGLE_USE brand-truthiness check inline instead of sharing it with the
single-chain runners. Extracted isSingleUseTask() in src/core/internal.ts
as the one shared predicate, now used by rejectDuplicateInstances,
markTasksConsumed, and the multichain flattened scan alike.

Bundle-size ceiling stays at 18KB (measured +0.46KB gzip for this round);
README badge updated to match.

---------

Co-authored-by: halaprix <halaprix@users.noreply.github.com>
halaprix added a commit that referenced this pull request Jul 24, 2026
* chore: release v1.3.0

- Version bump 1.2.0 → 1.3.0 in package.json and npm-lock
- CHANGELOG.md: [1.3.0] entry with facts verified against git log v1.2.0..HEAD (PRs #25, #26, #27):
  * Added: MultichainResolver, examples/refinance.ts
  * Changed: handlers reimplemented on defineTask (parity-gated, observable deltas documented)
  * Bundle size 15→18KB (current 16.8KB measured)
- README.md: updated bundle badge, added MultichainResolver to API table, added Multichain section with compiling example and refinance.ts pointer
- MIGRATION.md: appended v1.2.x → v1.3.0 section (additive, no consumer action required)
- SUMMARY.md: updated TOC with new sections
- docs/benchmarks.md: updated size figures (13.9 → 16.8 KB)


* docs: fix API accuracy — multichain, pinBlock, ethers

- README multichain example: replace task instance reuse with factory pattern (flattened duplicate validation throws before any execution)
- api-reference MultichainResolver.chain(): returns MulticallResolver (cached), throws on unknown chainId (not undefined)
- Remove fake per-chain onPin Record — onPin is single inherited callback from BatchOptions; each chain reports once
- Remove fake chainId < 1 validation rule — real rule: lowest-chainId rejection on concurrent failures
- docs/index.html: Fix ethers claim — "requires an adapter" (domino doesn't depend on ethers)
- pinBlock performance: Clarify +1 RPC only for absent/tag blocks; explicit blockNumber/blockHash add none
- CHANGELOG: Correct MultichainResolver description (blocks map, not per-chain onPin; deterministic rejection)
- Verify all snippets compile and bundle badge matches measured size


---------

Co-authored-by: halaprix <halaprix@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
halaprix added a commit that referenced this pull request Jul 24, 2026
* feat: MultichainResolver — parallel per-chain execution (F9)

Runs task-shaped plans across several chains at once, fanning out to the
existing single-chain runners (runMultistepTasks/runSettled) per chain:
constructor discriminates StepExecutor vs. Eip1193Provider entries (the
latter lazily wrapped + cached), chain()/snapshot()/runAll()/runAllSettled()
as pinned, per-chain `blocks` overrides, deterministic lowest-chainId error
selection on runAll failure, and onPin firing once per chain.

[v5]: duplicate-instance validation now scans the whole flattened plan
(every chain's task array) before any chain begins execution, rejecting a
branded task instance reused across two chains up front instead of letting
two chains race to consume it.

Raises the bundle-size gzip ceiling 15KB -> 18KB (measured delta: +2.72KB
gzip) — a whole-library budget, not per-consumer cost, since
"sideEffects": false lets tree-shaking consumers drop this entirely if
unused. Updates the README bundle-size badge to match.

* fix: MultichainResolver external review — snapshot() unhandled rejection, shared brand check

P1: snapshot() invoked executor.getBlockNumber() directly inside .map();
a custom executor throwing SYNCHRONOUSLY (instead of rejecting a promise)
aborted the mapping mid-way, leaving an earlier chain's already-created
promise with no handler attached — a genuine unhandled rejection once that
promise later rejected. Each call is now deferred through
Promise.resolve().then(...) so .map() can never throw, guaranteeing every
promise gets its no-op catch attached before any of them actually run.

P2: the flattened cross-chain duplicate scan re-implemented the
SINGLE_USE brand-truthiness check inline instead of sharing it with the
single-chain runners. Extracted isSingleUseTask() in src/core/internal.ts
as the one shared predicate, now used by rejectDuplicateInstances,
markTasksConsumed, and the multichain flattened scan alike.

Bundle-size ceiling stays at 18KB (measured +0.46KB gzip for this round);
README badge updated to match.

---------

Co-authored-by: halaprix <halaprix@users.noreply.github.com>
halaprix added a commit that referenced this pull request Jul 24, 2026
* chore: release v1.3.0

- Version bump 1.2.0 → 1.3.0 in package.json and npm-lock
- CHANGELOG.md: [1.3.0] entry with facts verified against git log v1.2.0..HEAD (PRs #25, #26, #27):
  * Added: MultichainResolver, examples/refinance.ts
  * Changed: handlers reimplemented on defineTask (parity-gated, observable deltas documented)
  * Bundle size 15→18KB (current 16.8KB measured)
- README.md: updated bundle badge, added MultichainResolver to API table, added Multichain section with compiling example and refinance.ts pointer
- MIGRATION.md: appended v1.2.x → v1.3.0 section (additive, no consumer action required)
- SUMMARY.md: updated TOC with new sections
- docs/benchmarks.md: updated size figures (13.9 → 16.8 KB)


* docs: fix API accuracy — multichain, pinBlock, ethers

- README multichain example: replace task instance reuse with factory pattern (flattened duplicate validation throws before any execution)
- api-reference MultichainResolver.chain(): returns MulticallResolver (cached), throws on unknown chainId (not undefined)
- Remove fake per-chain onPin Record — onPin is single inherited callback from BatchOptions; each chain reports once
- Remove fake chainId < 1 validation rule — real rule: lowest-chainId rejection on concurrent failures
- docs/index.html: Fix ethers claim — "requires an adapter" (domino doesn't depend on ethers)
- pinBlock performance: Clarify +1 RPC only for absent/tag blocks; explicit blockNumber/blockHash add none
- CHANGELOG: Correct MultichainResolver description (blocks map, not per-chain onPin; deterministic rejection)
- Verify all snippets compile and bundle badge matches measured size


---------

Co-authored-by: halaprix <halaprix@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant