From c0129d38de7859c2494a401dbd31736451908e55 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sat, 18 Oct 2025 04:36:50 -0400 Subject: [PATCH 01/45] Add task to generate v4 importmap and update dependencies - Created tasks/generate-importmap.ts to automatically collect all external dependencies from workspace packages - Added "generate-importmap" task to root deno.json - Replaced @gordonb/pipe with remeda in watch package - Updated all remeda versions to ^2 - Added ws dependency to websocket package - Added v4.importmap.json to .gitignore (generated file) --- .gitignore | 1 + deno.json | 5 +++- stream-helpers/deno.json | 2 +- tasks/generate-importmap.ts | 55 +++++++++++++++++++++++++++++++++++++ watch/deno.json | 2 +- watch/watch.ts | 2 +- 6 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 tasks/generate-importmap.ts diff --git a/.gitignore b/.gitignore index 6847cb4a..85db91de 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /deno.lock /**/build/ +/v4.importmap.json .vscode \ No newline at end of file diff --git a/deno.json b/deno.json index 8f1d9f28..51effdd5 100644 --- a/deno.json +++ b/deno.json @@ -39,5 +39,8 @@ "./stream-helpers", "./signals", "./process" - ] + ], + "tasks": { + "generate-importmap": "deno run --allow-read --allow-write tasks/generate-importmap.ts" + } } diff --git a/stream-helpers/deno.json b/stream-helpers/deno.json index cf2cfa9f..2c050c95 100644 --- a/stream-helpers/deno.json +++ b/stream-helpers/deno.json @@ -8,7 +8,7 @@ "@std/expect": "jsr:@std/expect@^1", "@std/testing": "jsr:@std/testing@^1", "@effectionx/bdd": "jsr:@effectionx/bdd@0.2.2", - "remeda": "npm:remeda@2.21.3" + "remeda": "npm:remeda@^2" }, "license": "MIT", "exports": { diff --git a/tasks/generate-importmap.ts b/tasks/generate-importmap.ts new file mode 100644 index 00000000..0564ded5 --- /dev/null +++ b/tasks/generate-importmap.ts @@ -0,0 +1,55 @@ +#!/usr/bin/env -S deno run --allow-read --allow-write + +import { expandGlob } from "jsr:@std/fs@^1"; + +interface DenoConfig { + imports?: Record; +} + +const rootDir = new URL("..", import.meta.url).pathname; +const imports = new Map(); + +// Walk through all deno.json files +for await (const file of expandGlob("**/deno.json", { + root: rootDir, + exclude: ["node_modules", "build"], +})) { + const content = await Deno.readTextFile(file.path); + const config: DenoConfig = JSON.parse(content); + + if (config.imports) { + for (const [key, value] of Object.entries(config.imports)) { + // Skip internal @effectionx packages unless they use npm: + if (key.startsWith("@effectionx/") && !value.startsWith("npm:")) { + continue; + } + + // Only include external dependencies (jsr: or npm:) + if (value.startsWith("jsr:") || value.startsWith("npm:")) { + imports.set(key, value); + } + } + } +} + +// Add effection v4 +imports.set("effection", "npm:effection@^4.0.0-0"); + +// Sort by key +const sortedImports = Object.fromEntries( + Array.from(imports.entries()).sort(([a], [b]) => a.localeCompare(b)) +); + +const output = { + "// To regenerate this file": "run: deno task generate-importmap", + "// Description": "Collects all unique external dependencies from workspace packages' deno.json files", + imports: sortedImports, +}; + +const outputPath = `${rootDir}/v4.importmap.json`; +await Deno.writeTextFile( + outputPath, + JSON.stringify(output, null, 2) + "\n" +); + +console.log(`Generated ${outputPath} with ${imports.size} dependencies`); diff --git a/watch/deno.json b/watch/deno.json index bc216443..06b652cf 100644 --- a/watch/deno.json +++ b/watch/deno.json @@ -11,7 +11,7 @@ "ignore": "npm:ignore@^7.0.3", "@std/expect": "jsr:@std/expect@^1", "@std/assert": "jsr:@std/assert@^1", - "@gordonb/pipe": "jsr:@gordonb/pipe@0.1.0", + "remeda": "npm:remeda@^2", "@std/fs": "jsr:@std/fs@^1", "@std/path": "jsr:@std/path@^1", "chokidar": "npm:chokidar@^4.0.3", diff --git a/watch/watch.ts b/watch/watch.ts index 9dd7b279..3baa5fee 100644 --- a/watch/watch.ts +++ b/watch/watch.ts @@ -16,7 +16,7 @@ import { withResolvers, } from "effection"; import { default as createIgnore } from "ignore"; -import { pipe } from "@gordonb/pipe"; +import { pipe } from "remeda"; import { readFile } from "node:fs/promises"; import { exec, type ExecOptions, type Process } from "@effectionx/process"; From 2f89e28fd6eb12ce8d3c87397e416cf5ecd65972 Mon Sep 17 00:00:00 2001 From: Jacob Bolda Date: Mon, 21 Apr 2025 17:21:37 -0500 Subject: [PATCH 02/45] convert fx to effection v4 --- fx/parallel.test.ts | 7 ++++--- fx/parallel.ts | 12 +++++------- fx/race.test.ts | 4 ++-- fx/race.ts | 44 ++++++++++++++++++++++++++------------------ fx/request.test.ts | 2 +- fx/safe.ts | 9 +++++---- 6 files changed, 43 insertions(+), 35 deletions(-) diff --git a/fx/parallel.test.ts b/fx/parallel.test.ts index 7bfcfba7..d56a1460 100644 --- a/fx/parallel.test.ts +++ b/fx/parallel.test.ts @@ -1,6 +1,6 @@ import { describe, it } from "@effectionx/bdd"; import { expect } from "@std/expect"; -import { each, Err, Ok, sleep, spawn } from "effection"; +import { each, Err, Ok, sleep, spawn, until } from "effection"; import { parallel } from "./parallel.ts"; @@ -101,7 +101,8 @@ describe("parallel()", () => { yield* sleep(15); two.resolve(2); }); - const results = yield* parallel([one, () => two.promise]); + + const results = yield* parallel([one, () => until(two.promise)]); expect(yield* results).toEqual([Ok(1), Ok(2)]); }); @@ -112,7 +113,7 @@ describe("parallel()", () => { function* genFn() { try { - const results = yield* parallel([() => one.promise, () => two.promise]); + const results = yield* parallel([() => until(one.promise), () => until(two.promise)]); actual = yield* results; } catch (_) { actual = [Err(new Error("should not get hit"))]; diff --git a/fx/parallel.ts b/fx/parallel.ts index c7e2ee5a..e10007b1 100644 --- a/fx/parallel.ts +++ b/fx/parallel.ts @@ -1,10 +1,9 @@ -import type { Callable, Channel, Operation, Result } from "effection"; +import type { Channel, Operation, Result, Task } from "effection"; import { createChannel, resource, spawn } from "effection"; import { safe } from "./safe.ts"; -import type { Computation } from "./type.ts"; -export interface ParallelRet extends Computation[]> { +export interface ParallelRet extends Operation[]> { sequence: Channel, void>; immediate: Channel, void>; } @@ -61,8 +60,8 @@ export interface ParallelRet extends Computation[]> { * } * ``` */ -export function parallel( - operations: Callable[], +export function parallel( + operations: ((...args: TArgs) => Operation)[], ): Operation> { const sequence = createChannel>(); const immediate = createChannel>(); @@ -70,7 +69,7 @@ export function parallel( return resource>(function* (provide) { const task = yield* spawn(function* () { - const tasks = []; + const tasks = [] as Task>[]; for (const op of operations) { tasks.push( yield* spawn(function* () { @@ -95,7 +94,6 @@ export function parallel( yield* task; return results; } - yield* provide({ sequence, immediate, diff --git a/fx/race.test.ts b/fx/race.test.ts index ee1fc7d3..c4d96c49 100644 --- a/fx/race.test.ts +++ b/fx/race.test.ts @@ -10,12 +10,12 @@ describe("raceMap()", () => { function* () { let winner; const results = yield* raceMap({ - first: function* () { + *first() { yield* sleep(10); winner = "first"; return "first"; }, - second: function* () { + *second() { yield* sleep(20); winner = "second"; return "second"; diff --git a/fx/race.ts b/fx/race.ts index de23dd08..8c396de7 100644 --- a/fx/race.ts +++ b/fx/race.ts @@ -1,9 +1,9 @@ // deno-lint-ignore-file no-explicit-any -import type { Callable, Operation, Task } from "effection"; -import { action, call, resource, spawn } from "effection"; +import type { Operation, Task } from "effection"; +import { resource, spawn, withResolvers } from "effection"; -interface OpMap { - [key: string]: Callable; +interface OpMap { + [key: string]: (...args: TArgs) => Operation; } export function raceMap(opMap: OpMap): Operation< @@ -16,21 +16,29 @@ export function raceMap(opMap: OpMap): Operation< return resource(function* Race(provide) { const keys = Object.keys(opMap); const taskMap: { [key: string]: Task } = {}; - const resultMap: { [key: keyof OpMap]: OpMap[keyof OpMap] } = {}; - - const winner = yield* action>(function* (resolve) { - for (let i = 0; i < keys.length; i += 1) { - const key = keys[i]; - yield* spawn(function* () { - const task = yield* spawn(function* () { - yield* call(opMap[key] as any); + const resultMap: { [key: keyof OpMap]: ReturnType } = + {}; + + function* start() { + const resolvers = withResolvers(); + + yield* spawn(function* () { + for (let i = 0; i < keys.length; i += 1) { + const key = keys[i]; + yield* spawn(function* () { + const task = yield* spawn(opMap[key]); + taskMap[key] = task; + (resultMap[key] as any) = yield* task; + resolvers.resolve(task); }); - taskMap[key] = task; - (resultMap as any)[key] = yield* task; - resolve(task); - }); - } - }); + } + yield* resolvers.operation; + }); + + return yield* resolvers.operation; + } + + const winner = yield* start(); for (let i = 0; i < keys.length; i += 1) { const key = keys[i]; diff --git a/fx/request.test.ts b/fx/request.test.ts index bc33a001..27af4ee3 100644 --- a/fx/request.test.ts +++ b/fx/request.test.ts @@ -1,9 +1,9 @@ import { beforeEach, describe, it } from "@effectionx/bdd"; import { expect } from "@std/expect"; -import { json, request } from "./request.ts"; import { call, ensure, withResolvers } from "effection"; import { createServer } from "node:http"; +import { json, request } from "./request.ts"; // Ensure to run tests with --allow-net permission describe("request() and json()", () => { diff --git a/fx/safe.ts b/fx/safe.ts index 015b1a19..e6ab9023 100644 --- a/fx/safe.ts +++ b/fx/safe.ts @@ -1,4 +1,4 @@ -import type { Callable, Operation, Result } from "effection"; +import type { Operation, Result } from "effection"; import { call, Err, Ok } from "effection"; /** @@ -23,10 +23,11 @@ function isError(error: unknown): error is Error { return error instanceof Error; } -export function* safe(operator: Callable): Operation> { +export function* safe( + operator: (...args: TArgs) => Operation, +): Operation> { try { - // deno-lint-ignore no-explicit-any - const value = yield* call(operator as any); + const value = yield* call(operator); return Ok(value); } catch (error) { return Err(isError(error) ? error : new Error(String(error))); From 0096f667d9026a00f0d501b29834298abfdb22e8 Mon Sep 17 00:00:00 2001 From: Jacob Bolda Date: Mon, 21 Apr 2025 20:55:45 -0500 Subject: [PATCH 03/45] remove outer spawn, not required --- fx/race.ts | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/fx/race.ts b/fx/race.ts index 8c396de7..05620028 100644 --- a/fx/race.ts +++ b/fx/race.ts @@ -22,18 +22,15 @@ export function raceMap(opMap: OpMap): Operation< function* start() { const resolvers = withResolvers(); - yield* spawn(function* () { - for (let i = 0; i < keys.length; i += 1) { - const key = keys[i]; - yield* spawn(function* () { - const task = yield* spawn(opMap[key]); - taskMap[key] = task; - (resultMap[key] as any) = yield* task; - resolvers.resolve(task); - }); - } - yield* resolvers.operation; - }); + for (let i = 0; i < keys.length; i += 1) { + const key = keys[i]; + yield* spawn(function* () { + const task = yield* spawn(opMap[key]); + taskMap[key] = task; + (resultMap[key] as any) = yield* task; + resolvers.resolve(task); + }); + } return yield* resolvers.operation; } From 1f44ad1e5ac408a77b18c590108cb0f01266e953 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sat, 18 Oct 2025 04:51:50 -0400 Subject: [PATCH 04/45] Fix dependency in worker/deno.json --- worker/deno.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worker/deno.json b/worker/deno.json index 5d684b28..38bb17c4 100644 --- a/worker/deno.json +++ b/worker/deno.json @@ -11,6 +11,6 @@ "@std/path": "jsr:@std/path@^1", "@std/fs": "jsr:@std/fs@^1", "@effectionx/bdd": "jsr:@effectionx/bdd@0.2.2", - "@effection/signals": "npm:@effectionx/signals@0.3.0" + "@effectionx/signals": "npm:@effectionx/signals@0.3.0" } } From ee0d69b9132af162f0c3d35dd74aa957137deccc Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sat, 18 Oct 2025 14:30:54 -0400 Subject: [PATCH 05/45] Include all dependencies in import map --- tasks/check-version-mismatches.ts | 73 +++++++++++++++++++++++++++++++ tasks/update-effection-version.ts | 31 +++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 tasks/check-version-mismatches.ts create mode 100644 tasks/update-effection-version.ts diff --git a/tasks/check-version-mismatches.ts b/tasks/check-version-mismatches.ts new file mode 100644 index 00000000..8ca518af --- /dev/null +++ b/tasks/check-version-mismatches.ts @@ -0,0 +1,73 @@ +import { expandGlob } from "jsr:@std/fs@^1"; + +const rootDir = new URL("..", import.meta.url).pathname; + +interface DenoConfig { + name?: string; + version?: string; + imports?: Record; +} + +// First, collect all package versions +const packageVersions = new Map(); + +for await (const file of expandGlob("**/deno.json", { + root: rootDir, + exclude: ["node_modules", "build"], +})) { + const content = await Deno.readTextFile(file.path); + const config: DenoConfig = JSON.parse(content); + + if (config.name && config.version) { + packageVersions.set(config.name, config.version); + } +} + +console.log("Package versions found:"); +for (const [name, version] of packageVersions) { + console.log(` ${name}: ${version}`); +} +console.log(""); + +// Now check for mismatches +let foundMismatches = false; + +for await (const file of expandGlob("**/deno.json", { + root: rootDir, + exclude: ["node_modules", "build"], +})) { + const content = await Deno.readTextFile(file.path); + const config: DenoConfig = JSON.parse(content); + const relativePath = file.path.replace(rootDir, ""); + + if (config.imports) { + for (const [depName, depSpec] of Object.entries(config.imports)) { + if (depName.startsWith("@effectionx/")) { + const actualVersion = packageVersions.get(depName); + if (actualVersion) { + // Extract version from spec (handles jsr:@effectionx/foo@version or npm:@effectionx/foo@version) + const match = depSpec.match(/@effectionx\/[^@]+@(.+)$/); + if (match) { + const declaredVersion = match[1]; + // Check if it's a caret version or exact version + const isCaretVersion = declaredVersion.startsWith("^"); + const versionToCheck = isCaretVersion + ? declaredVersion.substring(1) + : declaredVersion; + + if (versionToCheck !== actualVersion) { + foundMismatches = true; + console.log( + `❌ ${relativePath}: ${depName} declared as ${declaredVersion} but actual version is ${actualVersion}`, + ); + } + } + } + } + } + } +} + +if (!foundMismatches) { + console.log("✓ All internal dependency versions match!"); +} diff --git a/tasks/update-effection-version.ts b/tasks/update-effection-version.ts new file mode 100644 index 00000000..e31f4909 --- /dev/null +++ b/tasks/update-effection-version.ts @@ -0,0 +1,31 @@ +import { expandGlob } from "jsr:@std/fs@^1"; +import { join } from "jsr:@std/path@^1"; + +const rootDir = new URL("..", import.meta.url).pathname; + +interface DenoConfig { + imports?: Record; +} + +for await (const file of expandGlob("**/deno.json", { + root: rootDir, + exclude: ["node_modules", "build"], +})) { + const content = await Deno.readTextFile(file.path); + const config: DenoConfig = JSON.parse(content); + + if (config.imports && config.imports["effection"]) { + const currentVersion = config.imports["effection"]; + if (currentVersion === "npm:effection@^3") { + config.imports["effection"] = "npm:effection@^4.0.0-0"; + + const updatedContent = JSON.stringify(config, null, 2) + "\n"; + await Deno.writeTextFile(file.path, updatedContent); + + const relativePath = file.path.replace(rootDir, ""); + console.log(`Updated ${relativePath}`); + } + } +} + +console.log("✓ Effection version update complete"); From 507f714a5d6439388c5f7b450cce8c53b3e261c4 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sat, 18 Oct 2025 19:45:43 -0400 Subject: [PATCH 06/45] Fixed eventemitter test by wrapping event in spawn --- process/test/eventemitter.test.ts | 50 ++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/process/test/eventemitter.test.ts b/process/test/eventemitter.test.ts index 985d525f..35ebdf19 100644 --- a/process/test/eventemitter.test.ts +++ b/process/test/eventemitter.test.ts @@ -1,5 +1,5 @@ import { expect } from "@std/expect"; -import { spawn } from "effection"; +import { spawn, withResolvers } from "effection"; import { describe, it } from "@effectionx/bdd"; import { EventEmitter } from "node:events"; @@ -9,58 +9,74 @@ describe("once", () => { it("resolves with single argument as array", function* () { expect.assertions(1); const emitter = new EventEmitter(); + const { resolve, operation } = withResolvers<[string]>(); - let result; yield* spawn(function* () { - result = yield* once<[string]>(emitter, "test"); + resolve(yield* once<[string]>(emitter, "test")); }); - emitter.emit("test", "hello"); + yield* spawn(function* () { + emitter.emit("test", "hello"); + }); - expect(result).toEqual(["hello"]); + expect(yield* operation).toEqual(["hello"]); }); it("resolves with multiple arguments as array", function* () { expect.assertions(1); const emitter = new EventEmitter(); - let result; + let { resolve, operation } = withResolvers<[number, string]>(); yield* spawn(function* () { - result = yield* once<[number, string]>(emitter, "exit"); + resolve(yield* once<[number, string]>(emitter, "exit")); }); - emitter.emit("exit", 42, "SIGTERM"); + yield* spawn(function* () { + emitter.emit("exit", 42, "SIGTERM"); + }); - expect(result).toEqual([42, "SIGTERM"]); + expect(yield* operation).toEqual([42, "SIGTERM"]); }); it("only resolves once even with multiple emissions", function* () { const emitter = new EventEmitter(); - let first; + const { resolve, operation } = withResolvers(); + let results: string[][] = []; + + yield* spawn(function* () { + results.push(yield* once<[string]>(emitter, "data")); + resolve(); + }); + yield* spawn(function* () { - first = yield* once<[string]>(emitter, "data"); + emitter.emit("data", "first"); + emitter.emit("data", "second"); }); - emitter.emit("data", "first"); - emitter.emit("data", "second"); + yield* operation; - expect(first).toEqual(["first"]); + expect(results).toEqual([["first"]]); }); it("removes listener after resolving", function* () { expect.assertions(2); const emitter = new EventEmitter(); + const { resolve, operation } = withResolvers(); + yield* spawn(function* () { yield* once<[string]>(emitter, "test"); + resolve(); }); - expect(emitter.listenerCount("test")).toBe(1); - - emitter.emit("test", "hello"); + yield* spawn(function* () { + expect(emitter.listenerCount("test")).toBe(1); + emitter.emit("test", "hello"); + }); + yield* operation; expect(emitter.listenerCount("test")).toBe(0); }); }); From 9eb8cba3908bfec9b226469056bb855608e1a664 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sat, 18 Oct 2025 19:46:25 -0400 Subject: [PATCH 07/45] Give the operation tree 1 tick to setup a subscription before sending events in for-each tests --- stream-helpers/for-each.test.ts | 37 +++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/stream-helpers/for-each.test.ts b/stream-helpers/for-each.test.ts index da3e8ad7..28a08709 100644 --- a/stream-helpers/for-each.test.ts +++ b/stream-helpers/for-each.test.ts @@ -1,4 +1,10 @@ -import { createSignal, spawn } from "effection"; +import { + createChannel, + createSignal, + sleep, + spawn, + withResolvers, +} from "effection"; import { describe, it } from "@effectionx/bdd"; import { expect } from "@std/expect"; @@ -6,24 +12,34 @@ import { forEach } from "./for-each.ts"; describe("forEach", () => { it("should invoke function for each item in the stream", function* () { - const stream = createSignal(); + expect.assertions(1); + + const stream = createChannel(); const processedItems: number[] = []; + const { resolve, operation } = withResolvers(); + yield* spawn(() => forEach(function* (item: number) { processedItems.push(item); }, stream) ); - stream.send(1); - stream.send(2); - stream.send(3); + yield* spawn(function* () { + yield* sleep(1); + yield* stream.send(1); + yield* stream.send(2); + yield* stream.send(3); + + resolve(); + }); + yield* operation; expect(processedItems).toEqual([1, 2, 3]); }); it("should return the close value of the stream", function* () { - const stream = createSignal(); + const stream = createChannel(); const result = yield* spawn(() => forEach(function* () { @@ -31,9 +47,12 @@ describe("forEach", () => { }, stream) ); - stream.send("hello"); - stream.send("world"); - stream.close(42); // Close with value 42 + yield* spawn(function*() { + yield* sleep(1); + yield* stream.send("hello"); + yield* stream.send("world"); + yield* stream.close(42); // Close with value 42 + }); const closeValue = yield* result; expect(closeValue).toBe(42); From 5c8ea6404d6ea54a49f151c4e52f47bfd1dbe2b2 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sat, 18 Oct 2025 19:47:21 -0400 Subject: [PATCH 08/45] Run timers asyncronously in task-buffer tests --- task-buffer/task-buffer.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/task-buffer/task-buffer.test.ts b/task-buffer/task-buffer.test.ts index df8e480a..6e582bb0 100644 --- a/task-buffer/task-buffer.test.ts +++ b/task-buffer/task-buffer.test.ts @@ -1,5 +1,5 @@ import { FakeTime } from "@std/testing/time"; -import { sleep, spawn, type Task } from "effection"; +import { sleep, spawn, until, type Task } from "effection"; import { describe, it } from "@effectionx/bdd"; import { expect } from "@std/expect"; import { useTaskBuffer } from "./task-buffer.ts"; @@ -19,12 +19,12 @@ describe("TaskBuffer", () => { third = yield* yield* buffer.spawn(() => sleep(10)); }); - time.tick(5); + yield* until(time.tickAsync(5)); // right now the third spawn is queued up, but not spawned. expect(third).toBeUndefined(); - time.tick(10); + yield* until(time.tickAsync(10)); // the other tasks finished and so the third task is active. expect(third).toBeDefined(); From 6d62852ff998e64aa726aed8b94b612802066a55 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sat, 18 Oct 2025 19:48:20 -0400 Subject: [PATCH 09/45] Wrap arrary operations in a spawn in array tests --- signals/array.test.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/signals/array.test.ts b/signals/array.test.ts index 06b580f5..8a9f063b 100644 --- a/signals/array.test.ts +++ b/signals/array.test.ts @@ -1,8 +1,9 @@ import { describe, it } from "@effectionx/bdd"; import { expect } from "@std/expect"; -import { each, sleep, spawn } from "effection"; +import { each, sleep, spawn, withResolvers } from "effection"; import { createArraySignal } from "./array.ts"; +import { is } from "./helpers.ts"; describe("array signal", () => { it("accepts an initial value", function* () { @@ -23,6 +24,8 @@ describe("array signal", () => { it("does not send a value to the stream when the set value is the same as the current value", function* () { const array = yield* createArraySignal([]); + const { resolve, operation } = withResolvers(); + const updates: number[][] = []; yield* spawn(function* () { @@ -32,13 +35,18 @@ describe("array signal", () => { } }); - array.set([1, 2, 3]); + yield* spawn(function* () { + array.set([1, 2, 3]); + + expect(updates).toEqual([[1, 2, 3]]); - expect(updates).toEqual([[1, 2, 3]]); + array.set([1, 2, 3]); - array.set([1, 2, 3]); + expect(updates).toEqual([[1, 2, 3]]); + resolve(); + }); - expect(updates).toEqual([[1, 2, 3]]); + yield* operation; }); }); From e6b6d3c37c09dd3f4020abf98817e043ffe0d3e4 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sat, 18 Oct 2025 19:52:47 -0400 Subject: [PATCH 10/45] Wrapped boolean operations in spawn in boolean tests --- signals/boolean.test.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/signals/boolean.test.ts b/signals/boolean.test.ts index 0008d494..0889776b 100644 --- a/signals/boolean.test.ts +++ b/signals/boolean.test.ts @@ -1,4 +1,4 @@ -import { each, spawn } from "effection"; +import { each, spawn, withResolvers } from "effection"; import { describe, it } from "@effectionx/bdd"; import { expect } from "@std/expect"; import { createBooleanSignal } from "./boolean.ts"; @@ -20,6 +20,8 @@ describe("boolean", () => { it("does not send a value to the stream when the set value is the same as the current value", function* () { const boolean = yield* createBooleanSignal(true); + const { resolve, operation } = withResolvers(); + const updates: boolean[] = []; yield* spawn(function* () { @@ -29,13 +31,18 @@ describe("boolean", () => { } }); - boolean.set(true); + yield* spawn(function*() { + boolean.set(true); - expect(updates).toEqual([]); + expect(updates).toEqual([]); - boolean.set(false); + boolean.set(false); + + expect(updates).toEqual([false]); + resolve(); + }); - expect(updates).toEqual([false]); + yield* operation; }); }); describe("update", () => { From 6264ccf2a742f8255d4d83343c452b29f1fb012e Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sat, 18 Oct 2025 20:12:19 -0400 Subject: [PATCH 11/45] Fixed array test by giving the operation tree a tick in array tests --- signals/array.test.ts | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/signals/array.test.ts b/signals/array.test.ts index 8a9f063b..a4db7e2b 100644 --- a/signals/array.test.ts +++ b/signals/array.test.ts @@ -1,6 +1,14 @@ import { describe, it } from "@effectionx/bdd"; import { expect } from "@std/expect"; -import { each, sleep, spawn, withResolvers } from "effection"; +import { + all, + createChannel, + each, + race, + sleep, + spawn, + withResolvers, +} from "effection"; import { createArraySignal } from "./array.ts"; import { is } from "./helpers.ts"; @@ -22,27 +30,42 @@ describe("array signal", () => { }); it("does not send a value to the stream when the set value is the same as the current value", function* () { + expect.assertions(2); + const array = yield* createArraySignal([]); const { resolve, operation } = withResolvers(); - const updates: number[][] = []; + const updates = createChannel(); + const subscription = yield* updates; yield* spawn(function* () { for (const update of yield* each(array)) { - updates.push(update); + yield* updates.send(update); yield* each.next(); } }); yield* spawn(function* () { + yield* sleep(1); + array.set([1, 2, 3]); - expect(updates).toEqual([[1, 2, 3]]); + const firstUpdate = yield* subscription.next(); + + expect(firstUpdate.value).toEqual([1, 2, 3]); array.set([1, 2, 3]); - expect(updates).toEqual([[1, 2, 3]]); + const result = yield* race([ + subscription.next(), + (function* () { + yield* sleep(1); + return "sleep won; update not received"; + })(), + ]); + + expect(result).toEqual("sleep won; update not received"); resolve(); }); From 4c649912bf3823c8a2ab7333af566364521eeda8 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sat, 18 Oct 2025 20:28:50 -0400 Subject: [PATCH 12/45] Check for boolean.test.ts updates differently --- signals/boolean.test.ts | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/signals/boolean.test.ts b/signals/boolean.test.ts index 0889776b..d7d9de09 100644 --- a/signals/boolean.test.ts +++ b/signals/boolean.test.ts @@ -1,7 +1,8 @@ -import { each, spawn, withResolvers } from "effection"; +import { createChannel, each, race, sleep, spawn, withResolvers } from "effection"; import { describe, it } from "@effectionx/bdd"; import { expect } from "@std/expect"; import { createBooleanSignal } from "./boolean.ts"; +import { update } from "immutable"; describe("boolean", () => { it("takes an initial value", function* () { @@ -18,15 +19,17 @@ describe("boolean", () => { expect(boolean.valueOf()).toEqual(false); }); it("does not send a value to the stream when the set value is the same as the current value", function* () { + expect.assertions(2); const boolean = yield* createBooleanSignal(true); const { resolve, operation } = withResolvers(); - const updates: boolean[] = []; + const updates = createChannel(); + const subscription = yield* updates; yield* spawn(function* () { for (const update of yield* each(boolean)) { - updates.push(update); + yield* updates.send(update); yield* each.next(); } }); @@ -34,11 +37,21 @@ describe("boolean", () => { yield* spawn(function*() { boolean.set(true); - expect(updates).toEqual([]); + let next = yield* race([ + subscription.next(), + function*() { + yield* sleep(1); + return `sleep won; update not received`; + }() + ]); + + expect(next).toEqual(`sleep won; update not received`); boolean.set(false); - expect(updates).toEqual([false]); + next = yield* subscription.next(); + + expect(next.value).toEqual(false); resolve(); }); From f6a8194acbb277d3c876477c6f2d440226f329c6 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sat, 18 Oct 2025 20:32:28 -0400 Subject: [PATCH 13/45] Fixed is helper test by giving it 1 tick to setup the operation tree --- signals/helpers.test.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/signals/helpers.test.ts b/signals/helpers.test.ts index 8fb7076d..6e9ce8df 100644 --- a/signals/helpers.test.ts +++ b/signals/helpers.test.ts @@ -1,6 +1,6 @@ import { describe, it } from "@effectionx/bdd"; import { expect } from "@std/expect"; -import { spawn } from "effection"; +import { sleep, spawn, withResolvers } from "effection"; import { createBooleanSignal } from "./boolean.ts"; import { is } from "./helpers.ts"; @@ -9,13 +9,21 @@ describe("is", () => { it("waits until the value of the stream matches the predicate", function* () { const open = yield* createBooleanSignal(false); const update: string[] = []; + + const { resolve, operation } = withResolvers(); yield* spawn(function* () { yield* is(open, (open) => open === true); update.push("floodgates are open!"); + resolve(); }); - open.set(true); + yield* spawn(function* () { + yield* sleep(1); + open.set(true); + }); + + yield* operation; expect(update).toEqual(["floodgates are open!"]); }); From 355a3e75f6c86a349529e0da6f474f74c786c953 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sat, 18 Oct 2025 21:36:03 -0400 Subject: [PATCH 14/45] Fixed faucet tests by waiting 1 tick for operation tree and converging on results length --- stream-helpers/test-helpers/faucet.test.ts | 15 +++++++++++---- stream-helpers/test-helpers/faucet.ts | 8 ++++---- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/stream-helpers/test-helpers/faucet.test.ts b/stream-helpers/test-helpers/faucet.test.ts index 879ca54d..ff9b078a 100644 --- a/stream-helpers/test-helpers/faucet.test.ts +++ b/stream-helpers/test-helpers/faucet.test.ts @@ -18,8 +18,11 @@ describe("useFaucet", () => { } }); - // Pour an array of items - yield* faucet.pour([1, 2, 3]); + yield* spawn(function* () { + yield* sleep(1); + // Pour an array of items + yield* faucet.pour([1, 2, 3]); + }); // Wait for processing yield* is(results, (results) => results.length === 3); @@ -28,6 +31,7 @@ describe("useFaucet", () => { }); it("respects the open state", function* () { + expect.assertions(2); const faucet = yield* useFaucet({ open: false }); const results = yield* createArraySignal([]); @@ -48,6 +52,8 @@ describe("useFaucet", () => { yield* faucet.pour([4, 5, 6]); + yield* is(results, (results) => results.length === 3); + expect(results.valueOf()).toEqual([4, 5, 6]); }); @@ -65,6 +71,7 @@ describe("useFaucet", () => { // Pour using a generator function yield* spawn(function* () { + yield* sleep(1); yield* faucet.pour(function* (send) { yield* send(1); yield* sleep(10); @@ -93,6 +100,7 @@ describe("useFaucet", () => { // Start pouring with a generator yield* spawn(function* () { + yield* sleep(1); yield* faucet.pour(function* (send) { yield* send(1); yield* sleep(10); @@ -105,8 +113,7 @@ describe("useFaucet", () => { }); }); - // Wait for the first item - yield* sleep(50); + yield* is(results, results => results.length === 2); expect(results.valueOf()).toEqual([1, 2]); }); diff --git a/stream-helpers/test-helpers/faucet.ts b/stream-helpers/test-helpers/faucet.ts index 808df8ef..2454f824 100644 --- a/stream-helpers/test-helpers/faucet.ts +++ b/stream-helpers/test-helpers/faucet.ts @@ -1,4 +1,4 @@ -import { createSignal, type Operation, type Stream } from "effection"; +import { createChannel, type Operation, type Stream } from "effection"; import { createBooleanSignal, is } from "@effectionx/signals"; /** @@ -81,7 +81,7 @@ export interface FaucetOptions { * @returns stream of items coming from the faucet */ export function* useFaucet(options: FaucetOptions): Operation> { - let signal = createSignal(); + let signal = createChannel(); let open = yield* createBooleanSignal(options.open); return { @@ -90,12 +90,12 @@ export function* useFaucet(options: FaucetOptions): Operation> { if (Array.isArray(items)) { for (let i of items) { yield* is(open, (open) => open); - signal.send(i); + yield* signal.send(i); } } else { yield* items(function* (item) { yield* is(open, (open) => open); - signal.send(item); + yield* signal.send(item); }); } }, From 24e4ee5e501e1af268511c658329260d12b940df Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sun, 19 Oct 2025 10:55:02 -0400 Subject: [PATCH 15/45] Update generate-importmap to use actual package exports - Read exports field from each package's deno.json - Support both string and object export formats - Map workspace packages to their actual export paths - Automatically add @std/testing sub-packages (/bdd, /mock, /time) - Force all packages to use effection v4 --- deno.json | 3 ++- tasks/generate-importmap.ts | 54 ++++++++++++++++++++++++++++++++----- 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/deno.json b/deno.json index 51effdd5..520a02a7 100644 --- a/deno.json +++ b/deno.json @@ -3,7 +3,8 @@ "effection": "npm:effection@^3", "@deno/dnt": "jsr:@deno/dnt@0.42.3", "@std/path": "jsr:@std/path@^1", - "zod": "npm:zod@3.23.8" + "zod": "npm:zod@3.23.8", + "@std/fs": "jsr:@std/fs@^1" }, "compilerOptions": { "lib": [ diff --git a/tasks/generate-importmap.ts b/tasks/generate-importmap.ts index 0564ded5..383bf298 100644 --- a/tasks/generate-importmap.ts +++ b/tasks/generate-importmap.ts @@ -1,15 +1,43 @@ #!/usr/bin/env -S deno run --allow-read --allow-write -import { expandGlob } from "jsr:@std/fs@^1"; +import { expandGlob } from "@std/fs"; interface DenoConfig { + name?: string; + exports?: string | Record; imports?: Record; } const rootDir = new URL("..", import.meta.url).pathname; const imports = new Map(); +const workspacePackages = new Map(); -// Walk through all deno.json files +// First pass: collect all workspace package names and their exports +for await (const file of expandGlob("**/deno.json", { + root: rootDir, + exclude: ["node_modules", "build"], +})) { + const content = await Deno.readTextFile(file.path); + const config: DenoConfig = JSON.parse(content); + + if (config.name?.startsWith("@effectionx/")) { + // Get the default export path + let exportPath = "./mod.ts"; // fallback + if (typeof config.exports === "string") { + exportPath = config.exports; + } else if (config.exports && typeof config.exports === "object") { + exportPath = config.exports["."] || config.exports["default"] || "./mod.ts"; + } + + // Get package directory relative to root + const packageDir = file.path.replace(rootDir, "").replace("/deno.json", ""); + const fullExportPath = `./${packageDir}/${exportPath.replace("./", "")}`; + + workspacePackages.set(config.name, fullExportPath); + } +} + +// Second pass: collect external dependencies and convert workspace packages to local paths for await (const file of expandGlob("**/deno.json", { root: rootDir, exclude: ["node_modules", "build"], @@ -19,8 +47,14 @@ for await (const file of expandGlob("**/deno.json", { if (config.imports) { for (const [key, value] of Object.entries(config.imports)) { - // Skip internal @effectionx packages unless they use npm: - if (key.startsWith("@effectionx/") && !value.startsWith("npm:")) { + // Convert workspace packages to local paths using their actual export + if (workspacePackages.has(key)) { + imports.set(key, workspacePackages.get(key)!); + continue; + } + + // Skip other internal @effectionx packages + if (key.startsWith("@effectionx/")) { continue; } @@ -35,14 +69,22 @@ for await (const file of expandGlob("**/deno.json", { // Add effection v4 imports.set("effection", "npm:effection@^4.0.0-0"); +// Add @std/testing sub-packages if @std/testing is present +if (imports.has("@std/testing")) { + const testingSpec = imports.get("@std/testing")!; + // Extract version from "jsr:@std/testing@^1" -> "^1" + const version = testingSpec.split("@std/testing@")[1]; + imports.set("@std/testing/bdd", `jsr:@std/testing@${version}/bdd`); + imports.set("@std/testing/mock", `jsr:@std/testing@${version}/mock`); + imports.set("@std/testing/time", `jsr:@std/testing@${version}/time`); +} + // Sort by key const sortedImports = Object.fromEntries( Array.from(imports.entries()).sort(([a], [b]) => a.localeCompare(b)) ); const output = { - "// To regenerate this file": "run: deno task generate-importmap", - "// Description": "Collects all unique external dependencies from workspace packages' deno.json files", imports: sortedImports, }; From 32b6312d5ec27065b31cf06197213939d40f3a7c Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sun, 19 Oct 2025 13:21:39 -0400 Subject: [PATCH 16/45] Fix stream-helpers for effection v4 and remove debug logging - Rewrite batch implementation to use iterator-based approach with timebox - Remove all console.log debug statements from batch - Add sleep(1) calls in tests for operation tree setup timing - Update batch tests to use signal convergence patterns - Refactor tracker tests to use createArraySignal and is() - Remove unused imports from test files - Add @effectionx/timebox dependency to stream-helpers --- signals/array.test.ts | 2 - signals/boolean.test.ts | 1 - stream-helpers/batch.test.ts | 9 ++- stream-helpers/batch.ts | 107 ++++++++++++++++++++++---------- stream-helpers/deno.json | 1 + stream-helpers/for-each.test.ts | 1 - stream-helpers/map.ts | 4 +- stream-helpers/tracker.test.ts | 37 +++++------ stream-helpers/valve.test.ts | 2 + task-buffer/task-buffer.test.ts | 2 +- 10 files changed, 103 insertions(+), 63 deletions(-) diff --git a/signals/array.test.ts b/signals/array.test.ts index a4db7e2b..e9a42a1b 100644 --- a/signals/array.test.ts +++ b/signals/array.test.ts @@ -1,7 +1,6 @@ import { describe, it } from "@effectionx/bdd"; import { expect } from "@std/expect"; import { - all, createChannel, each, race, @@ -11,7 +10,6 @@ import { } from "effection"; import { createArraySignal } from "./array.ts"; -import { is } from "./helpers.ts"; describe("array signal", () => { it("accepts an initial value", function* () { diff --git a/signals/boolean.test.ts b/signals/boolean.test.ts index d7d9de09..9aeae23f 100644 --- a/signals/boolean.test.ts +++ b/signals/boolean.test.ts @@ -2,7 +2,6 @@ import { createChannel, each, race, sleep, spawn, withResolvers } from "effectio import { describe, it } from "@effectionx/bdd"; import { expect } from "@std/expect"; import { createBooleanSignal } from "./boolean.ts"; -import { update } from "immutable"; describe("boolean", () => { it("takes an initial value", function* () { diff --git a/stream-helpers/batch.test.ts b/stream-helpers/batch.test.ts index fbf35949..bdc287e1 100644 --- a/stream-helpers/batch.test.ts +++ b/stream-helpers/batch.test.ts @@ -57,6 +57,8 @@ describe("batch", () => { }, stream) ); + yield* sleep(1); + yield* faucet.pour(function* (send) { for (let i = 1; i <= 10; i++) { yield* send(i); @@ -82,10 +84,13 @@ describe("batch", () => { }, stream) ); + yield* sleep(1); + yield* faucet.pour([1, 2, 3, 4, 5, 6]); - yield* is(batches, (list) => list.length === 2); + yield* is(batches, batches => batches.flat().length >= 6); - expect(batches.valueOf()).toEqual([[1, 2, 3, 4, 5], [6]]); + expect(batches.length).toBeGreaterThan(1); + expect(batches.valueOf().every(batch => batch.length <= 5)).toBe(true); }); }); diff --git a/stream-helpers/batch.ts b/stream-helpers/batch.ts index 44ee8d6d..2acb800e 100644 --- a/stream-helpers/batch.ts +++ b/stream-helpers/batch.ts @@ -1,9 +1,19 @@ -import { each, race, sleep, spawn, type Stream } from "effection"; -import { createArraySignal, is } from "@effectionx/signals"; +import { + spawn, + type Task, + type Stream, + Operation, + WithResolvers, + withResolvers, + lift, +} from "effection"; +import { timebox } from "@effectionx/timebox"; -type RequireAtLeastOne = - & Pick> - & { +type RequireAtLeastOne = Pick< + T, + Exclude +> & + { [K in Keys]-?: Required> & Partial>>; }[Keys]; @@ -28,41 +38,72 @@ export function batch( return function (stream: Stream): Stream, never> { return { *[Symbol.iterator]() { - let batch = yield* createArraySignal([]); + const subscription = yield* stream; + let lastPull: Task> | undefined; - yield* spawn(function* () { - for (let item of yield* each(stream)) { - batch.push(item); - if (options.maxSize && batch.length >= options.maxSize) { - // wait until it's drained - yield* is(batch, (batch) => batch.length === 0); + return { + *next() { + let start: DOMHighResTimeStamp = performance.now(); + const batch: T[] = []; + if (lastPull) { + batch.push((yield* lastPull).value); + lastPull = undefined; } - yield* each.next(); - } - }); + // iterate incoming stream + let next = yield* subscription.next(); + // push the next value into the batch + while (!next.done) { + batch.push(next.value); + const now = performance.now(); + if (options.maxSize && batch.length >= options.maxSize) { + return { + done: false as const, + value: batch, + }; + } else if (options.maxTime && start + options.maxTime <= now) { + return { + done: false as const, + value: batch, + }; + } else if (options.maxTime) { + lastPull = yield* spawn(function* () { + const next = yield* subscription.next(); + return next; + }); - function drain() { - let value = batch.valueOf(); - batch.set([]); - return value; - } + const timeout = yield* timebox( + start + options.maxTime - performance.now(), + () => + lastPull!, + ); - return { - *next() { - yield* is(batch, (batch) => batch.length >= 1); + if (timeout.timeout) { + // produce the batch that we have + return { + done: false as const, + value: batch, + }; + } else { + lastPull = undefined; + next = timeout.value; + } + } else { + next = yield* subscription.next(); + } + } - if (options.maxTime && options.maxSize) { - yield* race([ - is(batch, (batch) => batch.length === options.maxSize), - sleep(options.maxTime), - ]); - } else if (options.maxTime) { - yield* sleep(options.maxTime); - } else if (options.maxSize) { - yield* is(batch, (batch) => batch.length === options.maxSize); + // Stream is done, return any remaining batch + if (batch.length > 0) { + return { + done: false as const, + value: batch, + }; } - return { done: false, value: drain() }; + return { + done: true as const, + value: undefined as never, + }; }, }; }, diff --git a/stream-helpers/deno.json b/stream-helpers/deno.json index 2c050c95..937a6297 100644 --- a/stream-helpers/deno.json +++ b/stream-helpers/deno.json @@ -7,6 +7,7 @@ "@effectionx/signals": "npm:@effectionx/signals@0.3.0", "@std/expect": "jsr:@std/expect@^1", "@std/testing": "jsr:@std/testing@^1", + "@effectionx/timebox": "npm:@effectionx/timebox@0.1.0", "@effectionx/bdd": "jsr:@effectionx/bdd@0.2.2", "remeda": "npm:remeda@^2" }, diff --git a/stream-helpers/for-each.test.ts b/stream-helpers/for-each.test.ts index 28a08709..ed17a922 100644 --- a/stream-helpers/for-each.test.ts +++ b/stream-helpers/for-each.test.ts @@ -1,6 +1,5 @@ import { createChannel, - createSignal, sleep, spawn, withResolvers, diff --git a/stream-helpers/map.ts b/stream-helpers/map.ts index 92ffee9b..b25cdb7d 100644 --- a/stream-helpers/map.ts +++ b/stream-helpers/map.ts @@ -21,9 +21,11 @@ export function map( return next; } + const value = yield* fn(next.value); + return { done: false, - value: yield* fn(next.value), + value, }; }, }; diff --git a/stream-helpers/tracker.test.ts b/stream-helpers/tracker.test.ts index 105f5755..3c8f49e9 100644 --- a/stream-helpers/tracker.test.ts +++ b/stream-helpers/tracker.test.ts @@ -1,18 +1,19 @@ import { describe, it } from "@effectionx/bdd"; import { expect } from "@std/expect"; -import { each, sleep, spawn, withResolvers } from "effection"; +import { each, sleep, spawn } from "effection"; import { pipe } from "remeda"; import { batch } from "./batch.ts"; import { map } from "./map.ts"; import { useFaucet } from "./test-helpers/faucet.ts"; import { createTracker } from "./tracker.ts"; +import { createArraySignal, is } from "@effectionx/signals"; describe("tracker", () => { it("waits for all items to be processed", function* () { - const { operation, resolve } = withResolvers(); const tracker = yield* createTracker(); const faucet = yield* useFaucet({ open: true }); + const received = yield* createArraySignal([]); const stream = pipe( faucet, @@ -23,22 +24,17 @@ describe("tracker", () => { }), ); - const received: number[] = []; - yield* spawn(function* () { - let count = 0; for (const item of yield* each(stream)) { yield* sleep(10); received.push(item); tracker.markOne(item); - count++; - if (count >= 3) { - resolve(); - } yield* each.next(); } }); + yield* sleep(1); + yield* faucet.pour(function* (send) { yield* send(1); yield* sleep(1); @@ -47,16 +43,18 @@ describe("tracker", () => { yield* send(3); }); - yield* operation; yield* tracker; - expect(received).toEqual([1, 2, 3]); + yield* is(received, received => received.length === 3); + + expect(received.valueOf()).toEqual([1, 2, 3]); }); - it("tracks batched items", function* () { - const { operation, resolve } = withResolvers(); + it("tracks batched items", function* () { const tracker = yield* createTracker(); const faucet = yield* useFaucet({ open: true }); + const received = yield* createArraySignal([]); + const stream = pipe( faucet, tracker.passthrough(), @@ -69,21 +67,16 @@ describe("tracker", () => { }), ); - const received: Readonly[] = []; - yield* spawn(function* () { - let count = 0; for (const items of yield* each(stream)) { received.push(items); tracker.markMany(items); - count = count + items.length; - if (count >= 9) { - resolve(); - } yield* each.next(); } }); + yield* sleep(1); + yield* faucet.pour(function* (send) { yield* send(1); yield* sleep(10); @@ -104,9 +97,9 @@ describe("tracker", () => { yield* send(9); }); - yield* operation; + yield* is(received, received => received.flat().length >= 9); yield* tracker; - expect(received).toEqual([[1, 2, 3], [4, 5, 6], [7, 8, 9]]); + expect(received.valueOf()).toEqual([[1, 2, 3], [4, 5, 6], [7, 8, 9]]); }); }); diff --git a/stream-helpers/valve.test.ts b/stream-helpers/valve.test.ts index c6bb1e73..4ee60bf4 100644 --- a/stream-helpers/valve.test.ts +++ b/stream-helpers/valve.test.ts @@ -36,6 +36,8 @@ describe("valve", () => { } }); + yield* sleep(1); + yield* faucet.pour([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); yield* is(values, (values) => values.length === 10); diff --git a/task-buffer/task-buffer.test.ts b/task-buffer/task-buffer.test.ts index 6e582bb0..b69b198d 100644 --- a/task-buffer/task-buffer.test.ts +++ b/task-buffer/task-buffer.test.ts @@ -48,7 +48,7 @@ describe("TaskBuffer", () => { expect(finished).toEqual(0); - time.tick(10); + yield* until(time.tickAsync(10)); yield* buffer; expect(finished).toEqual(3); From 783d30bdc6dbbf68e55de9414a989f47f4a529f8 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sun, 19 Oct 2025 14:05:19 -0400 Subject: [PATCH 17/45] Fix batch stream completion when lastPull times out - Initialize next with done=true to handle stream completion properly - When lastPull times out, halt it and clear the reference before checking next - When lastPull resolves, use its result as next instead of pushing to batch - Move subscription.next() into else block to avoid duplicate calls - Return next directly at end to propagate done state correctly - When lastPull exists at the start of next(), wrap it in a timebox to respect maxTime. If the timeout occurs, halt the task instead of waiting indefinitely. This fixes the hanging test where forEach would wait indefinitely for more items after the source stream was exhausted. --- deno.json | 5 +- signals/boolean.test.ts | 15 ++++-- signals/helpers.test.ts | 2 +- stream-helpers/batch.test.ts | 31 ++++++++++-- stream-helpers/batch.ts | 58 ++++++++++------------ stream-helpers/for-each.test.ts | 9 +--- stream-helpers/test-helpers/faucet.test.ts | 2 +- stream-helpers/tracker.test.ts | 4 +- task-buffer/task-buffer.test.ts | 2 +- tasks/check-version-mismatches.ts | 20 +++++--- tasks/generate-importmap.ts | 27 ++++++---- tasks/update-effection-version.ts | 10 ++-- 12 files changed, 110 insertions(+), 75 deletions(-) diff --git a/deno.json b/deno.json index 520a02a7..ce57680e 100644 --- a/deno.json +++ b/deno.json @@ -21,7 +21,10 @@ "prefer-const", "require-yield" ] - } + }, + "exclude": [ + "tasks" + ] }, "workspace": [ "./context-api", diff --git a/signals/boolean.test.ts b/signals/boolean.test.ts index 9aeae23f..d7f5dc83 100644 --- a/signals/boolean.test.ts +++ b/signals/boolean.test.ts @@ -1,4 +1,11 @@ -import { createChannel, each, race, sleep, spawn, withResolvers } from "effection"; +import { + createChannel, + each, + race, + sleep, + spawn, + withResolvers, +} from "effection"; import { describe, it } from "@effectionx/bdd"; import { expect } from "@std/expect"; import { createBooleanSignal } from "./boolean.ts"; @@ -33,15 +40,15 @@ describe("boolean", () => { } }); - yield* spawn(function*() { + yield* spawn(function* () { boolean.set(true); let next = yield* race([ subscription.next(), - function*() { + function* () { yield* sleep(1); return `sleep won; update not received`; - }() + }(), ]); expect(next).toEqual(`sleep won; update not received`); diff --git a/signals/helpers.test.ts b/signals/helpers.test.ts index 6e9ce8df..098bac68 100644 --- a/signals/helpers.test.ts +++ b/signals/helpers.test.ts @@ -9,7 +9,7 @@ describe("is", () => { it("waits until the value of the stream matches the predicate", function* () { const open = yield* createBooleanSignal(false); const update: string[] = []; - + const { resolve, operation } = withResolvers(); yield* spawn(function* () { diff --git a/stream-helpers/batch.test.ts b/stream-helpers/batch.test.ts index bdc287e1..f22dcbf1 100644 --- a/stream-helpers/batch.test.ts +++ b/stream-helpers/batch.test.ts @@ -50,9 +50,16 @@ describe("batch", () => { const stream = batch({ maxSize: 8, maxTime: 50 })(faucet); const batches = yield* createArraySignal([]); + const windows: number[] = []; + + let last = performance.now(); yield* spawn(() => forEach(function* (batch) { + const now = performance.now(); + windows.push(now - last); + last = now; + batches.push(batch); }, stream) ); @@ -68,7 +75,12 @@ describe("batch", () => { yield* is(batches, (list) => list.flat().length >= 10); - expect(batches.valueOf()).toHaveLength(4); + expect(windows.length).toBeGreaterThanOrEqual(3); + + const avg = average(windows); + const percentDiff = Math.abs((avg - 50) / 50) * 100; + expect(percentDiff).toBeLessThanOrEqual(20); + expect(batches.valueOf().flat()).toHaveLength(10); }); @@ -85,12 +97,23 @@ describe("batch", () => { ); yield* sleep(1); - + yield* faucet.pour([1, 2, 3, 4, 5, 6]); - yield* is(batches, batches => batches.flat().length >= 6); + yield* is(batches, (batches) => batches.flat().length >= 6); expect(batches.length).toBeGreaterThan(1); - expect(batches.valueOf().every(batch => batch.length <= 5)).toBe(true); + expect(batches.valueOf().every((batch) => batch.length <= 5)).toBe(true); }); }); + +function average(arr: number[]) { + if (arr.length === 0) { + return 0; + } + const sum = arr.reduce( + (accumulator, currentValue) => accumulator + currentValue, + 0, + ); + return sum / arr.length; +} diff --git a/stream-helpers/batch.ts b/stream-helpers/batch.ts index 2acb800e..84f1aebd 100644 --- a/stream-helpers/batch.ts +++ b/stream-helpers/batch.ts @@ -1,19 +1,12 @@ -import { - spawn, - type Task, - type Stream, - Operation, - WithResolvers, - withResolvers, - lift, -} from "effection"; +import { spawn, type Stream, type Task } from "effection"; import { timebox } from "@effectionx/timebox"; -type RequireAtLeastOne = Pick< - T, - Exclude -> & - { +type RequireAtLeastOne = + & Pick< + T, + Exclude + > + & { [K in Keys]-?: Required> & Partial>>; }[Keys]; @@ -45,12 +38,22 @@ export function batch( *next() { let start: DOMHighResTimeStamp = performance.now(); const batch: T[] = []; - if (lastPull) { - batch.push((yield* lastPull).value); - lastPull = undefined; + let next: IteratorResult = { + done: true as const, + value: undefined as never, + }; + if (lastPull && options.maxTime) { + const timeout = yield* timebox(options.maxTime, () => lastPull!); + if (timeout.timeout) { + yield* lastPull.halt(); + lastPull = undefined; + } else { + next = timeout.value; + lastPull = undefined; + } + } else { + next = yield* subscription.next(); } - // iterate incoming stream - let next = yield* subscription.next(); // push the next value into the batch while (!next.done) { batch.push(next.value); @@ -66,25 +69,21 @@ export function batch( value: batch, }; } else if (options.maxTime) { - lastPull = yield* spawn(function* () { - const next = yield* subscription.next(); - return next; - }); + const task = yield* spawn(() => subscription.next()); const timeout = yield* timebox( start + options.maxTime - performance.now(), - () => - lastPull!, + () => task!, ); if (timeout.timeout) { - // produce the batch that we have + // produce the batch that we have, save task for next batch + lastPull = task; return { done: false as const, value: batch, }; } else { - lastPull = undefined; next = timeout.value; } } else { @@ -100,10 +99,7 @@ export function batch( }; } - return { - done: true as const, - value: undefined as never, - }; + return next; }, }; }, diff --git a/stream-helpers/for-each.test.ts b/stream-helpers/for-each.test.ts index ed17a922..d083841a 100644 --- a/stream-helpers/for-each.test.ts +++ b/stream-helpers/for-each.test.ts @@ -1,9 +1,4 @@ -import { - createChannel, - sleep, - spawn, - withResolvers, -} from "effection"; +import { createChannel, sleep, spawn, withResolvers } from "effection"; import { describe, it } from "@effectionx/bdd"; import { expect } from "@std/expect"; @@ -46,7 +41,7 @@ describe("forEach", () => { }, stream) ); - yield* spawn(function*() { + yield* spawn(function* () { yield* sleep(1); yield* stream.send("hello"); yield* stream.send("world"); diff --git a/stream-helpers/test-helpers/faucet.test.ts b/stream-helpers/test-helpers/faucet.test.ts index ff9b078a..cc022f93 100644 --- a/stream-helpers/test-helpers/faucet.test.ts +++ b/stream-helpers/test-helpers/faucet.test.ts @@ -113,7 +113,7 @@ describe("useFaucet", () => { }); }); - yield* is(results, results => results.length === 2); + yield* is(results, (results) => results.length === 2); expect(results.valueOf()).toEqual([1, 2]); }); diff --git a/stream-helpers/tracker.test.ts b/stream-helpers/tracker.test.ts index 3c8f49e9..3a70df61 100644 --- a/stream-helpers/tracker.test.ts +++ b/stream-helpers/tracker.test.ts @@ -45,7 +45,7 @@ describe("tracker", () => { yield* tracker; - yield* is(received, received => received.length === 3); + yield* is(received, (received) => received.length === 3); expect(received.valueOf()).toEqual([1, 2, 3]); }); @@ -97,7 +97,7 @@ describe("tracker", () => { yield* send(9); }); - yield* is(received, received => received.flat().length >= 9); + yield* is(received, (received) => received.flat().length >= 9); yield* tracker; expect(received.valueOf()).toEqual([[1, 2, 3], [4, 5, 6], [7, 8, 9]]); diff --git a/task-buffer/task-buffer.test.ts b/task-buffer/task-buffer.test.ts index b69b198d..99b34969 100644 --- a/task-buffer/task-buffer.test.ts +++ b/task-buffer/task-buffer.test.ts @@ -1,5 +1,5 @@ import { FakeTime } from "@std/testing/time"; -import { sleep, spawn, until, type Task } from "effection"; +import { sleep, spawn, type Task, until } from "effection"; import { describe, it } from "@effectionx/bdd"; import { expect } from "@std/expect"; import { useTaskBuffer } from "./task-buffer.ts"; diff --git a/tasks/check-version-mismatches.ts b/tasks/check-version-mismatches.ts index 8ca518af..7890b694 100644 --- a/tasks/check-version-mismatches.ts +++ b/tasks/check-version-mismatches.ts @@ -11,10 +11,12 @@ interface DenoConfig { // First, collect all package versions const packageVersions = new Map(); -for await (const file of expandGlob("**/deno.json", { - root: rootDir, - exclude: ["node_modules", "build"], -})) { +for await ( + const file of expandGlob("**/deno.json", { + root: rootDir, + exclude: ["node_modules", "build"], + }) +) { const content = await Deno.readTextFile(file.path); const config: DenoConfig = JSON.parse(content); @@ -32,10 +34,12 @@ console.log(""); // Now check for mismatches let foundMismatches = false; -for await (const file of expandGlob("**/deno.json", { - root: rootDir, - exclude: ["node_modules", "build"], -})) { +for await ( + const file of expandGlob("**/deno.json", { + root: rootDir, + exclude: ["node_modules", "build"], + }) +) { const content = await Deno.readTextFile(file.path); const config: DenoConfig = JSON.parse(content); const relativePath = file.path.replace(rootDir, ""); diff --git a/tasks/generate-importmap.ts b/tasks/generate-importmap.ts index 383bf298..32d18de2 100644 --- a/tasks/generate-importmap.ts +++ b/tasks/generate-importmap.ts @@ -13,10 +13,12 @@ const imports = new Map(); const workspacePackages = new Map(); // First pass: collect all workspace package names and their exports -for await (const file of expandGlob("**/deno.json", { - root: rootDir, - exclude: ["node_modules", "build"], -})) { +for await ( + const file of expandGlob("**/deno.json", { + root: rootDir, + exclude: ["node_modules", "build"], + }) +) { const content = await Deno.readTextFile(file.path); const config: DenoConfig = JSON.parse(content); @@ -26,7 +28,8 @@ for await (const file of expandGlob("**/deno.json", { if (typeof config.exports === "string") { exportPath = config.exports; } else if (config.exports && typeof config.exports === "object") { - exportPath = config.exports["."] || config.exports["default"] || "./mod.ts"; + exportPath = config.exports["."] || config.exports["default"] || + "./mod.ts"; } // Get package directory relative to root @@ -38,10 +41,12 @@ for await (const file of expandGlob("**/deno.json", { } // Second pass: collect external dependencies and convert workspace packages to local paths -for await (const file of expandGlob("**/deno.json", { - root: rootDir, - exclude: ["node_modules", "build"], -})) { +for await ( + const file of expandGlob("**/deno.json", { + root: rootDir, + exclude: ["node_modules", "build"], + }) +) { const content = await Deno.readTextFile(file.path); const config: DenoConfig = JSON.parse(content); @@ -81,7 +86,7 @@ if (imports.has("@std/testing")) { // Sort by key const sortedImports = Object.fromEntries( - Array.from(imports.entries()).sort(([a], [b]) => a.localeCompare(b)) + Array.from(imports.entries()).sort(([a], [b]) => a.localeCompare(b)), ); const output = { @@ -91,7 +96,7 @@ const output = { const outputPath = `${rootDir}/v4.importmap.json`; await Deno.writeTextFile( outputPath, - JSON.stringify(output, null, 2) + "\n" + JSON.stringify(output, null, 2) + "\n", ); console.log(`Generated ${outputPath} with ${imports.size} dependencies`); diff --git a/tasks/update-effection-version.ts b/tasks/update-effection-version.ts index e31f4909..a3fe058b 100644 --- a/tasks/update-effection-version.ts +++ b/tasks/update-effection-version.ts @@ -7,10 +7,12 @@ interface DenoConfig { imports?: Record; } -for await (const file of expandGlob("**/deno.json", { - root: rootDir, - exclude: ["node_modules", "build"], -})) { +for await ( + const file of expandGlob("**/deno.json", { + root: rootDir, + exclude: ["node_modules", "build"], + }) +) { const content = await Deno.readTextFile(file.path); const config: DenoConfig = JSON.parse(content); From ae1d16341643cb70efdbc7aa93de44a6601d86d4 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sun, 19 Oct 2025 15:37:34 -0400 Subject: [PATCH 18/45] Run v3 and v4 tests --- .github/workflows/verify-posix.yaml | 7 ++++++- .github/workflows/verify-windows.yaml | 26 +++++++++++++++++++++++--- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/.github/workflows/verify-posix.yaml b/.github/workflows/verify-posix.yaml index a99028f7..df553063 100644 --- a/.github/workflows/verify-posix.yaml +++ b/.github/workflows/verify-posix.yaml @@ -34,5 +34,10 @@ jobs: - run: deno fmt --check - run: deno lint + - run: deno task generate-importmap - - run: deno test --allow-net --allow-read --allow-write --allow-env --allow-run --allow-ffi + - name: V3 Tests + run: deno test --allow-net --allow-read --allow-write --allow-env --allow-run --allow-ffi + + - name: V4 Tests + run: deno test --import-map v4.importmap.json --allow-net --allow-read --allow-write --allow-env --allow-run --allow-ffi --trace-leaks diff --git a/.github/workflows/verify-windows.yaml b/.github/workflows/verify-windows.yaml index 16c4e0e6..3ac350bb 100644 --- a/.github/workflows/verify-windows.yaml +++ b/.github/workflows/verify-windows.yaml @@ -41,20 +41,40 @@ jobs: - run: deno lint - - name: "Test (${{matrix.shell}})" + - run: deno task generate-importmap + + - name: "Test V3 (${{matrix.shell}})" if: matrix.shell == 'powershell' || matrix.shell == 'pwsh' run: | $env:SHELL = "${{matrix.shell}}" deno test --allow-net --allow-read --allow-write --allow-env --allow-run --allow-ffi --trace-leaks - - name: "Test (${{matrix.shell}})" + - name: "Test V4 (${{matrix.shell}})" + if: matrix.shell == 'powershell' || matrix.shell == 'pwsh' + run: | + $env:SHELL = "${{matrix.shell}}" + deno test --import-map v4.importmap.json --allow-net --allow-read --allow-write --allow-env --allow-run --allow-ffi --trace-leaks + + - name: "Test V3 (${{matrix.shell}})" if: matrix.shell == 'cmd' run: | set SHELL=${{matrix.shell}} deno test --allow-net --allow-read --allow-write --allow-env --allow-run --allow-ffi --trace-leaks - - name: "Test (${{matrix.shell}})" + - name: "Test V4 (${{matrix.shell}})" + if: matrix.shell == 'cmd' + run: | + set SHELL=${{matrix.shell}} + deno test --import-map v4.importmap.json --allow-net --allow-read --allow-write --allow-env --allow-run --allow-ffi --trace-leaks + + - name: "Test V3 (${{matrix.shell}})" if: matrix.shell == 'bash' run: | export SHELL=${{matrix.shell}} deno test --allow-net --allow-read --allow-write --allow-env --allow-run --allow-ffi --trace-leaks + + - name: "Test V4 (${{matrix.shell}})" + if: matrix.shell == 'bash' + run: | + export SHELL=${{matrix.shell}} + deno test --import-map v4.importmap.json --allow-net --allow-read --allow-write --allow-env --allow-run --allow-ffi --trace-leaks From 8c9cd5869cae4f3f8e9009b339de1e9fe370262c Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sun, 19 Oct 2025 15:40:47 -0400 Subject: [PATCH 19/45] Made generate importmap windows compatible --- tasks/generate-importmap.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tasks/generate-importmap.ts b/tasks/generate-importmap.ts index 32d18de2..e53bf164 100644 --- a/tasks/generate-importmap.ts +++ b/tasks/generate-importmap.ts @@ -1,6 +1,7 @@ #!/usr/bin/env -S deno run --allow-read --allow-write import { expandGlob } from "@std/fs"; +import { fromFileUrl } from "@std/path"; interface DenoConfig { name?: string; @@ -8,7 +9,7 @@ interface DenoConfig { imports?: Record; } -const rootDir = new URL("..", import.meta.url).pathname; +const rootDir = fromFileUrl(new URL("..", import.meta.url)); const imports = new Map(); const workspacePackages = new Map(); @@ -33,8 +34,12 @@ for await ( } // Get package directory relative to root - const packageDir = file.path.replace(rootDir, "").replace("/deno.json", ""); - const fullExportPath = `./${packageDir}/${exportPath.replace("./", "")}`; + const packageDir = file.path.replace(rootDir, "").replace( + /[\/\\]deno\.json$/, + "", + ); + const fullExportPath = `./${packageDir}/${exportPath.replace("./", "")}` + .replace(/\\/g, "/"); workspacePackages.set(config.name, fullExportPath); } From 7353342cab633cf7c51d271e0232c5d3510f33b5 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Mon, 20 Oct 2025 13:12:00 -0500 Subject: [PATCH 20/45] remove second loop --- signals/array.test.ts | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/signals/array.test.ts b/signals/array.test.ts index e9a42a1b..2a8b3055 100644 --- a/signals/array.test.ts +++ b/signals/array.test.ts @@ -1,13 +1,6 @@ import { describe, it } from "@effectionx/bdd"; import { expect } from "@std/expect"; -import { - createChannel, - each, - race, - sleep, - spawn, - withResolvers, -} from "effection"; +import { race, sleep, spawn, withResolvers } from "effection"; import { createArraySignal } from "./array.ts"; @@ -33,20 +26,9 @@ describe("array signal", () => { const array = yield* createArraySignal([]); const { resolve, operation } = withResolvers(); - - const updates = createChannel(); - const subscription = yield* updates; + const subscription = yield* array; yield* spawn(function* () { - for (const update of yield* each(array)) { - yield* updates.send(update); - yield* each.next(); - } - }); - - yield* spawn(function* () { - yield* sleep(1); - array.set([1, 2, 3]); const firstUpdate = yield* subscription.next(); From 770feb3acaa9c35662c0430f0e35cd0adb97da74 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Mon, 20 Oct 2025 13:25:17 -0500 Subject: [PATCH 21/45] s/sleep(1)/sleep(0)/ --- signals/array.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/signals/array.test.ts b/signals/array.test.ts index 2a8b3055..d04cf349 100644 --- a/signals/array.test.ts +++ b/signals/array.test.ts @@ -84,7 +84,7 @@ describe("array signal", () => { }); array.push(1); - yield* sleep(1); + yield* sleep(0); expect(ops).toEqual(["before shift", "got 1", "after shift"]); expect(array.valueOf()).toEqual([]); From 72ed7496a8e3a89037e4064b51566a6d784ba721 Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Mon, 20 Oct 2025 13:45:32 -0500 Subject: [PATCH 22/45] Make the sleeps for v3 compat, not v4 compat --- stream-helpers/for-each.test.ts | 30 +++++++++--------------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/stream-helpers/for-each.test.ts b/stream-helpers/for-each.test.ts index d083841a..c6706114 100644 --- a/stream-helpers/for-each.test.ts +++ b/stream-helpers/for-each.test.ts @@ -1,4 +1,4 @@ -import { createChannel, sleep, spawn, withResolvers } from "effection"; +import { createChannel, sleep, spawn } from "effection"; import { describe, it } from "@effectionx/bdd"; import { expect } from "@std/expect"; @@ -11,44 +11,32 @@ describe("forEach", () => { const stream = createChannel(); const processedItems: number[] = []; - const { resolve, operation } = withResolvers(); - - yield* spawn(() => - forEach(function* (item: number) { - processedItems.push(item); - }, stream) - ); - yield* spawn(function* () { - yield* sleep(1); + yield* sleep(0); yield* stream.send(1); yield* stream.send(2); yield* stream.send(3); - - resolve(); + yield* stream.close(); }); - yield* operation; + yield* forEach(function* (item: number) { + processedItems.push(item); + }, stream); + expect(processedItems).toEqual([1, 2, 3]); }); it("should return the close value of the stream", function* () { const stream = createChannel(); - const result = yield* spawn(() => - forEach(function* () { - // Just process the item - }, stream) - ); - yield* spawn(function* () { - yield* sleep(1); + yield* sleep(0); yield* stream.send("hello"); yield* stream.send("world"); yield* stream.close(42); // Close with value 42 }); - const closeValue = yield* result; + const closeValue = yield* forEach(function* () {}, stream); expect(closeValue).toBe(42); }); }); From b8419eb22b2a52dff2e6ca364b53887e3fedc0bb Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Tue, 28 Oct 2025 19:35:18 -0400 Subject: [PATCH 23/45] Fixed paths in generate importmap script --- tasks/generate-importmap.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tasks/generate-importmap.ts b/tasks/generate-importmap.ts index e53bf164..a2404f85 100644 --- a/tasks/generate-importmap.ts +++ b/tasks/generate-importmap.ts @@ -1,7 +1,7 @@ #!/usr/bin/env -S deno run --allow-read --allow-write import { expandGlob } from "@std/fs"; -import { fromFileUrl } from "@std/path"; +import { fromFileUrl, join } from "@std/path"; interface DenoConfig { name?: string; @@ -98,7 +98,7 @@ const output = { imports: sortedImports, }; -const outputPath = `${rootDir}/v4.importmap.json`; +const outputPath = join(rootDir, 'v4.importmap.json'); await Deno.writeTextFile( outputPath, JSON.stringify(output, null, 2) + "\n", From ad14a459c7b1b115a767f25aa37e37fb2d4e26cc Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Tue, 28 Oct 2025 20:18:28 -0400 Subject: [PATCH 24/45] Fix formatting --- fx/parallel.test.ts | 5 ++++- tasks/generate-importmap.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/fx/parallel.test.ts b/fx/parallel.test.ts index d56a1460..9ba7f201 100644 --- a/fx/parallel.test.ts +++ b/fx/parallel.test.ts @@ -113,7 +113,10 @@ describe("parallel()", () => { function* genFn() { try { - const results = yield* parallel([() => until(one.promise), () => until(two.promise)]); + const results = yield* parallel([ + () => until(one.promise), + () => until(two.promise), + ]); actual = yield* results; } catch (_) { actual = [Err(new Error("should not get hit"))]; diff --git a/tasks/generate-importmap.ts b/tasks/generate-importmap.ts index a2404f85..3072ece6 100644 --- a/tasks/generate-importmap.ts +++ b/tasks/generate-importmap.ts @@ -98,7 +98,7 @@ const output = { imports: sortedImports, }; -const outputPath = join(rootDir, 'v4.importmap.json'); +const outputPath = join(rootDir, "v4.importmap.json"); await Deno.writeTextFile( outputPath, JSON.stringify(output, null, 2) + "\n", From 2f29b4b23ba090bb9288f6ae2762af7c4fc989bd Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Tue, 28 Oct 2025 20:27:57 -0400 Subject: [PATCH 25/45] Let's have an assertion in 'ignores files in .gitignore' test --- watch/test/watch.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/watch/test/watch.test.ts b/watch/test/watch.test.ts index f029d1ca..1111ee3c 100644 --- a/watch/test/watch.test.ts +++ b/watch/test/watch.test.ts @@ -48,6 +48,7 @@ describe("watch", () => { }); it("ignores files in .gitignore", function* () { + expect.assertions(1); let fixture = yield* useFixture(); let processes = yield* inspector( @@ -64,6 +65,8 @@ describe("watch", () => { yield* fixture.write("dist/artifact.txt", "this file was built again"); yield* processes.expectNoRestart(); + + expect(processes.starts).toHaveLength(1); }); it.skip("ignores files in a .gitignore that is in a parent directory", function* () { From 25d85617170dd8ec7ec33fdfce8e848be0964bf1 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sun, 2 Nov 2025 05:03:19 -0800 Subject: [PATCH 26/45] Simplify the batch test to remove timing ambiguity --- stream-helpers/batch.test.ts | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/stream-helpers/batch.test.ts b/stream-helpers/batch.test.ts index f22dcbf1..1726e421 100644 --- a/stream-helpers/batch.test.ts +++ b/stream-helpers/batch.test.ts @@ -1,7 +1,7 @@ import { describe, it } from "@effectionx/bdd"; import { createArraySignal, is } from "@effectionx/signals"; import { expect } from "@std/expect"; -import { sleep, spawn } from "effection"; +import { createChannel, sleep, spawn } from "effection"; import { batch } from "./batch.ts"; import { forEach } from "./for-each.ts"; @@ -9,25 +9,18 @@ import { useFaucet } from "./test-helpers/faucet.ts"; describe("batch", () => { it("creates a batch when maxTime expires", function* () { - const faucet = yield* useFaucet({ open: true }); - const stream = batch({ maxTime: 5 })(faucet); + const source = createChannel(); + const stream = batch({ maxTime: 50 })(source); const subscription = yield* stream; - yield* faucet.pour(function* (send) { - yield* sleep(1); - yield* send(1); - yield* sleep(1); - yield* send(2); - yield* sleep(1); - yield* send(3); - }); - - yield* sleep(10); + let next = yield* spawn(() => subscription.next()); - let next = yield* subscription.next(); + yield* source.send(1); + yield* source.send(2); + yield* source.send(3); - expect(next.value).toEqual([1, 2, 3]); + expect((yield* next).value).toEqual([1, 2, 3]); }); it("creates a batch by maxSize when maxTime is not set", function* () { @@ -79,7 +72,7 @@ describe("batch", () => { const avg = average(windows); const percentDiff = Math.abs((avg - 50) / 50) * 100; - expect(percentDiff).toBeLessThanOrEqual(20); + expect(percentDiff).toBeLessThanOrEqual(30); expect(batches.valueOf().flat()).toHaveLength(10); }); From e7af7cebb586a412b75b8a40a1047cf70d1caed4 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sun, 2 Nov 2025 05:27:19 -0800 Subject: [PATCH 27/45] Make raf test a bit looser --- raf/raf.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/raf/raf.test.ts b/raf/raf.test.ts index 19f137f7..27cd0f28 100644 --- a/raf/raf.test.ts +++ b/raf/raf.test.ts @@ -23,7 +23,7 @@ describe("raf", () => { yield* each.next(); } }); - yield* sleep(100); - expect(count > 5).toBe(true); + yield* sleep(150); + expect(count).toBeGreaterThanOrEqual(5); }); }); From 845616fbcd2a4daaada6e8ee96948f8bc729721c Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sun, 2 Nov 2025 05:51:47 -0800 Subject: [PATCH 28/45] Removed unnecessary sleep --- process/src/exec/win32.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/process/src/exec/win32.ts b/process/src/exec/win32.ts index e733b3be..7296cc99 100644 --- a/process/src/exec/win32.ts +++ b/process/src/exec/win32.ts @@ -144,9 +144,6 @@ export const createWin32Process: CreateOSProcess = function* createWin32Process( // stdin might already be closed } - // Wait for graceful exit with a timeout - yield* race([processResult.operation, sleep(300)]); - // If process still hasn't exited, escalate if ( childProcess.exitCode === null && From 4aed49e55e098cbe518df04987fa91550dfd4a32 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sun, 2 Nov 2025 05:53:07 -0800 Subject: [PATCH 29/45] Fix lint --- process/src/exec/win32.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/process/src/exec/win32.ts b/process/src/exec/win32.ts index 7296cc99..783251d7 100644 --- a/process/src/exec/win32.ts +++ b/process/src/exec/win32.ts @@ -4,9 +4,7 @@ import { createSignal, Err, Ok, - race, type Result, - sleep, spawn, withResolvers, } from "effection"; From f1fc2b3c27cdf5ce78a2db91b33d7f79466885ef Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sun, 2 Nov 2025 09:55:41 -0500 Subject: [PATCH 30/45] Override effection dependency to v3 & v4 --- tasks/build-npm.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tasks/build-npm.ts b/tasks/build-npm.ts index ba1f87ea..295617f3 100644 --- a/tasks/build-npm.ts +++ b/tasks/build-npm.ts @@ -52,6 +52,9 @@ await build({ node: ">= 16", }, sideEffects: false, + dependencies: { + "effection": "^3 || ^4", + }, }, }); From df2ee37305edf2625dab582e09ed8e04329a7622 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sun, 2 Nov 2025 09:53:50 -0500 Subject: [PATCH 31/45] Try commenting out waiting for operation to complete --- process/src/exec/posix.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/process/src/exec/posix.ts b/process/src/exec/posix.ts index 08336b45..0a4a71b5 100644 --- a/process/src/exec/posix.ts +++ b/process/src/exec/posix.ts @@ -86,7 +86,7 @@ export const createPosixProcess: CreateOSProcess = function* createPosixProcess( yield* spawn(function* () { try { let value = yield* once(childProcess, "close"); - yield* all([io.stdoutDone.operation, io.stderrDone.operation]); + // yield* all([io.stdoutDone.operation, io.stderrDone.operation]); processResult.resolve(Ok(value)); } finally { try { From 8100215410f4ab89aba5e5b7b1bff34701ebf495 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Mon, 3 Nov 2025 18:47:47 -0500 Subject: [PATCH 32/45] Refactor inspector into a resource --- watch/test/helpers.ts | 131 ++++++++++++++++++++++-------------------- 1 file changed, 69 insertions(+), 62 deletions(-) diff --git a/watch/test/helpers.ts b/watch/test/helpers.ts index 067a16ce..f75ffa61 100644 --- a/watch/test/helpers.ts +++ b/watch/test/helpers.ts @@ -3,7 +3,7 @@ import { assert } from "@std/assert"; import { emptyDir, ensureDir } from "@std/fs"; import { dirname, fromFileUrl, join } from "@std/path"; import type { Operation, Result, Stream } from "effection"; -import { each, Ok, sleep, spawn, until } from "effection"; +import { all, each, Ok, resource, sleep, spawn, until } from "effection"; import { cp, readFile, writeFile } from "node:fs/promises"; import type { Start } from "../watch.ts"; @@ -67,72 +67,79 @@ type SuccessfulStart = { type ProcessStart = Result; -export function* inspector(stream: Stream) { - let starts: ProcessStart[] = []; +interface Inspector { + starts: ProcessStart[]; + expectNext(): Operation; + expectNoRestart(): Operation; +} - let expected = 0; +export function inspector(stream: Stream): Operation { + return resource(function* (provide) { + let starts: ProcessStart[] = []; - yield* spawn(function* () { - for (let { result } of yield* each(stream)) { - if (result.ok) { - let process = result.value; - let start = { - stdout: "", - stderr: "", - process: result.value, - }; - starts.push(Ok(start)); - yield* spawn(function* () { - for (let chunk of yield* each(process.stdout)) { - start.stdout += String(chunk); - yield* each.next(); - } - }); - yield* spawn(function* () { - for (let chunk of yield* each(process.stderr)) { - start.stderr += String(chunk); - yield* each.next(); - } - }); - } else { - starts.push(result); - } + let expected = 0; - yield* each.next(); - } - }); + yield* spawn(function* () { + for (let { result } of yield* each(stream)) { + if (result.ok) { + let process = result.value; + let start = { + stdout: "", + stderr: "", + process: result.value, + }; + starts.push(Ok(start)); + yield* spawn(function* () { + for (let chunk of yield* each(process.stdout)) { + start.stdout += String(chunk); + yield* each.next(); + } + }); + yield* spawn(function* () { + for (let chunk of yield* each(process.stderr)) { + start.stderr += String(chunk); + yield* each.next(); + } + }); + } else { + starts.push(result); + } - let inspector = { - starts, - *expectNext(): Operation { - let initial = expected; - for (let i = 0; i < 500; i++) { - if (initial < starts.length) { - yield* sleep(10); - expected = starts.length; - let result = inspector.starts[inspector.starts.length - 1]; - if (result.ok) { - return result.value; + yield* each.next(); + } + }); + + yield* provide({ + starts, + *expectNext() { + let initial = expected; + for (let i = 0; i < 500; i++) { + if (initial < starts.length) { + yield* sleep(10); + expected = starts.length; + let result = starts[starts.length - 1]; + if (result.ok) { + return result.value; + } else { + throw new Error( + `expected successful start, but failed: ${result.error}`, + ); + } } else { - throw new Error( - `expected successful start, but failed: ${result.error}`, - ); + yield* sleep(10); } - } else { - yield* sleep(10); } - } - throw new Error(`expecting a sucessful start but it never appeared.`); - }, - *expectNoRestart() { - let prexisting = inspector.starts.length; - yield* sleep(200); - let restarts = inspector.starts.length - prexisting; - assert( - restarts === 0, - `expected no process restarts to have happened, but instead there were: ${restarts}`, - ); - }, - }; - return inspector; + throw new Error(`expecting a sucessful start but it never appeared.`); + }, + *expectNoRestart() { + let prexisting = starts.length; + yield* sleep(200); + let restarts = starts.length - prexisting; + assert( + restarts === 0, + `expected no process restarts to have happened, but instead there were: ${restarts}`, + ); + }, + }); + }); } From 994550b1cdb8e5bad1de02d563232b38f30121b5 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Wed, 5 Nov 2025 16:53:52 -0500 Subject: [PATCH 33/45] Fix lint --- watch/test/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/watch/test/helpers.ts b/watch/test/helpers.ts index f75ffa61..f7b28016 100644 --- a/watch/test/helpers.ts +++ b/watch/test/helpers.ts @@ -3,7 +3,7 @@ import { assert } from "@std/assert"; import { emptyDir, ensureDir } from "@std/fs"; import { dirname, fromFileUrl, join } from "@std/path"; import type { Operation, Result, Stream } from "effection"; -import { all, each, Ok, resource, sleep, spawn, until } from "effection"; +import { each, Ok, resource, sleep, spawn, until } from "effection"; import { cp, readFile, writeFile } from "node:fs/promises"; import type { Start } from "../watch.ts"; From bc5ed012807f15a9bc79911487e99848c236b48c Mon Sep 17 00:00:00 2001 From: Charles Lowell Date: Wed, 5 Nov 2025 16:36:52 -0600 Subject: [PATCH 34/45] convert process to resource --- process/src/exec/posix.ts | 177 +++++++++++++++++----------------- process/src/exec/win32.ts | 196 +++++++++++++++++++------------------- 2 files changed, 188 insertions(+), 185 deletions(-) diff --git a/process/src/exec/posix.ts b/process/src/exec/posix.ts index 0a4a71b5..81c6e97b 100644 --- a/process/src/exec/posix.ts +++ b/process/src/exec/posix.ts @@ -4,6 +4,7 @@ import { createSignal, Err, Ok, + resource, type Result, spawn, withResolvers, @@ -16,78 +17,107 @@ import { ExecError } from "./error.ts"; type ProcessResultValue = [number?, string?]; -export const createPosixProcess: CreateOSProcess = function* createPosixProcess( +export const createPosixProcess: CreateOSProcess = ( command, options, -) { - let processResult = withResolvers>(); +) => { + return resource(function* (provide) { + let processResult = withResolvers>(); - // Killing all child processes started by this command is surprisingly - // tricky. If a process spawns another processes and we kill the parent, - // then the child process is NOT automatically killed. Instead we're using - // the `detached` option to force the child into its own process group, - // which all of its children in turn will inherit. By sending the signal to - // `-pid` rather than `pid`, we are sending it to the entire process group - // instead. This will send the signal to all processes started by the child - // process. - // - // More information here: https://unix.stackexchange.com/questions/14815/process-descendants - let childProcess = spawnProcess(command, options.arguments || [], { - detached: true, - shell: options.shell, - env: options.env, - cwd: options.cwd, - stdio: "pipe", - }); + // Killing all child processes started by this command is surprisingly + // tricky. If a process spawns another processes and we kill the parent, + // then the child process is NOT automatically killed. Instead we're using + // the `detached` option to force the child into its own process group, + // which all of its children in turn will inherit. By sending the signal to + // `-pid` rather than `pid`, we are sending it to the entire process group + // instead. This will send the signal to all processes started by the child + // process. + // + // More information here: https://unix.stackexchange.com/questions/14815/process-descendants + let childProcess = spawnProcess(command, options.arguments || [], { + detached: true, + shell: options.shell, + env: options.env, + cwd: options.cwd, + stdio: "pipe", + }); - let { pid } = childProcess; + let { pid } = childProcess; - let io = { - stdout: yield* useReadable(childProcess.stdout), - stderr: yield* useReadable(childProcess.stderr), - stdoutDone: withResolvers(), - stderrDone: withResolvers(), - }; + let io = { + stdout: yield* useReadable(childProcess.stdout), + stderr: yield* useReadable(childProcess.stderr), + stdoutDone: withResolvers(), + stderrDone: withResolvers(), + }; - let stdout = createSignal(); - let stderr = createSignal(); + let stdout = createSignal(); + let stderr = createSignal(); - yield* spawn(function* () { - let next = yield* io.stdout.next(); - while (!next.done) { - stdout.send(next.value); - next = yield* io.stdout.next(); - } - stdout.close(); - io.stdoutDone.resolve(); - }); + yield* spawn(function* () { + let next = yield* io.stdout.next(); + while (!next.done) { + stdout.send(next.value); + next = yield* io.stdout.next(); + } + stdout.close(); + io.stdoutDone.resolve(); + }); - yield* spawn(function* () { - let next = yield* io.stderr.next(); - while (!next.done) { - stderr.send(next.value); - next = yield* io.stderr.next(); - } - stderr.close(); - io.stderrDone.resolve(); - }); + yield* spawn(function* () { + let next = yield* io.stderr.next(); + while (!next.done) { + stderr.send(next.value); + next = yield* io.stderr.next(); + } + stderr.close(); + io.stderrDone.resolve(); + }); - yield* spawn(function* trapError() { - let [error] = yield* once<[Error]>(childProcess, "error"); - processResult.resolve(Err(error)); - }); + let stdin: Writable = { + send(data: string) { + childProcess.stdin.write(data); + }, + }; - let stdin: Writable = { - send(data: string) { - childProcess.stdin.write(data); - }, - }; + yield* spawn(function* trapError() { + let [error] = yield* once<[Error]>(childProcess, "error"); + processResult.resolve(Err(error)); + }); - yield* spawn(function* () { - try { + yield* spawn(function* () { let value = yield* once(childProcess, "close"); - // yield* all([io.stdoutDone.operation, io.stderrDone.operation]); processResult.resolve(Ok(value)); + }); + + function* join() { + let result = yield* processResult.operation; + if (result.ok) { + let [code, signal] = result.value; + return { command, options, code, signal } as ExitStatus; + } else { + throw result.error; + } + } + + function* expect() { + let status: ExitStatus = yield* join(); + if (status.code != 0) { + throw new ExecError(status, command, options); + } else { + return status; + } + } + + try { + yield* provide({ + pid: pid as number, + stdin, + stdout, + stderr, + join, + expect, + }); } finally { try { if (typeof childProcess.pid === "undefined") { @@ -101,33 +131,4 @@ export const createPosixProcess: CreateOSProcess = function* createPosixProcess( } } }); - - function* join() { - let result = yield* processResult.operation; - if (result.ok) { - let [code, signal] = result.value; - return { command, options, code, signal } as ExitStatus; - } else { - throw result.error; - } - } - - function* expect() { - let status: ExitStatus = yield* join(); - if (status.code != 0) { - throw new ExecError(status, command, options); - } else { - return status; - } - } - - // FYI: this function starts a process and returns without blocking - return { - pid: pid as number, - stdin, - stdout, - stderr, - join, - expect, - }; }; diff --git a/process/src/exec/win32.ts b/process/src/exec/win32.ts index 783251d7..659d15f5 100644 --- a/process/src/exec/win32.ts +++ b/process/src/exec/win32.ts @@ -4,6 +4,7 @@ import { createSignal, Err, Ok, + resource, type Result, spawn, withResolvers, @@ -31,81 +32,111 @@ function* killTree(pid: number) { } } -export const createWin32Process: CreateOSProcess = function* createWin32Process( +export const createWin32Process: CreateOSProcess = ( command, options, -) { - let processResult = withResolvers>(); - - // Windows-specific process spawning with different options than POSIX - let childProcess = spawnProcess(command, options.arguments || [], { - // We lose exit information and events if this is detached in windows - // and it opens a window in windows+powershell. - detached: false, - // The `shell` option is passed to `cross-spawn` to control whether a shell is used. - // On Windows, `shell: true` is necessary to run command strings, as it uses - // `cmd.exe` to parse the command and find executables in the PATH. - // Using a boolean `true` was previously disabled, causing ENOENT errors for - // commands that were not a direct path to an executable. - shell: options.shell || false, - // With stdio as pipe, windows gets stuck where neither the child nor the - // parent wants to close the stream, so we call it ourselves in the exit event. - stdio: "pipe", - // Hide the child window so that killing it will not block the parent - // with a Terminate Batch Process (Y/n) - windowsHide: true, - env: options.env, - cwd: options.cwd, - }); - - let { pid } = childProcess; - - let io = { - stdout: yield* useReadable(childProcess.stdout), - stderr: yield* useReadable(childProcess.stderr), - stdoutDone: withResolvers(), - stderrDone: withResolvers(), - }; - - const stdout = createSignal(); - const stderr = createSignal(); - - yield* spawn(function* () { - let next = yield* io.stdout.next(); - while (!next.done) { - stdout.send(next.value); - next = yield* io.stdout.next(); +) => { + return resource(function* (provide) { + let processResult = withResolvers>(); + + // Windows-specific process spawning with different options than POSIX + let childProcess = spawnProcess(command, options.arguments || [], { + // We lose exit information and events if this is detached in windows + // and it opens a window in windows+powershell. + detached: false, + // The `shell` option is passed to `cross-spawn` to control whether a shell is used. + // On Windows, `shell: true` is necessary to run command strings, as it uses + // `cmd.exe` to parse the command and find executables in the PATH. + // Using a boolean `true` was previously disabled, causing ENOENT errors for + // commands that were not a direct path to an executable. + shell: options.shell || false, + // With stdio as pipe, windows gets stuck where neither the child nor the + // parent wants to close the stream, so we call it ourselves in the exit event. + stdio: "pipe", + // Hide the child window so that killing it will not block the parent + // with a Terminate Batch Process (Y/n) + windowsHide: true, + env: options.env, + cwd: options.cwd, + }); + + let { pid } = childProcess; + + let io = { + stdout: yield* useReadable(childProcess.stdout), + stderr: yield* useReadable(childProcess.stderr), + stdoutDone: withResolvers(), + stderrDone: withResolvers(), + }; + + const stdout = createSignal(); + const stderr = createSignal(); + + yield* spawn(function* () { + let next = yield* io.stdout.next(); + while (!next.done) { + stdout.send(next.value); + next = yield* io.stdout.next(); + } + stdout.close(); + io.stdoutDone.resolve(); + }); + + yield* spawn(function* () { + let next = yield* io.stderr.next(); + while (!next.done) { + stderr.send(next.value); + next = yield* io.stderr.next(); + } + stderr.close(); + io.stderrDone.resolve(); + }); + + let stdin: Writable = { + send(data: string) { + childProcess.stdin.write(data); + }, + }; + + yield* spawn(function* trapError() { + const [error] = yield* once(childProcess, "error"); + processResult.resolve(Err(error)); + }); + + yield* spawn(function* () { + let value = yield* once(childProcess, "close"); + yield* all([io.stdoutDone.operation, io.stderrDone.operation]); + processResult.resolve(Ok(value)); + }); + + function* join() { + let result = yield* processResult.operation; + if (result.ok) { + let [code, signal] = result.value; + return { command, options, code, signal } as ExitStatus; + } else { + throw result.error; + } } - stdout.close(); - io.stdoutDone.resolve(); - }); - yield* spawn(function* () { - let next = yield* io.stderr.next(); - while (!next.done) { - stderr.send(next.value); - next = yield* io.stderr.next(); + function* expect() { + let status = yield* join(); + if (status.code != 0) { + throw new ExecError(status, command, options); + } else { + return status; + } } - stderr.close(); - io.stderrDone.resolve(); - }); - yield* spawn(function* trapError() { - const [error] = yield* once(childProcess, "error"); - processResult.resolve(Err(error)); - }); - - let stdin: Writable = { - send(data: string) { - childProcess.stdin.write(data); - }, - }; - - yield* spawn(function* () { try { - let value = yield* once(childProcess, "close"); - yield* all([io.stdoutDone.operation, io.stderrDone.operation]); - processResult.resolve(Ok(value)); + yield* provide({ + pid: pid as number, + stdin, + stdout, + stderr, + join, + expect, + }); } finally { try { // Only try to kill the process if it hasn't exited yet @@ -173,35 +204,6 @@ export const createWin32Process: CreateOSProcess = function* createWin32Process( } } }); - - function* join() { - let result = yield* processResult.operation; - if (result.ok) { - let [code, signal] = result.value; - return { command, options, code, signal } as ExitStatus; - } else { - throw result.error; - } - } - - function* expect() { - let status = yield* join(); - if (status.code != 0) { - throw new ExecError(status, command, options); - } else { - return status; - } - } - - // FYI: this function starts a process and returns without blocking - return { - pid: pid as number, - stdin, - stdout, - stderr, - join, - expect, - }; }; export const isWin32 = (): boolean => platform() === "win32"; From 73cb39bc41896c24412872dd9f54a05c56aa579c Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sun, 7 Dec 2025 19:22:04 -0500 Subject: [PATCH 35/45] Add sanitization options to test functions --- bdd/mod.ts | 91 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 74 insertions(+), 17 deletions(-) diff --git a/bdd/mod.ts b/bdd/mod.ts index 7f544319..1b6777db 100644 --- a/bdd/mod.ts +++ b/bdd/mod.ts @@ -6,16 +6,37 @@ import { it as $it, } from "@std/testing/bdd"; +/** + * Sanitization options for test cases and test suites. + * These options control Deno's test sanitizers. + */ +export interface SanitizeOptions { + /** Ensure the test case does not prematurely cause the process to exit. Defaults to true. */ + sanitizeExit?: boolean; + /** Check that the number of async completed ops after the test is the same as number of dispatched ops. Defaults to true. */ + sanitizeOps?: boolean; + /** Ensure the test case does not "leak" resources - ie. the resource table after the test has exactly the same contents as before the test. Defaults to true. */ + sanitizeResources?: boolean; +} + let current: TestAdapter | undefined; -export function describe(name: string, body: () => void) { +export function describe( + name: string, + body: () => void, + options?: SanitizeOptions, +) { const original = current; try { const child = current = createTestAdapter({ name, parent: original }); - $describe(name, () => { - $afterAll(() => child.destroy()); - body(); + $describe({ + name, + fn: () => { + $afterAll(() => child.destroy()); + body(); + }, + ...options, }); } finally { current = original; @@ -23,7 +44,27 @@ export function describe(name: string, body: () => void) { } describe.skip = $describe.skip; -describe.only = $describe.only; +describe.only = function ( + name: string, + body: () => void, + options?: SanitizeOptions, +): void { + const original = current; + try { + const child = current = createTestAdapter({ name, parent: original }); + + $describe.only({ + name, + fn: () => { + $afterAll(() => child.destroy()); + body(); + }, + ...options, + }); + } finally { + current = original; + } +}; export function beforeAll(body: () => Operation) { current?.addOnetimeSetup(body); @@ -33,17 +74,25 @@ export function beforeEach(body: () => Operation) { current?.addSetup(body); } -export function it(desc: string, body?: () => Operation): void { +export function it( + desc: string, + body?: () => Operation, + options?: SanitizeOptions, +): void { const adapter = current!; if (!body) { $it.skip(desc, () => {}); return; } - $it(desc, async () => { - const result = await adapter.runTest(body); - if (!result.ok) { - throw result.error; - } + $it({ + name: desc, + fn: async () => { + const result = await adapter.runTest(body); + if (!result.ok) { + throw result.error; + } + }, + ...options, }); } @@ -52,12 +101,20 @@ it.skip = (...args: Parameters): ReturnType => { return $it.skip(desc, () => {}); }; -it.only = (desc: string, body: () => Operation): void => { +it.only = ( + desc: string, + body: () => Operation, + options?: SanitizeOptions, +): void => { const adapter = current!; - $it.only(desc, async () => { - const result = await adapter.runTest(body); - if (!result.ok) { - throw result.error; - } + $it.only({ + name: desc, + fn: async () => { + const result = await adapter.runTest(body); + if (!result.ok) { + throw result.error; + } + }, + ...options, }); }; From 2597d124eb196de2d250d0a33881373c37d125c4 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sun, 7 Dec 2025 19:27:13 -0500 Subject: [PATCH 36/45] Update test to disable sanitization options for operations and resources --- watch/test/watch.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/watch/test/watch.test.ts b/watch/test/watch.test.ts index 1111ee3c..23f47f82 100644 --- a/watch/test/watch.test.ts +++ b/watch/test/watch.test.ts @@ -118,4 +118,4 @@ describe("watch", () => { // never exit. // send the command to exit the watch and the main returns }); -}); +}, { sanitizeOps: false, sanitizeResources: false }); From f8d97b54bc6a8bf24b6d3c33a13c0201a92a761a Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sun, 7 Dec 2025 19:35:22 -0500 Subject: [PATCH 37/45] Refactor GitHub Actions workflows to use workflow_call for POSIX and Windows verification, and update job conditions for publishing tasks. --- .github/workflows/publish.yaml | 13 ++++++++++--- .github/workflows/verify-posix.yaml | 4 +--- .github/workflows/verify-windows.yaml | 4 +--- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index c6dc9856..3bea29ef 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -5,7 +5,14 @@ on: - main jobs: + verify-posix: + uses: ./.github/workflows/verify-posix.yaml + + verify-windows: + uses: ./.github/workflows/verify-windows.yaml + generate-matrix: + needs: [verify-posix, verify-windows] name: Generate Job Matrix runs-on: ubuntu-latest outputs: @@ -23,7 +30,7 @@ jobs: id: set-matrix jsr: - if: fromJSON(needs.generate-matrix.outputs.exists) + if: github.ref == 'refs/heads/main' && fromJSON(needs.generate-matrix.outputs.exists) name: Publish ${{ matrix.name }}@${{matrix.version}} to JSR runs-on: ubuntu-latest @@ -46,7 +53,7 @@ jobs: working-directory: ${{ matrix.workspace }} npm: - if: fromJSON(needs.generate-matrix.outputs.exists) + if: github.ref == 'refs/heads/main' && fromJSON(needs.generate-matrix.outputs.exists) name: Publish ${{ matrix.name }}@${{matrix.version}} to NPM runs-on: ubuntu-latest @@ -77,7 +84,7 @@ jobs: NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} tag: - if: fromJSON(needs.generate-matrix.outputs.exists) + if: github.ref == 'refs/heads/main' && fromJSON(needs.generate-matrix.outputs.exists) name: "Tag Release" runs-on: ubuntu-latest needs: [generate-matrix, jsr, npm] diff --git a/.github/workflows/verify-posix.yaml b/.github/workflows/verify-posix.yaml index df553063..918e874e 100644 --- a/.github/workflows/verify-posix.yaml +++ b/.github/workflows/verify-posix.yaml @@ -1,9 +1,7 @@ name: Verify (POSIX) on: - push: - branches: - - main + workflow_call: pull_request: branches: - main diff --git a/.github/workflows/verify-windows.yaml b/.github/workflows/verify-windows.yaml index 3ac350bb..8647fb4f 100644 --- a/.github/workflows/verify-windows.yaml +++ b/.github/workflows/verify-windows.yaml @@ -1,9 +1,7 @@ name: Verify (Windows) on: - push: - branches: - - main + workflow_call: pull_request: branches: - main From c61670397b599eac6ba325bc6220d0dbb9ad29ae Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sun, 7 Dec 2025 19:41:58 -0500 Subject: [PATCH 38/45] Enhance generate-importmap task to fetch the latest v4 version of effection from npm and update deno.json to include --allow-net permission. --- deno.json | 2 +- tasks/generate-importmap.ts | 60 +++++++++++++++++++++++++++++++++++-- 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/deno.json b/deno.json index ce57680e..68a13ddb 100644 --- a/deno.json +++ b/deno.json @@ -45,6 +45,6 @@ "./process" ], "tasks": { - "generate-importmap": "deno run --allow-read --allow-write tasks/generate-importmap.ts" + "generate-importmap": "deno run --allow-read --allow-write --allow-net tasks/generate-importmap.ts" } } diff --git a/tasks/generate-importmap.ts b/tasks/generate-importmap.ts index 3072ece6..20b5f3b6 100644 --- a/tasks/generate-importmap.ts +++ b/tasks/generate-importmap.ts @@ -1,4 +1,4 @@ -#!/usr/bin/env -S deno run --allow-read --allow-write +#!/usr/bin/env -S deno run --allow-read --allow-write --allow-net import { expandGlob } from "@std/fs"; import { fromFileUrl, join } from "@std/path"; @@ -9,6 +9,58 @@ interface DenoConfig { imports?: Record; } +interface NpmRegistryResponse { + versions: Record; +} + +/** + * Fetches the latest effection v4 version from the npm registry. + * Returns the highest v4.x.x version (including prereleases). + */ +async function getLatestEffectionV4(): Promise { + const response = await fetch("https://registry.npmjs.org/effection"); + if (!response.ok) { + throw new Error(`Failed to fetch npm registry: ${response.statusText}`); + } + + const data: NpmRegistryResponse = await response.json(); + const versions = Object.keys(data.versions); + + // Filter to v4.x.x versions only + const v4Versions = versions.filter((v) => v.startsWith("4.")); + + if (v4Versions.length === 0) { + throw new Error("No v4 versions found for effection"); + } + + // Sort versions using Deno's semver comparison + // Parse and sort: stable versions first by semver, then prereleases + v4Versions.sort((a, b) => { + const parseVersion = (v: string) => { + const [main, prerelease] = v.split("-"); + const [major, minor, patch] = main.split(".").map(Number); + return { major, minor, patch, prerelease: prerelease || "" }; + }; + + const va = parseVersion(a); + const vb = parseVersion(b); + + // Compare major.minor.patch first + if (va.major !== vb.major) return va.major - vb.major; + if (va.minor !== vb.minor) return va.minor - vb.minor; + if (va.patch !== vb.patch) return va.patch - vb.patch; + + // If one has prerelease and other doesn't, stable comes after prerelease + if (!va.prerelease && vb.prerelease) return 1; + if (va.prerelease && !vb.prerelease) return -1; + + // Both have prereleases, compare alphabetically + return va.prerelease.localeCompare(vb.prerelease); + }); + + return v4Versions[v4Versions.length - 1]; +} + const rootDir = fromFileUrl(new URL("..", import.meta.url)); const imports = new Map(); const workspacePackages = new Map(); @@ -76,8 +128,10 @@ for await ( } } -// Add effection v4 -imports.set("effection", "npm:effection@^4.0.0-0"); +// Add effection v4 - fetch latest version from npm +const effectionV4Version = await getLatestEffectionV4(); +imports.set("effection", `npm:effection@${effectionV4Version}`); +console.log(`Using effection v4: ${effectionV4Version}`); // Add @std/testing sub-packages if @std/testing is present if (imports.has("@std/testing")) { From c306e053afbed7636d66cb567f76a7e9e265318e Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sun, 7 Dec 2025 19:46:41 -0500 Subject: [PATCH 39/45] Add testing instructions for compatibility with effection v3 and v4 in README.md --- README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/README.md b/README.md index 09fc6e12..e4518435 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,31 @@ more than a few times are welcome. 5. Add doc strings to your source code - they will be used for documentation on the site. +## Testing + +All packages are tested against both effection v3 and v4 to ensure +compatibility. + +### Running tests with v3 (default) + +```bash +deno test -A +``` + +### Running tests with v4 + +First, generate the v4 import map (fetches the latest v4 version from npm): + +```bash +deno task generate-importmap +``` + +Then run tests with the import map: + +```bash +deno test --import-map v4.importmap.json -A +``` + ## To publish a new project 1. Member of [jsr.io/@effectionx](https://jsr.io/@effectionx) has to add that From 7733a58f2d917967bb0e7d523dfc0b8c2cbbcbe1 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sun, 7 Dec 2025 19:56:38 -0500 Subject: [PATCH 40/45] Update package versions and effection dependency in various modules --- bdd/deno.json | 2 +- context-api/deno.json | 2 +- deno-deploy/deno.json | 2 +- fx/deno.json | 2 +- jsonl-store/deno.json | 2 +- process/deno.json | 2 +- raf/deno.json | 2 +- signals/deno.json | 2 +- stream-helpers/deno.json | 2 +- task-buffer/deno.json | 2 +- tasks/build-npm.ts | 2 +- test-adapter/deno.json | 2 +- timebox/deno.json | 2 +- tinyexec/deno.json | 2 +- watch/deno.json | 2 +- websocket/deno.json | 2 +- worker/deno.json | 2 +- 17 files changed, 17 insertions(+), 17 deletions(-) diff --git a/bdd/deno.json b/bdd/deno.json index cdf2fc77..1f1c5c8e 100644 --- a/bdd/deno.json +++ b/bdd/deno.json @@ -1,7 +1,7 @@ { "name": "@effectionx/bdd", "exports": "./mod.ts", - "version": "0.2.2", + "version": "0.3.0", "license": "MIT", "imports": { "effection": "npm:effection@^3", diff --git a/context-api/deno.json b/context-api/deno.json index 4e5dee0f..a0cf6546 100644 --- a/context-api/deno.json +++ b/context-api/deno.json @@ -1,6 +1,6 @@ { "name": "@effectionx/context-api", - "version": "0.1.0", + "version": "0.2.0", "license": "MIT", "exports": "./mod.ts", "imports": { diff --git a/deno-deploy/deno.json b/deno-deploy/deno.json index 6717186c..0d8b8ad4 100644 --- a/deno-deploy/deno.json +++ b/deno-deploy/deno.json @@ -1,6 +1,6 @@ { "name": "@effectionx/deno-deploy", - "version": "0.2.0", + "version": "0.3.0", "exports": "./mod.ts", "license": "MIT", "imports": { diff --git a/fx/deno.json b/fx/deno.json index d8644a36..e970f160 100644 --- a/fx/deno.json +++ b/fx/deno.json @@ -1,6 +1,6 @@ { "name": "@effectionx/fx", - "version": "0.2.0", + "version": "0.3.0", "exports": "./mod.ts", "license": "MIT", "imports": { diff --git a/jsonl-store/deno.json b/jsonl-store/deno.json index 8fa92f95..90dc4ef4 100644 --- a/jsonl-store/deno.json +++ b/jsonl-store/deno.json @@ -1,6 +1,6 @@ { "name": "@effectionx/jsonl-store", - "version": "0.1.0", + "version": "0.3.0", "exports": "./mod.ts", "license": "MIT", "imports": { diff --git a/process/deno.json b/process/deno.json index f50b1a68..efa68a5f 100644 --- a/process/deno.json +++ b/process/deno.json @@ -1,7 +1,7 @@ { "name": "@effectionx/process", "exports": "./mod.ts", - "version": "0.5.0", + "version": "0.6.0", "license": "MIT", "imports": { "@types/cross-spawn": "npm:@types/cross-spawn@6.0.6", diff --git a/raf/deno.json b/raf/deno.json index a1d96d04..d471846d 100644 --- a/raf/deno.json +++ b/raf/deno.json @@ -1,6 +1,6 @@ { "name": "@effectionx/raf", - "version": "0.0.1", + "version": "0.1.0", "exports": "./mod.ts", "license": "MIT", "imports": { diff --git a/signals/deno.json b/signals/deno.json index 79dbf2b6..9b170582 100644 --- a/signals/deno.json +++ b/signals/deno.json @@ -1,6 +1,6 @@ { "name": "@effectionx/signals", - "version": "0.3.0", + "version": "0.4.0", "imports": { "effection": "npm:effection@^3", "immutable": "npm:immutable@^5", diff --git a/stream-helpers/deno.json b/stream-helpers/deno.json index 937a6297..497822df 100644 --- a/stream-helpers/deno.json +++ b/stream-helpers/deno.json @@ -1,6 +1,6 @@ { "name": "@effectionx/stream-helpers", - "version": "0.3.0", + "version": "0.4.0", "imports": { "effection": "npm:effection@^3", "immutable": "npm:immutable@^5", diff --git a/task-buffer/deno.json b/task-buffer/deno.json index bd16fb2f..08d0538b 100644 --- a/task-buffer/deno.json +++ b/task-buffer/deno.json @@ -1,6 +1,6 @@ { "name": "@effectionx/task-buffer", - "version": "1.0.1", + "version": "1.2.0", "exports": "./mod.ts", "license": "MIT", "imports": { diff --git a/tasks/build-npm.ts b/tasks/build-npm.ts index 295617f3..7c97626f 100644 --- a/tasks/build-npm.ts +++ b/tasks/build-npm.ts @@ -53,7 +53,7 @@ await build({ }, sideEffects: false, dependencies: { - "effection": "^3 || ^4", + "effection": "^3 || ^4.0.0-0", }, }, }); diff --git a/test-adapter/deno.json b/test-adapter/deno.json index bda1334a..aea3ef9f 100644 --- a/test-adapter/deno.json +++ b/test-adapter/deno.json @@ -1,6 +1,6 @@ { "name": "@effectionx/test-adapter", - "version": "0.1.0", + "version": "0.6.0", "license": "MIT", "exports": "./mod.ts", "imports": { diff --git a/timebox/deno.json b/timebox/deno.json index a426a61f..e5638906 100644 --- a/timebox/deno.json +++ b/timebox/deno.json @@ -1,6 +1,6 @@ { "name": "@effectionx/timebox", - "version": "0.1.0", + "version": "0.3.0", "license": "MIT", "exports": "./mod.ts", "imports": { diff --git a/tinyexec/deno.json b/tinyexec/deno.json index 98e6c59e..8a0377c9 100644 --- a/tinyexec/deno.json +++ b/tinyexec/deno.json @@ -1,6 +1,6 @@ { "name": "@effectionx/tinyexec", - "version": "0.2.0", + "version": "0.3.0", "exports": "./mod.ts", "license": "MIT", "imports": { diff --git a/watch/deno.json b/watch/deno.json index 06b652cf..a4c535bd 100644 --- a/watch/deno.json +++ b/watch/deno.json @@ -1,6 +1,6 @@ { "name": "@effectionx/watch", - "version": "0.1.1", + "version": "0.3.0", "license": "MIT", "exports": { ".": "./main.ts", diff --git a/websocket/deno.json b/websocket/deno.json index 2a7cf842..eaeb307f 100644 --- a/websocket/deno.json +++ b/websocket/deno.json @@ -1,6 +1,6 @@ { "name": "@effectionx/websocket", - "version": "2.0.0", + "version": "2.2.0", "exports": "./mod.ts", "license": "MIT", "imports": { diff --git a/worker/deno.json b/worker/deno.json index 38bb17c4..f12fa6f9 100644 --- a/worker/deno.json +++ b/worker/deno.json @@ -1,6 +1,6 @@ { "name": "@effectionx/worker", - "version": "0.1.0", + "version": "0.3.0", "license": "MIT", "exports": "./mod.ts", "imports": { From 5d5b801a65eb6e33c4da0bd89ef6110c2ea69b7e Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sun, 7 Dec 2025 20:01:29 -0500 Subject: [PATCH 41/45] Update README.md to provide comprehensive documentation on process management features, including usage examples for exec() and daemon() functions, and clarify error handling with join() and expect() methods. --- process/README.md | 207 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 198 insertions(+), 9 deletions(-) diff --git a/process/README.md b/process/README.md index 3866cda0..f4aa2f5d 100644 --- a/process/README.md +++ b/process/README.md @@ -1,12 +1,201 @@ -# @effection/process +# Process -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![Created by Frontside](https://img.shields.io/badge/created%20by-frontside-26abe8.svg)](https://frontside.com) -[![Chat on Discord](https://img.shields.io/discord/700803887132704931?Label=Discord)](https://discord.gg/Ug5nWH8) +Execute and manage system processes with structured concurrency. A library for +spawning and controlling child processes in Effection programs. -[Effection][Effection] is the structured concurrency toolkit for JavaScript. You -can find detailed information about using effection to manage system process in -node in the -[spawning processes guide](https://frontside.com/effection/docs/guides/processes) +--- -[Effection]: https://frontside.com/effection +This package provides two main functions: `exec()` for running processes with a +finite lifetime, and `daemon()` for long-running processes like servers. + +## Features + +- Stream-based access to stdout and stderr +- Writable stdin for sending input to processes +- Proper signal handling and cleanup on both POSIX and Windows +- Shell mode for complex commands with glob expansion +- Structured error handling with `join()` and `expect()` methods + +## Basic Usage + +### Running a Command + +Use `exec()` to run a command and wait for it to complete: + +```typescript +import { main } from "effection"; +import { exec } from "@effectionx/process"; + +await main(function* () { + // Run a command and get the result + let result = yield* exec("echo 'Hello World'").join(); + + console.log(result.stdout); // "Hello World\n" + console.log(result.code); // 0 +}); +``` + +### Streaming Output + +Access stdout and stderr as streams for real-time output processing: + +```typescript +import { each, main, spawn } from "effection"; +import { exec } from "@effectionx/process"; + +await main(function* () { + let process = yield* exec("npm install"); + + // Stream stdout in real-time + yield* spawn(function* () { + for (let chunk of yield* each(yield* process.stdout)) { + console.log(chunk); + yield* each.next(); + } + }); + + // Wait for the process to complete + yield* process.expect(); +}); +``` + +### Sending Input to stdin + +Write to a process's stdin: + +```typescript +import { main } from "effection"; +import { exec } from "@effectionx/process"; + +await main(function* () { + let process = yield* exec("cat"); + + process.stdin.send("Hello from stdin!\n"); + + let result = yield* process.join(); + console.log(result.stdout); // "Hello from stdin!\n" +}); +``` + +## join() vs expect() + +Both methods wait for the process to complete and collect stdout/stderr, but +they differ in error handling: + +- **`join()`** - Always returns the result, regardless of exit code +- **`expect()`** - Throws an `ExecError` if the process exits with a non-zero + code + +```typescript +import { main } from "effection"; +import { exec, ExecError } from "@effectionx/process"; + +await main(function* () { + // join() returns result even on failure + let result = yield* exec("exit 1", { shell: true }).join(); + console.log(result.code); // 1 + + // expect() throws on non-zero exit + try { + yield* exec("exit 1", { shell: true }).expect(); + } catch (error) { + if (error instanceof ExecError) { + console.log(error.message); // Command failed with exit code 1 + } + } +}); +``` + +## Running Daemons + +Use `daemon()` for long-running processes like servers. Unlike `exec()`, a +daemon is expected to run forever - if it exits prematurely, it raises an error: + +```typescript +import { main, suspend } from "effection"; +import { daemon } from "@effectionx/process"; + +await main(function* () { + // Start a web server + let server = yield* daemon("node server.js"); + + console.log(`Server started with PID: ${server.pid}`); + + // The server will be automatically terminated when this scope exits + yield* suspend(); +}); +``` + +## Options + +The `exec()` and `daemon()` functions accept an options object: + +```typescript +interface ExecOptions { + // Additional arguments to pass to the command + arguments?: string[]; + + // Environment variables for the process + env?: Record; + + // Use shell to interpret the command (enables glob expansion, pipes, etc.) + // Can be true for default shell or a path to a specific shell + shell?: boolean | string; + + // Working directory for the process + cwd?: string; +} +``` + +### Examples + +```typescript +import { main } from "effection"; +import { exec } from "@effectionx/process"; + +await main(function* () { + // Pass arguments + yield* exec("git", { + arguments: ["commit", "-m", "Initial commit"], + }).expect(); + + // Set environment variables + yield* exec("node app.js", { + env: { NODE_ENV: "production", PORT: "3000" }, + }).expect(); + + // Use shell mode for complex commands + yield* exec("ls *.ts | wc -l", { + shell: true, + }).expect(); + + // Set working directory + yield* exec("npm install", { + cwd: "./packages/my-package", + }).expect(); +}); +``` + +## Process Interface + +The `Process` object returned by `exec()` provides: + +```typescript +interface Process { + // Process ID + readonly pid: number; + + // Output streams + stdout: Stream; + stderr: Stream; + + // Input stream + stdin: Writable; + + // Wait for completion (returns exit status) + join(): Operation; + + // Wait for successful completion (throws on non-zero exit) + expect(): Operation; +} +``` From 6b8458b2f616e610d6d0dbba9e24206591aaa945 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sun, 7 Dec 2025 20:07:34 -0500 Subject: [PATCH 42/45] Update @effectionx/bdd and related dependencies to version 0.3.0 across multiple modules, and update @effectionx/test-adapter to version 0.6.0 in bdd module. --- bdd/deno.json | 2 +- context-api/deno.json | 2 +- fx/deno.json | 2 +- jsonl-store/deno.json | 2 +- process/deno.json | 6 +++--- raf/deno.json | 2 +- signals/deno.json | 2 +- stream-helpers/deno.json | 6 +++--- task-buffer/deno.json | 2 +- timebox/deno.json | 2 +- watch/deno.json | 8 ++++---- websocket/deno.json | 2 +- worker/deno.json | 6 +++--- 13 files changed, 22 insertions(+), 22 deletions(-) diff --git a/bdd/deno.json b/bdd/deno.json index 1f1c5c8e..99aa6f02 100644 --- a/bdd/deno.json +++ b/bdd/deno.json @@ -5,7 +5,7 @@ "license": "MIT", "imports": { "effection": "npm:effection@^3", - "@effectionx/test-adapter": "jsr:@effectionx/test-adapter@^0.1.0", + "@effectionx/test-adapter": "jsr:@effectionx/test-adapter@^0.6.0", "@std/testing": "jsr:@std/testing@^1" } } diff --git a/context-api/deno.json b/context-api/deno.json index a0cf6546..9121f973 100644 --- a/context-api/deno.json +++ b/context-api/deno.json @@ -6,6 +6,6 @@ "imports": { "effection": "npm:effection@^3", "@std/expect": "jsr:@std/expect@^1", - "@effectionx/bdd": "jsr:@effectionx/bdd@0.2.2" + "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0" } } diff --git a/fx/deno.json b/fx/deno.json index e970f160..dd3da02a 100644 --- a/fx/deno.json +++ b/fx/deno.json @@ -6,6 +6,6 @@ "imports": { "effection": "npm:effection@^3", "@std/expect": "jsr:@std/expect@^1", - "@effectionx/bdd": "jsr:@effectionx/bdd@0.2.2" + "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0" } } diff --git a/jsonl-store/deno.json b/jsonl-store/deno.json index 90dc4ef4..88bde8a6 100644 --- a/jsonl-store/deno.json +++ b/jsonl-store/deno.json @@ -9,7 +9,7 @@ "@std/streams": "jsr:@std/streams@^1", "@std/fs": "jsr:@std/fs@^1", "@std/path": "jsr:@std/path@^1", - "@effectionx/bdd": "jsr:@effectionx/bdd@0.2.2", + "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0", "@std/expect": "jsr:@std/expect@^1" } } diff --git a/process/deno.json b/process/deno.json index efa68a5f..8e809548 100644 --- a/process/deno.json +++ b/process/deno.json @@ -10,8 +10,8 @@ "effection": "npm:effection@^3", "shellwords": "npm:shellwords@^1.1.1", "@std/expect": "jsr:@std/expect@1.0.17", - "@effectionx/stream-helpers": "npm:@effectionx/stream-helpers@0.3.0", - "@effectionx/bdd": "jsr:@effectionx/bdd@0.2.2", - "@effectionx/signals": "npm:@effectionx/signals@0.3.0" + "@effectionx/stream-helpers": "npm:@effectionx/stream-helpers@0.4.0", + "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0", + "@effectionx/signals": "npm:@effectionx/signals@0.4.0" } } diff --git a/raf/deno.json b/raf/deno.json index d471846d..35353bc6 100644 --- a/raf/deno.json +++ b/raf/deno.json @@ -7,6 +7,6 @@ "effection": "npm:effection@^3", "@essentials/raf": "npm:@essentials/raf@^1.2.0", "@std/expect": "jsr:@std/expect@^1", - "@effectionx/bdd": "jsr:@effectionx/bdd@0.2.2" + "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0" } } diff --git a/signals/deno.json b/signals/deno.json index 9b170582..17a402ea 100644 --- a/signals/deno.json +++ b/signals/deno.json @@ -5,7 +5,7 @@ "effection": "npm:effection@^3", "immutable": "npm:immutable@^5", "@std/expect": "jsr:@std/expect@^1", - "@effectionx/bdd": "jsr:@effectionx/bdd@0.2.2" + "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0" }, "license": "MIT", "exports": { diff --git a/stream-helpers/deno.json b/stream-helpers/deno.json index 497822df..5d174cc9 100644 --- a/stream-helpers/deno.json +++ b/stream-helpers/deno.json @@ -4,11 +4,11 @@ "imports": { "effection": "npm:effection@^3", "immutable": "npm:immutable@^5", - "@effectionx/signals": "npm:@effectionx/signals@0.3.0", + "@effectionx/signals": "npm:@effectionx/signals@0.4.0", "@std/expect": "jsr:@std/expect@^1", "@std/testing": "jsr:@std/testing@^1", - "@effectionx/timebox": "npm:@effectionx/timebox@0.1.0", - "@effectionx/bdd": "jsr:@effectionx/bdd@0.2.2", + "@effectionx/timebox": "npm:@effectionx/timebox@0.3.0", + "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0", "remeda": "npm:remeda@^2" }, "license": "MIT", diff --git a/task-buffer/deno.json b/task-buffer/deno.json index 08d0538b..4537d9ca 100644 --- a/task-buffer/deno.json +++ b/task-buffer/deno.json @@ -6,7 +6,7 @@ "imports": { "effection": "npm:effection@^3", "@std/expect": "jsr:@std/expect@^1", - "@effectionx/bdd": "jsr:@effectionx/bdd@0.2.2", + "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0", "@std/testing": "jsr:@std/testing@^1" } } diff --git a/timebox/deno.json b/timebox/deno.json index e5638906..9706bfac 100644 --- a/timebox/deno.json +++ b/timebox/deno.json @@ -6,6 +6,6 @@ "imports": { "effection": "npm:effection@^3", "@std/expect": "jsr:@std/expect@1", - "@effectionx/bdd": "jsr:@effectionx/bdd@0.2.2" + "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0" } } diff --git a/watch/deno.json b/watch/deno.json index a4c535bd..c732fa55 100644 --- a/watch/deno.json +++ b/watch/deno.json @@ -17,10 +17,10 @@ "chokidar": "npm:chokidar@^4.0.3", "zod": "npm:zod@^3.20.2", "zod-opts": "npm:zod-opts@0.1.8", - "@effectionx/bdd": "jsr:@effectionx/bdd@0.2.2", - "@effectionx/process": "npm:@effectionx/process@0.5.0", - "@effectionx/stream-helpers": "npm:@effectionx/stream-helpers@0.3.0", - "@effectionx/signals": "npm:@effectionx/signals@0.3.0" + "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0", + "@effectionx/process": "npm:@effectionx/process@0.6.0", + "@effectionx/stream-helpers": "npm:@effectionx/stream-helpers@0.4.0", + "@effectionx/signals": "npm:@effectionx/signals@0.4.0" }, "lint": { "rules": { diff --git a/websocket/deno.json b/websocket/deno.json index eaeb307f..b59fd61f 100644 --- a/websocket/deno.json +++ b/websocket/deno.json @@ -6,7 +6,7 @@ "imports": { "effection": "npm:effection@^3", "@std/expect": "jsr:@std/expect@^1", - "@effectionx/bdd": "jsr:@effectionx/bdd@0.2.2", + "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0", "ws": "npm:ws@^8" } } diff --git a/worker/deno.json b/worker/deno.json index f12fa6f9..a8621732 100644 --- a/worker/deno.json +++ b/worker/deno.json @@ -7,10 +7,10 @@ "effection": "npm:effection@^3", "@std/assert": "jsr:@std/assert@^1", "@std/expect": "jsr:@std/expect@^1", - "@effectionx/timebox": "npm:@effectionx/timebox@^0.1.0", + "@effectionx/timebox": "npm:@effectionx/timebox@^0.3.0", "@std/path": "jsr:@std/path@^1", "@std/fs": "jsr:@std/fs@^1", - "@effectionx/bdd": "jsr:@effectionx/bdd@0.2.2", - "@effectionx/signals": "npm:@effectionx/signals@0.3.0" + "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0", + "@effectionx/signals": "npm:@effectionx/signals@0.4.0" } } From 0bdeb12d3e32f38dd024140c6a4c39f62e6636d4 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sun, 7 Dec 2025 20:12:45 -0500 Subject: [PATCH 43/45] Update dependencies to use workspace references for @effectionx/bdd, @effectionx/test-adapter, and related packages across multiple modules. --- bdd/deno.json | 2 +- context-api/deno.json | 2 +- fx/deno.json | 2 +- jsonl-store/deno.json | 2 +- process/deno.json | 6 +++--- raf/deno.json | 2 +- signals/deno.json | 2 +- stream-helpers/deno.json | 6 +++--- task-buffer/deno.json | 2 +- timebox/deno.json | 2 +- watch/deno.json | 8 ++++---- websocket/deno.json | 2 +- worker/deno.json | 6 +++--- 13 files changed, 22 insertions(+), 22 deletions(-) diff --git a/bdd/deno.json b/bdd/deno.json index 99aa6f02..b08e1469 100644 --- a/bdd/deno.json +++ b/bdd/deno.json @@ -5,7 +5,7 @@ "license": "MIT", "imports": { "effection": "npm:effection@^3", - "@effectionx/test-adapter": "jsr:@effectionx/test-adapter@^0.6.0", + "@effectionx/test-adapter": "workspace:^", "@std/testing": "jsr:@std/testing@^1" } } diff --git a/context-api/deno.json b/context-api/deno.json index 9121f973..e1740e5c 100644 --- a/context-api/deno.json +++ b/context-api/deno.json @@ -6,6 +6,6 @@ "imports": { "effection": "npm:effection@^3", "@std/expect": "jsr:@std/expect@^1", - "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0" + "@effectionx/bdd": "workspace:^" } } diff --git a/fx/deno.json b/fx/deno.json index dd3da02a..27709d2f 100644 --- a/fx/deno.json +++ b/fx/deno.json @@ -6,6 +6,6 @@ "imports": { "effection": "npm:effection@^3", "@std/expect": "jsr:@std/expect@^1", - "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0" + "@effectionx/bdd": "workspace:^" } } diff --git a/jsonl-store/deno.json b/jsonl-store/deno.json index 88bde8a6..ddaa66cd 100644 --- a/jsonl-store/deno.json +++ b/jsonl-store/deno.json @@ -9,7 +9,7 @@ "@std/streams": "jsr:@std/streams@^1", "@std/fs": "jsr:@std/fs@^1", "@std/path": "jsr:@std/path@^1", - "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0", + "@effectionx/bdd": "workspace:^", "@std/expect": "jsr:@std/expect@^1" } } diff --git a/process/deno.json b/process/deno.json index 8e809548..7cf31a56 100644 --- a/process/deno.json +++ b/process/deno.json @@ -10,8 +10,8 @@ "effection": "npm:effection@^3", "shellwords": "npm:shellwords@^1.1.1", "@std/expect": "jsr:@std/expect@1.0.17", - "@effectionx/stream-helpers": "npm:@effectionx/stream-helpers@0.4.0", - "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0", - "@effectionx/signals": "npm:@effectionx/signals@0.4.0" + "@effectionx/stream-helpers": "workspace:^", + "@effectionx/bdd": "workspace:^", + "@effectionx/signals": "workspace:^" } } diff --git a/raf/deno.json b/raf/deno.json index 35353bc6..e0e64546 100644 --- a/raf/deno.json +++ b/raf/deno.json @@ -7,6 +7,6 @@ "effection": "npm:effection@^3", "@essentials/raf": "npm:@essentials/raf@^1.2.0", "@std/expect": "jsr:@std/expect@^1", - "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0" + "@effectionx/bdd": "workspace:^" } } diff --git a/signals/deno.json b/signals/deno.json index 17a402ea..5471e05c 100644 --- a/signals/deno.json +++ b/signals/deno.json @@ -5,7 +5,7 @@ "effection": "npm:effection@^3", "immutable": "npm:immutable@^5", "@std/expect": "jsr:@std/expect@^1", - "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0" + "@effectionx/bdd": "workspace:^" }, "license": "MIT", "exports": { diff --git a/stream-helpers/deno.json b/stream-helpers/deno.json index 5d174cc9..25e0419e 100644 --- a/stream-helpers/deno.json +++ b/stream-helpers/deno.json @@ -4,11 +4,11 @@ "imports": { "effection": "npm:effection@^3", "immutable": "npm:immutable@^5", - "@effectionx/signals": "npm:@effectionx/signals@0.4.0", + "@effectionx/signals": "workspace:^", "@std/expect": "jsr:@std/expect@^1", "@std/testing": "jsr:@std/testing@^1", - "@effectionx/timebox": "npm:@effectionx/timebox@0.3.0", - "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0", + "@effectionx/timebox": "workspace:^", + "@effectionx/bdd": "workspace:^", "remeda": "npm:remeda@^2" }, "license": "MIT", diff --git a/task-buffer/deno.json b/task-buffer/deno.json index 4537d9ca..029bfd57 100644 --- a/task-buffer/deno.json +++ b/task-buffer/deno.json @@ -6,7 +6,7 @@ "imports": { "effection": "npm:effection@^3", "@std/expect": "jsr:@std/expect@^1", - "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0", + "@effectionx/bdd": "workspace:^", "@std/testing": "jsr:@std/testing@^1" } } diff --git a/timebox/deno.json b/timebox/deno.json index 9706bfac..cb331ed6 100644 --- a/timebox/deno.json +++ b/timebox/deno.json @@ -6,6 +6,6 @@ "imports": { "effection": "npm:effection@^3", "@std/expect": "jsr:@std/expect@1", - "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0" + "@effectionx/bdd": "workspace:^" } } diff --git a/watch/deno.json b/watch/deno.json index c732fa55..0170d38f 100644 --- a/watch/deno.json +++ b/watch/deno.json @@ -17,10 +17,10 @@ "chokidar": "npm:chokidar@^4.0.3", "zod": "npm:zod@^3.20.2", "zod-opts": "npm:zod-opts@0.1.8", - "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0", - "@effectionx/process": "npm:@effectionx/process@0.6.0", - "@effectionx/stream-helpers": "npm:@effectionx/stream-helpers@0.4.0", - "@effectionx/signals": "npm:@effectionx/signals@0.4.0" + "@effectionx/bdd": "workspace:^", + "@effectionx/process": "workspace:^", + "@effectionx/stream-helpers": "workspace:^", + "@effectionx/signals": "workspace:^" }, "lint": { "rules": { diff --git a/websocket/deno.json b/websocket/deno.json index b59fd61f..31decc06 100644 --- a/websocket/deno.json +++ b/websocket/deno.json @@ -6,7 +6,7 @@ "imports": { "effection": "npm:effection@^3", "@std/expect": "jsr:@std/expect@^1", - "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0", + "@effectionx/bdd": "workspace:^", "ws": "npm:ws@^8" } } diff --git a/worker/deno.json b/worker/deno.json index a8621732..1a65dae9 100644 --- a/worker/deno.json +++ b/worker/deno.json @@ -7,10 +7,10 @@ "effection": "npm:effection@^3", "@std/assert": "jsr:@std/assert@^1", "@std/expect": "jsr:@std/expect@^1", - "@effectionx/timebox": "npm:@effectionx/timebox@^0.3.0", + "@effectionx/timebox": "workspace:^", "@std/path": "jsr:@std/path@^1", "@std/fs": "jsr:@std/fs@^1", - "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0", - "@effectionx/signals": "npm:@effectionx/signals@0.4.0" + "@effectionx/bdd": "workspace:^", + "@effectionx/signals": "workspace:^" } } From da8cd7309102e1574f481cfd2a7640db5977b456 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sun, 7 Dec 2025 20:25:59 -0500 Subject: [PATCH 44/45] Revert "Update dependencies to use workspace references for @effectionx/bdd, @effectionx/test-adapter, and related packages across multiple modules." This reverts commit 0bdeb12d3e32f38dd024140c6a4c39f62e6636d4. --- bdd/deno.json | 2 +- context-api/deno.json | 2 +- fx/deno.json | 2 +- jsonl-store/deno.json | 2 +- process/deno.json | 6 +++--- raf/deno.json | 2 +- signals/deno.json | 2 +- stream-helpers/deno.json | 6 +++--- task-buffer/deno.json | 2 +- timebox/deno.json | 2 +- watch/deno.json | 8 ++++---- websocket/deno.json | 2 +- worker/deno.json | 6 +++--- 13 files changed, 22 insertions(+), 22 deletions(-) diff --git a/bdd/deno.json b/bdd/deno.json index b08e1469..99aa6f02 100644 --- a/bdd/deno.json +++ b/bdd/deno.json @@ -5,7 +5,7 @@ "license": "MIT", "imports": { "effection": "npm:effection@^3", - "@effectionx/test-adapter": "workspace:^", + "@effectionx/test-adapter": "jsr:@effectionx/test-adapter@^0.6.0", "@std/testing": "jsr:@std/testing@^1" } } diff --git a/context-api/deno.json b/context-api/deno.json index e1740e5c..9121f973 100644 --- a/context-api/deno.json +++ b/context-api/deno.json @@ -6,6 +6,6 @@ "imports": { "effection": "npm:effection@^3", "@std/expect": "jsr:@std/expect@^1", - "@effectionx/bdd": "workspace:^" + "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0" } } diff --git a/fx/deno.json b/fx/deno.json index 27709d2f..dd3da02a 100644 --- a/fx/deno.json +++ b/fx/deno.json @@ -6,6 +6,6 @@ "imports": { "effection": "npm:effection@^3", "@std/expect": "jsr:@std/expect@^1", - "@effectionx/bdd": "workspace:^" + "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0" } } diff --git a/jsonl-store/deno.json b/jsonl-store/deno.json index ddaa66cd..88bde8a6 100644 --- a/jsonl-store/deno.json +++ b/jsonl-store/deno.json @@ -9,7 +9,7 @@ "@std/streams": "jsr:@std/streams@^1", "@std/fs": "jsr:@std/fs@^1", "@std/path": "jsr:@std/path@^1", - "@effectionx/bdd": "workspace:^", + "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0", "@std/expect": "jsr:@std/expect@^1" } } diff --git a/process/deno.json b/process/deno.json index 7cf31a56..8e809548 100644 --- a/process/deno.json +++ b/process/deno.json @@ -10,8 +10,8 @@ "effection": "npm:effection@^3", "shellwords": "npm:shellwords@^1.1.1", "@std/expect": "jsr:@std/expect@1.0.17", - "@effectionx/stream-helpers": "workspace:^", - "@effectionx/bdd": "workspace:^", - "@effectionx/signals": "workspace:^" + "@effectionx/stream-helpers": "npm:@effectionx/stream-helpers@0.4.0", + "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0", + "@effectionx/signals": "npm:@effectionx/signals@0.4.0" } } diff --git a/raf/deno.json b/raf/deno.json index e0e64546..35353bc6 100644 --- a/raf/deno.json +++ b/raf/deno.json @@ -7,6 +7,6 @@ "effection": "npm:effection@^3", "@essentials/raf": "npm:@essentials/raf@^1.2.0", "@std/expect": "jsr:@std/expect@^1", - "@effectionx/bdd": "workspace:^" + "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0" } } diff --git a/signals/deno.json b/signals/deno.json index 5471e05c..17a402ea 100644 --- a/signals/deno.json +++ b/signals/deno.json @@ -5,7 +5,7 @@ "effection": "npm:effection@^3", "immutable": "npm:immutable@^5", "@std/expect": "jsr:@std/expect@^1", - "@effectionx/bdd": "workspace:^" + "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0" }, "license": "MIT", "exports": { diff --git a/stream-helpers/deno.json b/stream-helpers/deno.json index 25e0419e..5d174cc9 100644 --- a/stream-helpers/deno.json +++ b/stream-helpers/deno.json @@ -4,11 +4,11 @@ "imports": { "effection": "npm:effection@^3", "immutable": "npm:immutable@^5", - "@effectionx/signals": "workspace:^", + "@effectionx/signals": "npm:@effectionx/signals@0.4.0", "@std/expect": "jsr:@std/expect@^1", "@std/testing": "jsr:@std/testing@^1", - "@effectionx/timebox": "workspace:^", - "@effectionx/bdd": "workspace:^", + "@effectionx/timebox": "npm:@effectionx/timebox@0.3.0", + "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0", "remeda": "npm:remeda@^2" }, "license": "MIT", diff --git a/task-buffer/deno.json b/task-buffer/deno.json index 029bfd57..4537d9ca 100644 --- a/task-buffer/deno.json +++ b/task-buffer/deno.json @@ -6,7 +6,7 @@ "imports": { "effection": "npm:effection@^3", "@std/expect": "jsr:@std/expect@^1", - "@effectionx/bdd": "workspace:^", + "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0", "@std/testing": "jsr:@std/testing@^1" } } diff --git a/timebox/deno.json b/timebox/deno.json index cb331ed6..9706bfac 100644 --- a/timebox/deno.json +++ b/timebox/deno.json @@ -6,6 +6,6 @@ "imports": { "effection": "npm:effection@^3", "@std/expect": "jsr:@std/expect@1", - "@effectionx/bdd": "workspace:^" + "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0" } } diff --git a/watch/deno.json b/watch/deno.json index 0170d38f..c732fa55 100644 --- a/watch/deno.json +++ b/watch/deno.json @@ -17,10 +17,10 @@ "chokidar": "npm:chokidar@^4.0.3", "zod": "npm:zod@^3.20.2", "zod-opts": "npm:zod-opts@0.1.8", - "@effectionx/bdd": "workspace:^", - "@effectionx/process": "workspace:^", - "@effectionx/stream-helpers": "workspace:^", - "@effectionx/signals": "workspace:^" + "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0", + "@effectionx/process": "npm:@effectionx/process@0.6.0", + "@effectionx/stream-helpers": "npm:@effectionx/stream-helpers@0.4.0", + "@effectionx/signals": "npm:@effectionx/signals@0.4.0" }, "lint": { "rules": { diff --git a/websocket/deno.json b/websocket/deno.json index 31decc06..b59fd61f 100644 --- a/websocket/deno.json +++ b/websocket/deno.json @@ -6,7 +6,7 @@ "imports": { "effection": "npm:effection@^3", "@std/expect": "jsr:@std/expect@^1", - "@effectionx/bdd": "workspace:^", + "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0", "ws": "npm:ws@^8" } } diff --git a/worker/deno.json b/worker/deno.json index 1a65dae9..a8621732 100644 --- a/worker/deno.json +++ b/worker/deno.json @@ -7,10 +7,10 @@ "effection": "npm:effection@^3", "@std/assert": "jsr:@std/assert@^1", "@std/expect": "jsr:@std/expect@^1", - "@effectionx/timebox": "workspace:^", + "@effectionx/timebox": "npm:@effectionx/timebox@^0.3.0", "@std/path": "jsr:@std/path@^1", "@std/fs": "jsr:@std/fs@^1", - "@effectionx/bdd": "workspace:^", - "@effectionx/signals": "workspace:^" + "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0", + "@effectionx/signals": "npm:@effectionx/signals@0.4.0" } } From 53bd33c4f1ede386758f0183247979722fefdb85 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sun, 7 Dec 2025 20:42:21 -0500 Subject: [PATCH 45/45] Enhance generate-importmap task to support version argument parsing and improve usage documentation. Update dependencies for @effectionx/bdd and related packages to version 0.2.2 across multiple modules, and adjust GitHub Actions workflows to use the new import map generation. --- .github/workflows/verify-posix.yaml | 5 +- .github/workflows/verify-windows.yaml | 9 ++- .gitignore | 1 + README.md | 16 ++-- bdd/deno.json | 2 +- context-api/deno.json | 2 +- fx/deno.json | 2 +- jsonl-store/deno.json | 2 +- process/deno.json | 6 +- raf/deno.json | 2 +- signals/deno.json | 2 +- stream-helpers/deno.json | 6 +- task-buffer/deno.json | 2 +- tasks/generate-importmap.ts | 106 ++++++++++++++------------ timebox/deno.json | 2 +- watch/deno.json | 8 +- websocket/deno.json | 2 +- worker/deno.json | 6 +- 18 files changed, 96 insertions(+), 85 deletions(-) diff --git a/.github/workflows/verify-posix.yaml b/.github/workflows/verify-posix.yaml index 918e874e..a252d23f 100644 --- a/.github/workflows/verify-posix.yaml +++ b/.github/workflows/verify-posix.yaml @@ -32,10 +32,11 @@ jobs: - run: deno fmt --check - run: deno lint - - run: deno task generate-importmap + - run: deno task generate-importmap "^3" v3.importmap.json + - run: deno task generate-importmap v4 v4.importmap.json - name: V3 Tests - run: deno test --allow-net --allow-read --allow-write --allow-env --allow-run --allow-ffi + run: deno test --import-map v3.importmap.json --allow-net --allow-read --allow-write --allow-env --allow-run --allow-ffi - name: V4 Tests run: deno test --import-map v4.importmap.json --allow-net --allow-read --allow-write --allow-env --allow-run --allow-ffi --trace-leaks diff --git a/.github/workflows/verify-windows.yaml b/.github/workflows/verify-windows.yaml index 8647fb4f..99c4543c 100644 --- a/.github/workflows/verify-windows.yaml +++ b/.github/workflows/verify-windows.yaml @@ -39,13 +39,14 @@ jobs: - run: deno lint - - run: deno task generate-importmap + - run: deno task generate-importmap "^3" v3.importmap.json + - run: deno task generate-importmap v4 v4.importmap.json - name: "Test V3 (${{matrix.shell}})" if: matrix.shell == 'powershell' || matrix.shell == 'pwsh' run: | $env:SHELL = "${{matrix.shell}}" - deno test --allow-net --allow-read --allow-write --allow-env --allow-run --allow-ffi --trace-leaks + deno test --import-map v3.importmap.json --allow-net --allow-read --allow-write --allow-env --allow-run --allow-ffi --trace-leaks - name: "Test V4 (${{matrix.shell}})" if: matrix.shell == 'powershell' || matrix.shell == 'pwsh' @@ -57,7 +58,7 @@ jobs: if: matrix.shell == 'cmd' run: | set SHELL=${{matrix.shell}} - deno test --allow-net --allow-read --allow-write --allow-env --allow-run --allow-ffi --trace-leaks + deno test --import-map v3.importmap.json --allow-net --allow-read --allow-write --allow-env --allow-run --allow-ffi --trace-leaks - name: "Test V4 (${{matrix.shell}})" if: matrix.shell == 'cmd' @@ -69,7 +70,7 @@ jobs: if: matrix.shell == 'bash' run: | export SHELL=${{matrix.shell}} - deno test --allow-net --allow-read --allow-write --allow-env --allow-run --allow-ffi --trace-leaks + deno test --import-map v3.importmap.json --allow-net --allow-read --allow-write --allow-env --allow-run --allow-ffi --trace-leaks - name: "Test V4 (${{matrix.shell}})" if: matrix.shell == 'bash' diff --git a/.gitignore b/.gitignore index 85db91de..642b0639 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /deno.lock /**/build/ +/v3.importmap.json /v4.importmap.json .vscode \ No newline at end of file diff --git a/README.md b/README.md index e4518435..607544bf 100644 --- a/README.md +++ b/README.md @@ -20,21 +20,23 @@ more than a few times are welcome. All packages are tested against both effection v3 and v4 to ensure compatibility. -### Running tests with v3 (default) +### Generate import maps ```bash -deno test -A -``` +# Generate v3 import map +deno task generate-importmap "^3" v3.importmap.json -### Running tests with v4 +# Generate v4 import map (fetches latest v4 from npm) +deno task generate-importmap v4 v4.importmap.json +``` -First, generate the v4 import map (fetches the latest v4 version from npm): +### Running tests with v3 ```bash -deno task generate-importmap +deno test --import-map v3.importmap.json -A ``` -Then run tests with the import map: +### Running tests with v4 ```bash deno test --import-map v4.importmap.json -A diff --git a/bdd/deno.json b/bdd/deno.json index 99aa6f02..f19cdaee 100644 --- a/bdd/deno.json +++ b/bdd/deno.json @@ -5,7 +5,7 @@ "license": "MIT", "imports": { "effection": "npm:effection@^3", - "@effectionx/test-adapter": "jsr:@effectionx/test-adapter@^0.6.0", + "@effectionx/test-adapter": "jsr:@effectionx/test-adapter@^0.5.1", "@std/testing": "jsr:@std/testing@^1" } } diff --git a/context-api/deno.json b/context-api/deno.json index 9121f973..a0cf6546 100644 --- a/context-api/deno.json +++ b/context-api/deno.json @@ -6,6 +6,6 @@ "imports": { "effection": "npm:effection@^3", "@std/expect": "jsr:@std/expect@^1", - "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0" + "@effectionx/bdd": "jsr:@effectionx/bdd@0.2.2" } } diff --git a/fx/deno.json b/fx/deno.json index dd3da02a..e970f160 100644 --- a/fx/deno.json +++ b/fx/deno.json @@ -6,6 +6,6 @@ "imports": { "effection": "npm:effection@^3", "@std/expect": "jsr:@std/expect@^1", - "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0" + "@effectionx/bdd": "jsr:@effectionx/bdd@0.2.2" } } diff --git a/jsonl-store/deno.json b/jsonl-store/deno.json index 88bde8a6..90dc4ef4 100644 --- a/jsonl-store/deno.json +++ b/jsonl-store/deno.json @@ -9,7 +9,7 @@ "@std/streams": "jsr:@std/streams@^1", "@std/fs": "jsr:@std/fs@^1", "@std/path": "jsr:@std/path@^1", - "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0", + "@effectionx/bdd": "jsr:@effectionx/bdd@0.2.2", "@std/expect": "jsr:@std/expect@^1" } } diff --git a/process/deno.json b/process/deno.json index 8e809548..efa68a5f 100644 --- a/process/deno.json +++ b/process/deno.json @@ -10,8 +10,8 @@ "effection": "npm:effection@^3", "shellwords": "npm:shellwords@^1.1.1", "@std/expect": "jsr:@std/expect@1.0.17", - "@effectionx/stream-helpers": "npm:@effectionx/stream-helpers@0.4.0", - "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0", - "@effectionx/signals": "npm:@effectionx/signals@0.4.0" + "@effectionx/stream-helpers": "npm:@effectionx/stream-helpers@0.3.0", + "@effectionx/bdd": "jsr:@effectionx/bdd@0.2.2", + "@effectionx/signals": "npm:@effectionx/signals@0.3.0" } } diff --git a/raf/deno.json b/raf/deno.json index 35353bc6..d471846d 100644 --- a/raf/deno.json +++ b/raf/deno.json @@ -7,6 +7,6 @@ "effection": "npm:effection@^3", "@essentials/raf": "npm:@essentials/raf@^1.2.0", "@std/expect": "jsr:@std/expect@^1", - "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0" + "@effectionx/bdd": "jsr:@effectionx/bdd@0.2.2" } } diff --git a/signals/deno.json b/signals/deno.json index 17a402ea..9b170582 100644 --- a/signals/deno.json +++ b/signals/deno.json @@ -5,7 +5,7 @@ "effection": "npm:effection@^3", "immutable": "npm:immutable@^5", "@std/expect": "jsr:@std/expect@^1", - "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0" + "@effectionx/bdd": "jsr:@effectionx/bdd@0.2.2" }, "license": "MIT", "exports": { diff --git a/stream-helpers/deno.json b/stream-helpers/deno.json index 5d174cc9..216feced 100644 --- a/stream-helpers/deno.json +++ b/stream-helpers/deno.json @@ -4,11 +4,11 @@ "imports": { "effection": "npm:effection@^3", "immutable": "npm:immutable@^5", - "@effectionx/signals": "npm:@effectionx/signals@0.4.0", + "@effectionx/signals": "npm:@effectionx/signals@0.3.0", "@std/expect": "jsr:@std/expect@^1", "@std/testing": "jsr:@std/testing@^1", - "@effectionx/timebox": "npm:@effectionx/timebox@0.3.0", - "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0", + "@effectionx/timebox": "npm:@effectionx/timebox@0.2.0", + "@effectionx/bdd": "jsr:@effectionx/bdd@0.2.2", "remeda": "npm:remeda@^2" }, "license": "MIT", diff --git a/task-buffer/deno.json b/task-buffer/deno.json index 4537d9ca..08d0538b 100644 --- a/task-buffer/deno.json +++ b/task-buffer/deno.json @@ -6,7 +6,7 @@ "imports": { "effection": "npm:effection@^3", "@std/expect": "jsr:@std/expect@^1", - "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0", + "@effectionx/bdd": "jsr:@effectionx/bdd@0.2.2", "@std/testing": "jsr:@std/testing@^1" } } diff --git a/tasks/generate-importmap.ts b/tasks/generate-importmap.ts index 20b5f3b6..421e0077 100644 --- a/tasks/generate-importmap.ts +++ b/tasks/generate-importmap.ts @@ -13,10 +13,6 @@ interface NpmRegistryResponse { versions: Record; } -/** - * Fetches the latest effection v4 version from the npm registry. - * Returns the highest v4.x.x version (including prereleases). - */ async function getLatestEffectionV4(): Promise { const response = await fetch("https://registry.npmjs.org/effection"); if (!response.ok) { @@ -25,47 +21,70 @@ async function getLatestEffectionV4(): Promise { const data: NpmRegistryResponse = await response.json(); const versions = Object.keys(data.versions); - - // Filter to v4.x.x versions only const v4Versions = versions.filter((v) => v.startsWith("4.")); if (v4Versions.length === 0) { throw new Error("No v4 versions found for effection"); } - // Sort versions using Deno's semver comparison - // Parse and sort: stable versions first by semver, then prereleases v4Versions.sort((a, b) => { - const parseVersion = (v: string) => { - const [main, prerelease] = v.split("-"); + const parse = (v: string) => { + const [main, pre] = v.split("-"); const [major, minor, patch] = main.split(".").map(Number); - return { major, minor, patch, prerelease: prerelease || "" }; + return { major, minor, patch, pre: pre || "" }; }; - - const va = parseVersion(a); - const vb = parseVersion(b); - - // Compare major.minor.patch first + const va = parse(a); + const vb = parse(b); if (va.major !== vb.major) return va.major - vb.major; if (va.minor !== vb.minor) return va.minor - vb.minor; if (va.patch !== vb.patch) return va.patch - vb.patch; - - // If one has prerelease and other doesn't, stable comes after prerelease - if (!va.prerelease && vb.prerelease) return 1; - if (va.prerelease && !vb.prerelease) return -1; - - // Both have prereleases, compare alphabetically - return va.prerelease.localeCompare(vb.prerelease); + if (!va.pre && vb.pre) return 1; + if (va.pre && !vb.pre) return -1; + return va.pre.localeCompare(vb.pre); }); return v4Versions[v4Versions.length - 1]; } +function printUsage() { + console.log(`Usage: generate-importmap.ts [output-file] + +Arguments: + effection-version Version specifier for effection package + Examples: "^3", "3.6.1", "4.0.0-beta.3" + Special: "v4" or "latest-v4" fetches latest v4 from npm + + output-file Output filename (default: importmap.json) + +Examples: + generate-importmap.ts "^3" + generate-importmap.ts "^3" v3.importmap.json + generate-importmap.ts v4 v4.importmap.json + generate-importmap.ts "4.0.0-beta.3" custom.importmap.json`); +} + +// Parse arguments +const [versionArg, outputArg] = Deno.args; + +if (!versionArg || versionArg === "--help" || versionArg === "-h") { + printUsage(); + Deno.exit(versionArg ? 0 : 1); +} + +// Resolve effection version +let effectionVersion: string; +if (versionArg === "v4" || versionArg === "latest-v4") { + effectionVersion = await getLatestEffectionV4(); + console.log(`Resolved latest v4: ${effectionVersion}`); +} else { + effectionVersion = versionArg; +} + const rootDir = fromFileUrl(new URL("..", import.meta.url)); const imports = new Map(); const workspacePackages = new Map(); -// First pass: collect all workspace package names and their exports +// Collect workspace packages for await ( const file of expandGlob("**/deno.json", { root: rootDir, @@ -76,8 +95,7 @@ for await ( const config: DenoConfig = JSON.parse(content); if (config.name?.startsWith("@effectionx/")) { - // Get the default export path - let exportPath = "./mod.ts"; // fallback + let exportPath = "./mod.ts"; if (typeof config.exports === "string") { exportPath = config.exports; } else if (config.exports && typeof config.exports === "object") { @@ -85,7 +103,6 @@ for await ( "./mod.ts"; } - // Get package directory relative to root const packageDir = file.path.replace(rootDir, "").replace( /[\/\\]deno\.json$/, "", @@ -97,7 +114,7 @@ for await ( } } -// Second pass: collect external dependencies and convert workspace packages to local paths +// Collect dependencies for await ( const file of expandGlob("**/deno.json", { root: rootDir, @@ -109,18 +126,13 @@ for await ( if (config.imports) { for (const [key, value] of Object.entries(config.imports)) { - // Convert workspace packages to local paths using their actual export if (workspacePackages.has(key)) { imports.set(key, workspacePackages.get(key)!); continue; } - - // Skip other internal @effectionx packages if (key.startsWith("@effectionx/")) { continue; } - - // Only include external dependencies (jsr: or npm:) if (value.startsWith("jsr:") || value.startsWith("npm:")) { imports.set(key, value); } @@ -128,34 +140,28 @@ for await ( } } -// Add effection v4 - fetch latest version from npm -const effectionV4Version = await getLatestEffectionV4(); -imports.set("effection", `npm:effection@${effectionV4Version}`); -console.log(`Using effection v4: ${effectionV4Version}`); - -// Add @std/testing sub-packages if @std/testing is present +// Add @std/testing sub-packages if (imports.has("@std/testing")) { const testingSpec = imports.get("@std/testing")!; - // Extract version from "jsr:@std/testing@^1" -> "^1" const version = testingSpec.split("@std/testing@")[1]; imports.set("@std/testing/bdd", `jsr:@std/testing@${version}/bdd`); imports.set("@std/testing/mock", `jsr:@std/testing@${version}/mock`); imports.set("@std/testing/time", `jsr:@std/testing@${version}/time`); } -// Sort by key +// Set effection version +imports.set("effection", `npm:effection@${effectionVersion}`); + +// Sort and output const sortedImports = Object.fromEntries( Array.from(imports.entries()).sort(([a], [b]) => a.localeCompare(b)), ); -const output = { - imports: sortedImports, -}; +const output = { imports: sortedImports }; +const outputPath = join(rootDir, outputArg || "importmap.json"); -const outputPath = join(rootDir, "v4.importmap.json"); -await Deno.writeTextFile( - outputPath, - JSON.stringify(output, null, 2) + "\n", -); +await Deno.writeTextFile(outputPath, JSON.stringify(output, null, 2) + "\n"); -console.log(`Generated ${outputPath} with ${imports.size} dependencies`); +console.log(`Generated ${outputPath}`); +console.log(` effection: npm:effection@${effectionVersion}`); +console.log(` ${imports.size} total dependencies`); diff --git a/timebox/deno.json b/timebox/deno.json index 9706bfac..e5638906 100644 --- a/timebox/deno.json +++ b/timebox/deno.json @@ -6,6 +6,6 @@ "imports": { "effection": "npm:effection@^3", "@std/expect": "jsr:@std/expect@1", - "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0" + "@effectionx/bdd": "jsr:@effectionx/bdd@0.2.2" } } diff --git a/watch/deno.json b/watch/deno.json index c732fa55..a4c535bd 100644 --- a/watch/deno.json +++ b/watch/deno.json @@ -17,10 +17,10 @@ "chokidar": "npm:chokidar@^4.0.3", "zod": "npm:zod@^3.20.2", "zod-opts": "npm:zod-opts@0.1.8", - "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0", - "@effectionx/process": "npm:@effectionx/process@0.6.0", - "@effectionx/stream-helpers": "npm:@effectionx/stream-helpers@0.4.0", - "@effectionx/signals": "npm:@effectionx/signals@0.4.0" + "@effectionx/bdd": "jsr:@effectionx/bdd@0.2.2", + "@effectionx/process": "npm:@effectionx/process@0.5.0", + "@effectionx/stream-helpers": "npm:@effectionx/stream-helpers@0.3.0", + "@effectionx/signals": "npm:@effectionx/signals@0.3.0" }, "lint": { "rules": { diff --git a/websocket/deno.json b/websocket/deno.json index b59fd61f..eaeb307f 100644 --- a/websocket/deno.json +++ b/websocket/deno.json @@ -6,7 +6,7 @@ "imports": { "effection": "npm:effection@^3", "@std/expect": "jsr:@std/expect@^1", - "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0", + "@effectionx/bdd": "jsr:@effectionx/bdd@0.2.2", "ws": "npm:ws@^8" } } diff --git a/worker/deno.json b/worker/deno.json index a8621732..646e9578 100644 --- a/worker/deno.json +++ b/worker/deno.json @@ -7,10 +7,10 @@ "effection": "npm:effection@^3", "@std/assert": "jsr:@std/assert@^1", "@std/expect": "jsr:@std/expect@^1", - "@effectionx/timebox": "npm:@effectionx/timebox@^0.3.0", + "@effectionx/timebox": "npm:@effectionx/timebox@^0.2.0", "@std/path": "jsr:@std/path@^1", "@std/fs": "jsr:@std/fs@^1", - "@effectionx/bdd": "jsr:@effectionx/bdd@0.3.0", - "@effectionx/signals": "npm:@effectionx/signals@0.4.0" + "@effectionx/bdd": "jsr:@effectionx/bdd@0.2.2", + "@effectionx/signals": "npm:@effectionx/signals@0.3.0" } }