From ca990cae05330cdac256fc862fe10f742b426a9a Mon Sep 17 00:00:00 2001 From: halaprix Date: Thu, 23 Jul 2026 20:58:41 +0200 Subject: [PATCH 1/3] chore: release v1.2.0 Bump package version to 1.2.0 and update CHANGELOG with new features (F6a concurrency pool, F6b adaptive bisection, F7 dedup/Presets, F8 pinBlock). Updates release link reference at bottom of CHANGELOG. Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 14 ++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1897a53..ade377b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,19 @@ All notable changes to this project will be documented in this file. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## [1.2.0] — 2026-07-23 + +### Added +- `maxConcurrentBatches` (default 1): within-step concurrency pool for physical batch dispatch; completion-order-independent result routing; fail-fast cancellation for `run` (queued batches undispatched, in-flight settle, deterministic error selection); `runSettled` never cancels queued batches. +- `adaptiveBatching` (default false): bisection of transport-failed batches through the central queue, bounded by `maxBatchAttempts` (default `2·⌈log₂(batchSize)⌉+1`, every execution counts; exhaustion → coarse `kind: 'batch'` failures with `cause` = last transport error, never wrong data); deadlock-free by construction; reverts never retried. +- `dedupe` (default false): within-step cross-task merge of eligible calls keyed on `(target, calldata, canonical output signature)` with selector-resolved overloads; fan-out of success and failure to all subscribers; only `TypedCallSpec` calls eligible (legacy tasks never affected). +- `Presets.throughput = { maxConcurrentBatches: 5, adaptiveBatching: true, dedupe: true }` — ready-made preset for portfolio/index workloads. +- `pinBlock` (default false) + `onPin(pinnedBlock)` + `PinnedBlock` type + optional `StepExecutor.getBlockNumber` (Eip1193Executor implements): resolve one concrete block at run start (+1 RT) and reuse for every step; pending unsupported; explicit blockNumber/blockHash are no-ops; onPin fires exactly once per run. + +### Changed +- Bundle budget is gzip-only (<15KB); current 13.9 KB gzip. +- Internal: `run`/`runSettled` unified onto one step engine + pool (no observable change at defaults; defaults byte-compatible with 1.1 — pinned by the compat suite). + ## [1.1.0] — 2026-07-23 ### Added @@ -105,6 +118,7 @@ First public release of `@halaprix/domino`. - `position.assets` is `bigint | undefined` — correctly represents the case where `balanceOf` succeeds but `convertToAssets` reverts. - CI now runs real `tsc --noEmit` (typecheck); build step runs before tests so `dist/` exists for bundle-size checks on a clean checkout. +[1.2.0]: https://github.com/halaprix/domino/releases/tag/v1.2.0 [1.1.0]: https://github.com/halaprix/domino/releases/tag/v1.1.0 [1.0.1]: https://github.com/halaprix/domino/releases/tag/v1.0.1 [1.0.0]: https://github.com/halaprix/domino/releases/tag/v1.0.0 diff --git a/package-lock.json b/package-lock.json index 41cb194..04e974c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@halaprix/domino", - "version": "1.1.0", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@halaprix/domino", - "version": "1.1.0", + "version": "1.2.0", "license": "MIT", "dependencies": { "abitype": "^1.2.4", diff --git a/package.json b/package.json index 0d0e038..3181197 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@halaprix/domino", - "version": "1.1.0", + "version": "1.2.0", "private": false, "files": [ "dist", From c1a2f4856d62f13f98f4e54d76f89b463d8c507a Mon Sep 17 00:00:00 2001 From: halaprix Date: Thu, 23 Jul 2026 20:58:44 +0200 Subject: [PATCH 2/3] docs: v1.2.0 coherence pass MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix stale bisection line (now shipped), add Batching Options section with all fields, defaults, and rationale blocks. Update bundle figures (10.9KB → 13.9KB gzip) in benchmarks and API Surface. Verify Atomicity section reflects F8 pinBlock behavior. Co-Authored-By: Claude Fable 5 --- docs/api-reference.md | 31 +++++++++++++++++++++++++++++-- docs/benchmarks.md | 6 +++--- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/docs/api-reference.md b/docs/api-reference.md index b832bbc..b8197db 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -288,11 +288,38 @@ Always present on both branches — never optional, even when empty (`{ optional - **Hand-written `MultistepTask`:** rejected iff the task's own `buildStepCalls`/`consumeStepResults`/`finalize` throws (the "dead-task rule" — once one of these three throws for a task, no further hooks run for it, and it settles `{ status: 'rejected', error: }`; sibling tasks are unaffected). - **Programmer errors reject the WHOLE call, not one task's settlement:** an invalid `batchSize`, a reused single-use task (`DominoTaskReuseError`), or a `StepExecutor` that resolves with the wrong number of results for a batch all reject the returned promise itself — none of these produce a `{ status: 'rejected' }` array entry. -### Batch-failure isolation (and its pre-1.2 limitation) +### Batch-failure isolation and adaptive bisection When `executor.executeMulticall` rejects for a physical batch, `runSettled` does not throw. Every call in that batch becomes a failure carrying its own `DominoCallError` (`kind: 'batch'`) — every one of those errors shares the SAME underlying transport error as `cause`, but each call gets its own `DominoCallError` instance. Execution continues: later batches in the same step, and later steps, still run; tasks whose `finalize()` copes with the resulting failures (or `defineTask` tasks where none of the failed calls are reachable from the returned shape) still settle `fulfilled`. -**Current limitation:** failure isolation operates at physical-batch granularity, not per-call. If one call inside a failed batch would have succeeded on its own (say, only some other call in that batch triggered a malformed response or the RPC timed out), every call in that batch — including the ones that would have succeeded — is reported failed. Adaptive bisection (splitting a failed batch into smaller batches and retrying, to isolate exactly which calls failed) is planned for a future 1.2 release and is not implemented yet. +Adaptive bisection (`adaptiveBatching: true`, off by default) automatically splits a failed batch in half and retries both halves independently, recursing until every call is isolated to its own single-call execution or `maxBatchAttempts` is exhausted. See the [`adaptiveBatching` field](#batching-options) below for when to enable it and its failure semantics. + +## Batching Options + +`BatchOptions` — passed as the third parameter to `runMultistepTasks(executor, tasks, options)` — controls step batching, concurrency, adaptive isolation, deduplication, block pinning, and error handling. All fields are optional; defaults are shown below. + +| Field | Type | Default | Rationale | +|---|---|---|---| +| `batchSize` | `number` | `100` | Multicall3 aggregate3 has per-call gas limits. Split large steps into sequential batches to stay under the limit. Must be positive. | +| `maxConcurrentBatches` | `number` | `1` | Provider rate limits and executor design typically assume serial execution. Set to >1 only if your provider/executor supports parallelism; `run` fail-fast cancels queued batches, `runSettled` does not. | +| `adaptiveBatching` | `boolean` | `false` | Bisection cannot distinguish a per-call failure (e.g. out-of-gas) from a transient transport problem (e.g. HTTP 429 rate-limiting); under rate-limiting, retries amplify load by up to 2N−1 calls for a single original batch. Enable only when your transport failures are dominated by bad calls, not rate limits; a future failure-cause classification may allow enabling by default. | +| `maxBatchAttempts` | `number` | `2·⌈log₂(batchSize)⌉+1` | Bounds bisection recursion. Default is sized for the common single-bad-call case; batches with multiple failing calls may exhaust it before every one is isolated (producing coarser-grained `kind: 'batch'` failures instead, never wrong data). | +| `dedupe` | `boolean` | `false` | Cross-task call merging changes executor invocation counts seen by instrumentation/billing. Only `TypedCallSpec` calls (from `t.call` in `defineTask`) are ever eligible; legacy tasks are never affected. | +| `block` | `BlockParam` | `{ blockTag: 'latest' }` | Block to query at (EIP-1898). Every step's `executeMulticall` call uses this parameter; without `pinBlock: true`, stable tags like `'latest'` are re-resolved by the node on each separate `eth_call`, so the chain can advance between steps. | +| `pinBlock` | `boolean` | `false` | Resolve one concrete block at run start (+1 RPC) and reuse for every step, closing the "chain advanced between steps" gap. Requires `executor.getBlockNumber()`. | +| `onPin` | `(block: PinnedBlock) => void` | — | Synchronous callback reporting the resolved block (fired exactly once per run when `pinBlock: true`). Only invoked when `pinBlock: true`; supplying it without `pinBlock` is accepted but it is never called. | + +See `Presets.throughput` (exports: `{ maxConcurrentBatches: 5, adaptiveBatching: true, dedupe: true }`) for a ready-made bundle suited to portfolio workloads: + +```typescript +import { runMultistepTasks, Presets } from "@halaprix/domino" +import type { StepExecutor, MultistepTask } from "@halaprix/domino" + +declare const executor: StepExecutor +declare const tasks: MultistepTask[] + +await runMultistepTasks(executor, tasks, { ...Presets.throughput, batchSize: 200 }) +``` ## Error taxonomy diff --git a/docs/benchmarks.md b/docs/benchmarks.md index aa46258..49fecdd 100644 --- a/docs/benchmarks.md +++ b/docs/benchmarks.md @@ -2,7 +2,7 @@ ## Bundle Size -`@halaprix/domino` is a lightweight wrapper. The package size (gzip) is **10.9 KB**. The library requires viem as a hard dependency. +`@halaprix/domino` is a lightweight wrapper. The package size (gzip) is **13.9 KB**. The library requires viem as a hard dependency. ```typescript import { Eip1193Executor, MulticallResolver } from "@halaprix/domino" @@ -15,7 +15,7 @@ const resolver = new MulticallResolver(executor) | Package | Size (gzip) | Notes | |---|---|---| -| `@halaprix/domino` | 10.9 KB | viem is a hard dependency; installed with the package, not bundled into dist | +| `@halaprix/domino` | 13.9 KB | viem is a hard dependency; installed with the package, not bundled into dist | ## Compared to Alternatives @@ -25,7 +25,7 @@ const resolver = new MulticallResolver(executor) | 2-step vault resolution | ✅ | ❌ | ❌ | | Cross-entity step batching | ✅ | ❌ | ❌ | | Framework-agnostic core | ✅ | ❌ | ❌ | -| Size (gzip) | 10.9 KB | ~40 KB | 0 (no dep) | +| Size (gzip) | 13.9 KB | ~40 KB | 0 (no dep) | ## RPC Round-Trip Counts From 7cf010af3a8cf3e87e6aa8a6c2a3d7df54600ed2 Mon Sep 17 00:00:00 2001 From: halaprix Date: Thu, 23 Jul 2026 21:03:17 +0200 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20release=20notes=20accuracy=20?= =?UTF-8?q?=E2=80=94=20error=20determinism,=20bisection=20semantics,=20bat?= =?UTF-8?q?chSize=20limits?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit External review fixes: 1. maxConcurrentBatches: qualify deterministic error selection (only when exactly one batch fails; otherwise timing-dependent). 2. Adaptive bisection: rejected regions recurse until success/single-call/exhausted; successful sub-batches kept immediately. 3. batchSize rationale: provider limits (gas/payload/response), not per-call costs; splitting can't fix one expensive call. Verified against src/core/pool.ts JSDoc comments. --- CHANGELOG.md | 2 +- docs/api-reference.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ade377b..433ee0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [1.2.0] — 2026-07-23 ### Added -- `maxConcurrentBatches` (default 1): within-step concurrency pool for physical batch dispatch; completion-order-independent result routing; fail-fast cancellation for `run` (queued batches undispatched, in-flight settle, deterministic error selection); `runSettled` never cancels queued batches. +- `maxConcurrentBatches` (default 1): within-step concurrency pool for physical batch dispatch; completion-order-independent result routing; fail-fast cancellation for `run` (queued batches undispatched, in-flight settle, deterministic error selection when exactly one batch fails); `runSettled` never cancels queued batches. - `adaptiveBatching` (default false): bisection of transport-failed batches through the central queue, bounded by `maxBatchAttempts` (default `2·⌈log₂(batchSize)⌉+1`, every execution counts; exhaustion → coarse `kind: 'batch'` failures with `cause` = last transport error, never wrong data); deadlock-free by construction; reverts never retried. - `dedupe` (default false): within-step cross-task merge of eligible calls keyed on `(target, calldata, canonical output signature)` with selector-resolved overloads; fan-out of success and failure to all subscribers; only `TypedCallSpec` calls eligible (legacy tasks never affected). - `Presets.throughput = { maxConcurrentBatches: 5, adaptiveBatching: true, dedupe: true }` — ready-made preset for portfolio/index workloads. diff --git a/docs/api-reference.md b/docs/api-reference.md index b8197db..ed1da50 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -292,7 +292,7 @@ Always present on both branches — never optional, even when empty (`{ optional When `executor.executeMulticall` rejects for a physical batch, `runSettled` does not throw. Every call in that batch becomes a failure carrying its own `DominoCallError` (`kind: 'batch'`) — every one of those errors shares the SAME underlying transport error as `cause`, but each call gets its own `DominoCallError` instance. Execution continues: later batches in the same step, and later steps, still run; tasks whose `finalize()` copes with the resulting failures (or `defineTask` tasks where none of the failed calls are reachable from the returned shape) still settle `fulfilled`. -Adaptive bisection (`adaptiveBatching: true`, off by default) automatically splits a failed batch in half and retries both halves independently, recursing until every call is isolated to its own single-call execution or `maxBatchAttempts` is exhausted. See the [`adaptiveBatching` field](#batching-options) below for when to enable it and its failure semantics. +Adaptive bisection (`adaptiveBatching: true`, off by default) automatically splits a failed batch in half and retries both halves independently; rejected regions recurse until they succeed, reach a single call, or exhaust `maxBatchAttempts`, while successful sub-batches are kept immediately. See the [`adaptiveBatching` field](#batching-options) below for when to enable it and its failure semantics. ## Batching Options @@ -300,7 +300,7 @@ Adaptive bisection (`adaptiveBatching: true`, off by default) automatically spli | Field | Type | Default | Rationale | |---|---|---|---| -| `batchSize` | `number` | `100` | Multicall3 aggregate3 has per-call gas limits. Split large steps into sequential batches to stay under the limit. Must be positive. | +| `batchSize` | `number` | `100` | Batching keeps each aggregate `eth_call` under provider gas/payload/response limits. One very expensive call cannot be fixed by splitting; this parameter splits the logical step, not individual calls. Must be positive. | | `maxConcurrentBatches` | `number` | `1` | Provider rate limits and executor design typically assume serial execution. Set to >1 only if your provider/executor supports parallelism; `run` fail-fast cancels queued batches, `runSettled` does not. | | `adaptiveBatching` | `boolean` | `false` | Bisection cannot distinguish a per-call failure (e.g. out-of-gas) from a transient transport problem (e.g. HTTP 429 rate-limiting); under rate-limiting, retries amplify load by up to 2N−1 calls for a single original batch. Enable only when your transport failures are dominated by bad calls, not rate limits; a future failure-cause classification may allow enabling by default. | | `maxBatchAttempts` | `number` | `2·⌈log₂(batchSize)⌉+1` | Bounds bisection recursion. Default is sized for the common single-bad-call case; batches with multiple failing calls may exhaust it before every one is isolated (producing coarser-grained `kind: 'batch'` failures instead, never wrong data). |