xblox 1/2

This commit is contained in:
lovebird 2026-04-07 19:41:32 +02:00
parent 968268d7ca
commit 4c5e7d7690
20 changed files with 336 additions and 78 deletions

View File

@ -2,4 +2,5 @@ node_modules/
dist/
dist-in/
coverage/
reports/
*.log

View File

@ -34,7 +34,7 @@ npx pm-xblox run --source=./graph.json --loglevel=info
| Option | Default | Description |
|--------|---------|-------------|
| `--source` | (required) | Path to a blocks JSON file (`version: 1`). |
| `--output` | — | If set, writes `{ "result": … }` as JSON. |
| `--output` | — | If set, writes `{ "result": …, "results": […] }` as JSON. |
| `--loglevel` | `info` | `debug` · `info` · `warn` · `error` |
## Blocks file format
@ -45,14 +45,20 @@ Top-level shape (see `src/schema/blocks-file.ts`):
{
"version": 1,
"context": {},
"root": { "kind": "…", … }
"roots": [{ "kind": "…", … }, …]
}
```
- **`context`** — plain object merged into the execution `this` (expressions and scripts use **`this`** like the legacy `apply(ctx, args)` model).
- **`root`** — a discriminated union on **`kind`**: `if`, `runScript`, `for`, `while`, `switch`, `break`, plus `case` / `switchDefault` inside `switch.items`.
- **`roots`** — non-empty array of top-level blocks run **sequentially**, in order (shared `ctx`), like statements in a script. A UI can map one row per root to a collapsible tree of children.
**Legacy note:** older UIs often stored a **flat array** of blocks with `id`, `parentId`, `declaredClass`. This package currently executes the **nested** tree; you can add an adapter layer (flat → tree) for editors without changing the executor.
Block **`kind`** values include: `if`, `runScript`, `for`, `while`, `switch`, `break`, `wait`, plus `case` / `switchDefault` inside `switch.items`.
- **`wait`** — `{ "kind": "wait", "ms": 5000 }` pauses for the given milliseconds (**async**; the executor is `async`).
**CLI / API output:** `executeRoots` and `pm-xblox run` return **`results`** (one entry per root) and **`result`** (last roots return value).
**Legacy note:** older UIs often stored a **flat array** of blocks with `id`, `parentId`, `declaredClass`. This package executes **nested trees** under each root; you can add an adapter (flat → `roots`) for editors without changing the executor.
## Expressions and scripts
@ -72,10 +78,13 @@ Top-level shape (see `src/schema/blocks-file.ts`):
| `npm run test:watch` | Vitest watch mode |
| `npm run test:coverage` | Vitest with V8 coverage |
| `npm run test:ui` | Vitest browser UI |
| `npm run test:perf` | Runs `tests/unit/perf-report.test.ts` — prints a **`console.table`** of timings. With **`XBLOX_PERF_REPORT=1`**, also writes **`reports/perf-latest.json`** (gitignored). |
| `npm run lint` | ESLint on `src/` |
| `npm run webpack` | After `build`, emits `dist/main_node.js` from `dist-in/main.js` (default package exports only; **not** `@polymech/xblox/runtime` / vm2). |
Tests live under `tests/`; shared paths use `tests/test-commons.ts`. Fixtures are in `tests/fixtures/`.
Tests live under `tests/`; shared paths and **performance helpers** (`PerfReporter`, `measureMs`, `fixture`) live in `tests/test-commons.ts`. Fixtures are in `tests/fixtures/`.
**Performance:** import `PerfReporter` in any test, wrap steps with `await perf.measure('label', () => …)`, then call `perf.printSummary()` in `afterAll`. Set **`XBLOX_PERF_REPORT=1`** when running tests to also persist **`reports/perf-latest.json`**.
## Publishing to npm

View File

@ -40,6 +40,7 @@
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"test:ui": "vitest --ui",
"test:perf": "vitest run tests/unit/perf-report.test.ts",
"prepack": "npm run build",
"webpack": "webpack --config webpack.config.js --stats-error-details"
},

View File

@ -1,12 +1,14 @@
import { readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { executeBlock } from '../engine/execute-block.js';
import { executeRoots } from '../engine/execute-block.js';
import { blocksFileSchema } from '../schema/blocks-file.js';
import { createLogger } from './logger.js';
import { runArgsSchema } from './run-args.js';
export async function runCommand(raw: Record<string, unknown>): Promise<{ result: unknown }> {
export async function runCommand(
raw: Record<string, unknown>,
): Promise<{ result: unknown; results: unknown[] }> {
const args = runArgsSchema.parse({
source: raw.source,
output: raw.output,
@ -23,11 +25,11 @@ export async function runCommand(raw: Record<string, unknown>): Promise<{ result
const file = blocksFileSchema.parse(json);
const ctx = Object.assign(Object.create(null), file.context) as object;
const result = executeBlock(file.root, ctx);
const { result, results } = await executeRoots(file.roots, ctx);
log.info('Execution finished.');
const payload = { result };
const payload = { result, results };
if (args.output) {
const outPath = path.resolve(args.output);

View File

@ -10,6 +10,10 @@ export type ExecState = {
switchCtl?: { stopped: boolean };
};
function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function isTruthy(value: unknown): boolean {
return Boolean(value);
}
@ -24,7 +28,7 @@ function forStep(counter: number | string, modifier: string, ctx: object): unkno
return evalExpression(expr, ctx);
}
function executeFor(node: Extract<BlockNode, { kind: 'for' }>, ctx: object, state?: ExecState): unknown {
async function executeFor(node: Extract<BlockNode, { kind: 'for' }>, ctx: object, state?: ExecState): Promise<unknown> {
let counter = coerceNumber(evalExpression(node.initial, ctx));
if (Number.isNaN(counter)) {
counter = 0;
@ -32,7 +36,7 @@ function executeFor(node: Extract<BlockNode, { kind: 'for' }>, ctx: object, stat
let last: unknown;
while (forCondition(counter, node.comparator, node.final, ctx)) {
const childState: ExecState = { ...state, loopIndex: counter };
last = runBlockList(node.items, ctx, childState);
last = await runBlockList(node.items, ctx, childState);
const nextRaw = forStep(counter, node.modifier, ctx);
const next = coerceNumber(nextRaw);
if (Number.isNaN(next)) {
@ -46,12 +50,12 @@ function executeFor(node: Extract<BlockNode, { kind: 'for' }>, ctx: object, stat
return last;
}
function executeWhile(node: Extract<BlockNode, { kind: 'while' }>, ctx: object, state?: ExecState): unknown {
async function executeWhile(node: Extract<BlockNode, { kind: 'while' }>, ctx: object, state?: ExecState): Promise<unknown> {
const limit = node.loopLimit ?? 1500;
let iterations = 0;
let last: unknown;
while (isTruthy(evalExpression(node.condition, ctx)) && iterations < limit) {
last = runBlockList(node.items, ctx, state);
last = await runBlockList(node.items, ctx, state);
iterations++;
}
return last;
@ -65,7 +69,7 @@ function matchCase(ctx: object, variable: string, item: Pick<CaseBlockNode, 'com
return isTruthy(evalExpression(combined, ctx));
}
function executeSwitch(node: Extract<BlockNode, { kind: 'switch' }>, ctx: object, state?: ExecState): unknown {
async function executeSwitch(node: Extract<BlockNode, { kind: 'switch' }>, ctx: object, state?: ExecState): Promise<unknown> {
const switchCtl = state?.switchCtl ?? { stopped: false };
const childState: ExecState = { ...state, switchCtl };
let anyMatched = false;
@ -74,7 +78,7 @@ function executeSwitch(node: Extract<BlockNode, { kind: 'switch' }>, ctx: object
for (const item of node.items) {
if (item.kind === 'case') {
if (matchCase(ctx, node.variable, item)) {
last = runBlockList(item.consequent, ctx, childState);
last = await runBlockList(item.consequent, ctx, childState);
anyMatched = true;
break;
}
@ -87,7 +91,7 @@ function executeSwitch(node: Extract<BlockNode, { kind: 'switch' }>, ctx: object
if (!anyMatched) {
for (const item of node.items) {
if (item.kind === 'switchDefault') {
last = runBlockList(item.consequent, ctx, childState);
last = await runBlockList(item.consequent, ctx, childState);
}
}
}
@ -95,56 +99,61 @@ function executeSwitch(node: Extract<BlockNode, { kind: 'switch' }>, ctx: object
return last;
}
export function runBlockList(blocks: BlockNode[], ctx: object, state?: ExecState): unknown {
export async function runBlockList(blocks: BlockNode[], ctx: object, state?: ExecState): Promise<unknown> {
let last: unknown;
for (const b of blocks) {
last = executeBlock(b, ctx, state);
last = await executeBlock(b, ctx, state);
}
return last;
}
/**
* Execute a tree starting at `root` with `ctx` as `this` in expressions and runScript blocks.
* Execute one block node with `ctx` as `this` in expressions and runScript blocks.
*/
export function executeBlock(root: BlockNode, ctx: object, state?: ExecState): unknown {
if (root.kind === 'runScript') {
return runRunScriptBlock(new RunScriptBlock({ method: root.method }), ctx, []);
export async function executeBlock(node: BlockNode, ctx: object, state?: ExecState): Promise<unknown> {
if (node.kind === 'runScript') {
return runRunScriptBlock(new RunScriptBlock({ method: node.method }), ctx, []);
}
if (root.kind === 'break') {
if (node.kind === 'wait') {
await delay(node.ms);
return undefined;
}
if (node.kind === 'break') {
if (state?.switchCtl) {
state.switchCtl.stopped = true;
}
return undefined;
}
if (root.kind === 'for') {
return executeFor(root, ctx, state);
if (node.kind === 'for') {
return executeFor(node, ctx, state);
}
if (root.kind === 'while') {
return executeWhile(root, ctx, state);
if (node.kind === 'while') {
return executeWhile(node, ctx, state);
}
if (root.kind === 'switch') {
return executeSwitch(root, ctx, state);
if (node.kind === 'switch') {
return executeSwitch(node, ctx, state);
}
if (root.kind === 'if') {
if (isTruthy(evalExpression(root.condition, ctx))) {
return runBlockList(root.consequent, ctx, state);
if (node.kind === 'if') {
if (isTruthy(evalExpression(node.condition, ctx))) {
return runBlockList(node.consequent, ctx, state);
}
if (root.elseIfBlocks) {
for (const branch of root.elseIfBlocks) {
if (node.elseIfBlocks) {
for (const branch of node.elseIfBlocks) {
if (isTruthy(evalExpression(branch.condition, ctx))) {
return runBlockList(branch.consequent, ctx, state);
}
}
}
if (root.alternate?.length) {
return runBlockList(root.alternate, ctx, state);
if (node.alternate?.length) {
return runBlockList(node.alternate, ctx, state);
}
return undefined;
@ -152,3 +161,21 @@ export function executeBlock(root: BlockNode, ctx: object, state?: ExecState): u
return undefined;
}
export type ExecuteRootsResult = {
/** Return value of each top-level block, in order. */
results: unknown[];
/** Last top-level return (same as `results[results.length - 1]`). */
result: unknown;
};
/** Run many top-level blocks in order (shared `ctx`), e.g. UI “run all” / script-like flow. */
export async function executeRoots(roots: BlockNode[], ctx: object): Promise<ExecuteRootsResult> {
const results: unknown[] = [];
let result: unknown;
for (const block of roots) {
result = await executeBlock(block, ctx);
results.push(result);
}
return { results, result };
}

View File

@ -3,3 +3,11 @@ export * from './types.js';
export * from './uuid.js';
export * from './model/model-base.js';
export * from './blocks/run-script-block.js';
export {
executeBlock,
executeRoots,
runBlockList,
type ExecState,
type ExecuteRootsResult,
} from './engine/execute-block.js';
export { blocksFileSchema, type BlocksFile, type BlockNode } from './schema/blocks-file.js';

View File

@ -60,6 +60,11 @@ export type BlockNode =
/** Legacy `xblox.model.logic.BreakBlock`. */
| {
kind: 'break';
}
/** Async delay between steps (milliseconds). */
| {
kind: 'wait';
ms: number;
};
export const blockNodeSchema: z.ZodType<BlockNode> = z.lazy(() =>
@ -119,13 +124,17 @@ export const blockNodeSchema: z.ZodType<BlockNode> = z.lazy(() =>
z.object({
kind: z.literal('break'),
}),
z.object({
kind: z.literal('wait'),
ms: z.number().nonnegative().finite(),
}),
]),
);
export const blocksFileSchema = z.object({
version: z.literal(1),
context: z.record(z.string(), z.unknown()).optional().default({}),
root: blockNodeSchema,
roots: z.array(blockNodeSchema).min(1),
});
export type BlocksFile = z.infer<typeof blocksFileSchema>;

View File

@ -1,7 +1,8 @@
{
"version": 1,
"context": { "x": 0 },
"root": {
"roots": [
{
"kind": "for",
"initial": "0",
"final": "3",
@ -13,5 +14,6 @@
"method": "this.x = (this.x || 0) + 1; return this.x;"
}
]
}
}
]
}

View File

@ -1,7 +1,8 @@
{
"version": 1,
"context": { "n": 0 },
"root": {
"roots": [
{
"kind": "if",
"condition": "this.n > 0",
"consequent": [{ "kind": "runScript", "method": "return 'pos';" }],
@ -12,5 +13,6 @@
}
],
"alternate": [{ "kind": "runScript", "method": "return 'zero';" }]
}
}
]
}

View File

@ -1,7 +1,8 @@
{
"version": 1,
"context": { "n": -3 },
"root": {
"roots": [
{
"kind": "if",
"condition": "this.n > 0",
"consequent": [{ "kind": "runScript", "method": "return 'pos';" }],
@ -12,5 +13,6 @@
}
],
"alternate": [{ "kind": "runScript", "method": "return 'zero';" }]
}
}
]
}

View File

@ -0,0 +1,18 @@
{
"version": 1,
"context": { "steps": [] },
"roots": [
{
"kind": "runScript",
"method": "this.steps = this.steps || []; this.steps.push('a'); return 'a';"
},
{
"kind": "wait",
"ms": 5000
},
{
"kind": "runScript",
"method": "this.steps.push('b'); return 'b';"
}
]
}

View File

@ -1,7 +1,8 @@
{
"version": 1,
"context": { "mode": 2 },
"root": {
"roots": [
{
"kind": "switch",
"variable": "mode",
"items": [
@ -22,5 +23,6 @@
"consequent": [{ "kind": "runScript", "method": "return 'def';" }]
}
]
}
}
]
}

View File

@ -1,7 +1,8 @@
{
"version": 1,
"context": { "mode": 99 },
"root": {
"roots": [
{
"kind": "switch",
"variable": "mode",
"items": [
@ -16,5 +17,6 @@
"consequent": [{ "kind": "runScript", "method": "return 'def';" }]
}
]
}
}
]
}

View File

@ -1,7 +1,8 @@
{
"version": 1,
"context": { "n": 0 },
"root": {
"roots": [
{
"kind": "while",
"condition": "this.n < 3",
"items": [
@ -10,5 +11,6 @@
"method": "this.n++; return this.n;"
}
]
}
}
]
}

View File

@ -1,7 +1,8 @@
/**
* Shared paths and helpers for vitest (import from tests only).
* Shared paths, performance measurement, and optional JSON reports for vitest.
*/
import { mkdirSync, writeFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
@ -12,6 +13,97 @@ export const testsDir = __dirname;
export const fixturesDir = path.join(testsDir, 'fixtures');
/** Default JSON output: `packages/xblox/reports/perf-latest.json` (gitignored). */
export const defaultPerfReportPath = path.join(testsDir, '..', 'reports', 'perf-latest.json');
export function fixture(...parts: string[]): string {
return path.join(fixturesDir, ...parts);
}
// ——— performance ———
export type PerfEntry = {
/** Logical label (e.g. suite step name). */
name: string;
/** Duration in milliseconds (`performance.now()` delta). */
ms: number;
/** ISO timestamp when the measurement finished. */
at: string;
};
/** When set to `1`, `PerfReporter.writeJsonFile()` writes to disk (see `npm run test:perf`). */
export function isPerfReportEnabled(): boolean {
return process.env.XBLOX_PERF_REPORT === '1';
}
/**
* Measure sync or async work; returns the function result and duration.
* Does not record to a report use `PerfReporter.measure` for that.
*/
export async function measureMs<T>(fn: () => T | Promise<T>): Promise<{ result: T; ms: number }> {
const t0 = performance.now();
const result = await fn();
const ms = performance.now() - t0;
return { result, ms };
}
/**
* Collect timings for a test file or suite. Call `printSummary()` in `afterAll`,
* and optionally `writeJsonFile()` when `XBLOX_PERF_REPORT=1`.
*/
export class PerfReporter {
private readonly entries: PerfEntry[] = [];
get snapshot(): readonly PerfEntry[] {
return [...this.entries];
}
/** Run `fn`, record duration under `name`, return `fn`s result. */
async measure<T>(name: string, fn: () => T | Promise<T>): Promise<T> {
const { result, ms } = await measureMs(fn);
this.entries.push({
name,
ms,
at: new Date().toISOString(),
});
return result;
}
/** Log a compact table to stdout (Vitest / CI). */
printSummary(title = 'xblox perf'): void {
if (this.entries.length === 0) {
return;
}
const rows = this.entries.map((e) => ({
name: e.name,
ms: Number(e.ms.toFixed(3)),
at: e.at,
}));
console.info(`\n[${title}]`);
console.table(rows);
const total = this.entries.reduce((s, e) => s + e.ms, 0);
console.info(`[${title}] total: ${total.toFixed(3)} ms\n`);
}
/**
* Write `reports/perf-latest.json` (or `path`). Safe to call from `afterAll`.
* Set env `XBLOX_PERF_REPORT=1` to enable writes; otherwise no-op (avoids churn in CI).
*/
writeJsonFile(filePath: string = defaultPerfReportPath): string | undefined {
if (!isPerfReportEnabled()) {
return undefined;
}
mkdirSync(path.dirname(filePath), { recursive: true });
const payload = {
generatedAt: new Date().toISOString(),
entries: this.entries.map((e) => ({
name: e.name,
ms: e.ms,
at: e.at,
})),
totalMs: this.entries.reduce((s, e) => s + e.ms, 0),
};
writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
return filePath;
}
}

View File

@ -33,6 +33,6 @@ describe('pm-xblox run (runCommand)', () => {
loglevel: 'error',
});
const text = await readFile(outPath, 'utf8');
expect(JSON.parse(text)).toEqual({ result: 'zero' });
expect(JSON.parse(text)).toEqual({ result: 'zero', results: ['zero'] });
});
});

View File

@ -11,13 +11,13 @@ describe('if / else-if / alternate engine', () => {
const raw = JSON.parse(await readFile(fixture('if-else.json'), 'utf8'));
const file = blocksFileSchema.parse(raw);
const ctx = Object.assign(Object.create(null), file.context) as object;
expect(executeBlock(file.root, ctx)).toBe('neg');
expect(await executeBlock(file.roots[0], ctx)).toBe('neg');
});
it('evaluates alternate when no branch matches', async () => {
const raw = JSON.parse(await readFile(fixture('if-else-zero.json'), 'utf8'));
const file = blocksFileSchema.parse(raw);
const ctx = Object.assign(Object.create(null), file.context) as object;
expect(executeBlock(file.root, ctx)).toBe('zero');
expect(await executeBlock(file.roots[0], ctx)).toBe('zero');
});
});

View File

@ -10,7 +10,7 @@ describe('ForBlock (legacy xblox.model.loops.ForBlock)', () => {
const raw = JSON.parse(await readFile(fixture('for-sum.json'), 'utf8'));
const file = blocksFileSchema.parse(raw);
const ctx = Object.assign(Object.create(null), file.context) as object;
expect(executeBlock(file.root, ctx)).toBe(3);
expect(await executeBlock(file.roots[0], ctx)).toBe(3);
expect((ctx as { x: number }).x).toBe(3);
});
});
@ -20,7 +20,7 @@ describe('WhileBlock (legacy xblox.model.loops.WhileBlock)', () => {
const raw = JSON.parse(await readFile(fixture('while-count.json'), 'utf8'));
const file = blocksFileSchema.parse(raw);
const ctx = Object.assign(Object.create(null), file.context) as object;
expect(executeBlock(file.root, ctx)).toBe(3);
expect(await executeBlock(file.roots[0], ctx)).toBe(3);
expect((ctx as { n: number }).n).toBe(3);
});
});
@ -30,14 +30,14 @@ describe('SwitchBlock / CaseBlock / DefaultBlock (legacy logic)', () => {
const raw = JSON.parse(await readFile(fixture('switch-case.json'), 'utf8'));
const file = blocksFileSchema.parse(raw);
const ctx = Object.assign(Object.create(null), file.context) as object;
expect(executeBlock(file.root, ctx)).toBe('two');
expect(await executeBlock(file.roots[0], ctx)).toBe('two');
});
it('runs switchDefault when no case matches', async () => {
const raw = JSON.parse(await readFile(fixture('switch-default-only.json'), 'utf8'));
const file = blocksFileSchema.parse(raw);
const ctx = Object.assign(Object.create(null), file.context) as object;
expect(executeBlock(file.root, ctx)).toBe('def');
expect(await executeBlock(file.roots[0], ctx)).toBe('def');
});
});
@ -46,28 +46,30 @@ describe('BreakBlock', () => {
const file = blocksFileSchema.parse({
version: 1,
context: { mode: 1 },
root: {
kind: 'switch',
variable: 'mode',
items: [
{
kind: 'case',
comparator: '==',
expression: '1',
consequent: [
{ kind: 'runScript', method: "return 'hit';" },
{ kind: 'break' },
{ kind: 'runScript', method: "return 'after';" },
],
},
{
kind: 'switchDefault',
consequent: [{ kind: 'runScript', method: "return 'def';" }],
},
],
},
roots: [
{
kind: 'switch',
variable: 'mode',
items: [
{
kind: 'case',
comparator: '==',
expression: '1',
consequent: [
{ kind: 'runScript', method: "return 'hit';" },
{ kind: 'break' },
{ kind: 'runScript', method: "return 'after';" },
],
},
{
kind: 'switchDefault',
consequent: [{ kind: 'runScript', method: "return 'def';" }],
},
],
},
],
});
const ctx = Object.assign(Object.create(null), file.context) as object;
expect(executeBlock(file.root, ctx)).toBe('after');
expect(await executeBlock(file.roots[0], ctx)).toBe('after');
});
});

View File

@ -0,0 +1,30 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { readFile } from 'node:fs/promises';
import { executeRoots } from '../../src/engine/execute-block.js';
import { blocksFileSchema } from '../../src/schema/blocks-file.js';
import { fixture } from '../test-commons.js';
describe('multiple roots + wait', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('runs each top-level block in order; wait(ms) is async (5000ms)', async () => {
const raw = JSON.parse(await readFile(fixture('multi-root-wait.json'), 'utf8'));
const file = blocksFileSchema.parse(raw);
const ctx = Object.assign(Object.create(null), file.context) as { steps: string[] };
const run = executeRoots(file.roots, ctx);
await vi.advanceTimersByTimeAsync(5000);
const out = await run;
expect(out.results).toEqual(['a', undefined, 'b']);
expect(out.result).toBe('b');
expect(ctx.steps).toEqual(['a', 'b']);
});
});

View File

@ -0,0 +1,47 @@
import { describe, it, expect, afterAll } from 'vitest';
import { readFile } from 'node:fs/promises';
import { executeRoots } from '../../src/engine/execute-block.js';
import { blocksFileSchema } from '../../src/schema/blocks-file.js';
import { fixture, PerfReporter } from '../test-commons.js';
const perf = new PerfReporter();
describe('performance (test-commons reporter)', () => {
afterAll(() => {
perf.printSummary('xblox');
perf.writeJsonFile();
});
it('parses and runs representative fixtures under measurement', async () => {
const ifElseFile = await perf.measure('parse if-else.json', async () => {
const raw = JSON.parse(await readFile(fixture('if-else.json'), 'utf8'));
return blocksFileSchema.parse(raw);
});
const ctx1 = Object.assign(Object.create(null), ifElseFile.context) as object;
await perf.measure('executeRoots if-else', async () => {
return executeRoots(ifElseFile.roots, ctx1);
});
const multiRaw = await perf.measure('parse multi-root-wait.json', async () => {
const raw = JSON.parse(await readFile(fixture('multi-root-wait.json'), 'utf8'));
return blocksFileSchema.parse(raw);
});
const ctx2 = Object.assign(Object.create(null), multiRaw.context) as object;
await perf.measure('executeRoots multi-root (sync parts only)', async () => {
return executeRoots(
[
multiRaw.roots[0],
{ kind: 'wait' as const, ms: 0 },
multiRaw.roots[2],
],
ctx2,
);
});
expect(ifElseFile.roots).toHaveLength(1);
expect(multiRaw.roots).toHaveLength(3);
});
});