Skip to content

feat(k6): add @effectionx/k6 package#156

Open
taras wants to merge 30 commits intomainfrom
feat/effectionx-k6-preview
Open

feat(k6): add @effectionx/k6 package#156
taras wants to merge 30 commits intomainfrom
feat/effectionx-k6-preview

Conversation

@taras
Copy link
Member

@taras taras commented Feb 15, 2026

Motivation

Provide a preview @effectionx/k6 package that brings Effection structured concurrency semantics to K6 scripts, and verify runtime compatibility with a concrete conformance and BDD test surface.

What This PR Adds

  • New workspace package: @effectionx/k6
  • Core APIs: main(), group(), withGroup(), useGroups(), useTags(), HTTP wrappers, WebSocket resource
  • Runtime conformance suite under k6/conformance
  • Demo scripts under k6/demos
  • New BDD testing module: @effectionx/k6/testing
    • describe, it, beforeAll, beforeEach, expect, runTests, testMain
    • Test results reported via K6 check()
  • Build updates to emit dist/testing.js and bundle k6/tests/*.ts
  • README updates documenting testing usage and current runtime limitations

Test Scenarios Added

1) Group Context (k6/tests/group-context.test.ts)

  • preserves single group context
  • builds nested group paths in order
  • restores parent group after withGroup
  • keeps context through async boundaries (sleep, HTTP)
  • keeps deep nesting intact
  • verifies tags integration for group attribution

2) Cleanup Semantics (k6/tests/cleanup.test.ts)

  • resource acquisition/provision lifecycle
  • cleanup on thrown errors
  • LIFO cleanup ordering for multiple resources
  • nested scope cleanup ordering (inner before outer)
  • spawned task cancellation on parent scope exit
  • spawned child resource cleanup on parent exit
  • websocket auto-close on scope exit

3) Error Propagation (k6/tests/error-propagation.test.ts)

  • synchronous throw/catch behavior in grouped operations
  • async throw/catch after HTTP and after timers
  • error propagation across multiple async boundaries
  • recovery patterns (try/catch/finally, nested recovery)
  • known-limitation scenarios currently skipped:
    • child task throw surfaces when parent awaits
    • parent catches child task thrown error

4) WebSocket Behavior (k6/tests/websocket.test.ts)

  • connection + send/receive flow
  • handling echo server greeting before payload assertions
  • scope-owned websocket cleanup

Runtime Findings / Sobek Work

  • Conformance confirms stock K6 runtime limitations around generator return/finally behavior; custom Sobek/K6 builds improve baseline support.
  • Additional Sobek hardening and regression tests were developed in parallel to address panic paths and callback/unwind safety.
  • Remaining known issue: when child-task error scenarios are force-unskipped, execution can terminate early in current runtime path; scenarios remain skipped and documented.

Validation Performed

  • pnpm -w run fmt
  • pnpm -w run lint
  • node k6/build.js
  • docker compose run --rm k6-conformance tests/group-context.test.js
  • docker compose run --rm k6-conformance tests/cleanup.test.js
  • docker compose run --rm k6-conformance tests/error-propagation.test.js
  • docker compose run --rm k6-conformance tests/websocket.test.js

Notes

  • Added optional test-run debug markers in k6/testing/mod.ts gated by:
    • EFFECTIONX_K6_TEST_DEBUG=1
    • logs per-test START/END to help pinpoint runtime abort points.

@coderabbitai
Copy link

coderabbitai bot commented Feb 15, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR introduces the @effectionx/k6 package, integrating Effection structured concurrency into the K6 load testing framework. It includes a multi-stage Docker build creating a custom K6 binary with patched Sobek, comprehensive runtime conformance tests, demo applications showcasing features, and library helpers for HTTP, WebSocket, tags/groups, and VU iteration management.

Changes

Cohort / File(s) Summary
Build & Infrastructure
k6/Dockerfile, k6/docker-compose.yml, k6/.dockerignore, k6/build.js, .gitignore, k6/tsconfig.json, k6/package.json
Docker multi-stage build for custom K6 binary with Sobek patches, compose services for conformance/demo/dev workflows, esbuild bundler configuration, and package metadata.
Documentation
k6/README.md
Comprehensive package guide covering motivation, installation, conformance testing, usage examples, API references, demos, development workflow, and Sobek runtime status.
Core Library Implementation
k6/lib/run.ts, k6/lib/tags.ts, k6/lib/http.ts, k6/lib/websocket.ts, k6/lib/mod.ts
VU iteration wrapper, tag/group context management with hierarchical grouping, HTTP verb helpers with context-aware tagging, Effection-managed WebSocket resource with message streaming and event handling, and consolidated public API surface.
Conformance Test Suite
k6/conformance/types.ts, k6/conformance/01-symbols.ts, k6/conformance/02-generators.ts, k6/conformance/03-yield-delegation.ts, k6/conformance/04-yield-throw.ts, k6/conformance/05-yield-return.ts, k6/conformance/06-promises.ts, k6/conformance/07-timers.ts, k6/conformance/08-abort-controller.ts, k6/conformance/mod.ts, k6/conformance/k6-runner.ts
Type definitions for conformance results, eight test modules validating symbols, generators, yield delegation, throw/return semantics, promises, timers, and abort controller support; test orchestrator and K6 runner with setup/teardown lifecycle.
Demo Applications
k6/demos/01-group-context.ts, k6/demos/02-websocket.ts, k6/demos/03-error-propagation.ts, k6/demos/04-cleanup.ts
Demonstrations of group context preservation across async boundaries, structured WebSocket handling with message collection, error propagation with main wrapper, and resource lifecycle management with guaranteed cleanup.
Package Integration
k6/mod.ts, pnpm-workspace.yaml, tsconfig.json
Root module re-exporting library and conformance APIs, monorepo workspace expansion, and TypeScript project reference addition.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Merge Conflict Detection ⚠️ Warning ❌ Merge conflicts detected (5 files):

⚔️ .coderabbit.yaml (content)
⚔️ .gitignore (content)
⚔️ pnpm-lock.yaml (content)
⚔️ pnpm-workspace.yaml (content)
⚔️ tsconfig.json (content)

These conflicts must be resolved before merging into main.
Resolve conflicts locally and push changes to this branch.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding a new @effectionx/k6 package as a preview feature, which is the primary purpose of this PR.
Docstring Coverage ✅ Passed Docstring coverage is 95.83% which is sufficient. The required threshold is 80.00%.
Description check ✅ Passed The PR description comprehensively covers motivation, changes, test scenarios, validation performed, and known limitations.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/effectionx-k6-preview

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 18

🤖 Fix all issues with AI agents
In `@k6/conformance/03-yield-delegation.ts`:
- Around line 42-48: Formatter mismatch in the if-check block comparing `values`
(the yield* delegation assertion) — run the project formatter to align styling
(or manually apply the same formatting) so the region containing the `if
(values.length !== 3 || values[0] !== 1 || values[1] !== 2 || values[2] !== 3) {
... }` block matches the repository style; run `pnpm biome format --write
k6/conformance/03-yield-delegation.ts` (or run the repo-wide formatter) and
commit the resulting changes.

In `@k6/conformance/04-yield-throw.ts`:
- Around line 131-137: Run the Biome formatter on the changed file to fix CI
formatting errors: execute the formatter command (e.g., pnpm biome format
k6/conformance/04-yield-throw.ts) and commit the resulting changes; specifically
ensure formatting is applied around the errorPath check and the object returned
in the failing branch (the block containing the string "Error propagation order
incorrect" and the details using JSON.stringify) so the file matches CI's
expected style.

In `@k6/conformance/05-yield-return.ts`:
- Around line 172-176: Run Biome to auto-format the file containing the return
block that constructs the result object (the code that returns { pass: true,
message: "...", details: checks.join("; "), }). Specifically, run the project
formatter (for example: pnpm biome format k6/conformance/05-yield-return.ts) or
apply your editor’s Biome integration so the object and surrounding code match
CI formatting rules, then re-run CI.

In `@k6/conformance/07-timers.ts`:
- Around line 131-136: The if-condition block that checks sequence
([sequence.length !== 3 || sequence[0] !== 1 || sequence[1] !== 2 || sequence[2]
!== 3]) is causing a Biome formatting failure; reflow or reformat this
conditional to satisfy the formatter (for example break the long condition
across lines or run `biome format`), ensuring the returned object
(pass/message/details) stays correctly aligned and maintains the same logic;
locate the if block that references the variable sequence and adjust
spacing/line breaks or run the formatter to clear the CI formatting error.

In `@k6/conformance/08-abort-controller.ts`:
- Around line 1-2: The file k6/conformance/08-abort-controller.ts has a Biome
formatting mismatch (starting at the import line); run the Biome formatter to
fix styling so CI passes. Specifically, format the file (e.g., run biome format
on k6/conformance/08-abort-controller.ts or your repo root) and commit the
updated file containing the properly formatted import line `import type {
ConformanceResult } from "./types.ts";` so the Biome diff is resolved.

In `@k6/conformance/k6-runner.ts`:
- Around line 40-42: The file k6/conformance/k6-runner.ts has Biome formatting
mismatches around the default exported async function and another block around
lines 64-70; fix by running the project formatter (e.g., pnpm biome format
k6/conformance/k6-runner.ts) or applying Biome rules to reformat the default
export function and surrounding code (the anonymous exported async function that
destructures syncResults and the subsequent block referenced in the diff) so CI
no longer reports style differences.

In `@k6/conformance/mod.ts`:
- Around line 10-18: The CI failure is due to Biome formatting differences in
k6/conformance/mod.ts; run the Biome formatter (biome format) on that file (or
the repo) and commit the resulting changes so imports and spacing match the
project's formatter settings; ensure the file containing the imports (mod.ts) is
updated and staged before pushing.

In `@k6/demos/04-cleanup.ts`:
- Around line 100-112: Update the hardcoded WebSocket URL in the
"websocket-cleanup" demo to support an override from the environment: read
__ENV.WS_URL (fallback to "wss://echo.websocket.org") where
useWebSocket("wss://echo.websocket.org") is called, so the call to useWebSocket
uses the env value; keep the existing behavior when the env var is unset and
ensure references are to the group name "websocket-cleanup" and the useWebSocket
symbol so tests/demos pick up the new URL.

In `@k6/docker-compose.yml`:
- Around line 37-41: The docker-compose service k6-dev currently uses image
"alpine:latest" which can cause nondeterministic builds; update the image
reference in the k6-dev service (the "image:" field) to a specific, supported
Alpine tag (e.g., "alpine:3.18" or another chosen minor/patch pin) to ensure
reproducible environments across machines and CI.

In `@k6/Dockerfile`:
- Around line 38-49: The Dockerfile enables pnpm (RUN corepack enable &&
corepack prepare pnpm@latest --activate) but then runs npm install, which
ignores pnpm lockfile and workspace; replace the npm install step to run pnpm
install and ensure any workspace config (pnpm-workspace.yaml) and pnpm-lock.yaml
are copied (the COPY package.json pnpm-lock.yaml* ./ and COPY . . steps) so the
build uses pnpm, then keep the existing RUN pnpm run build:bundle; in short: use
pnpm install instead of npm install and ensure pnpm-workspace.yaml is included
in the image.
- Around line 54-69: The Dockerfile runtime stage currently uses an unpinned
base image and runs as root; update the FROM to a specific Alpine tag (e.g.,
alpine:3.18 or another approved tag) and add a non-root user and group (create
with addgroup/adduser or adduser -D), chown the copied artifacts
(/usr/local/bin/k6 and /tests) to that user, and switch to it with USER before
the WORKDIR/ENTRYPOINT; optionally add a HEALTHCHECK instruction to the image if
it will be long-lived. Ensure the RUN apk add --no-cache ca-certificates remains
but include creation of the user/group and chown steps to avoid root runtime and
keep ENTRYPOINT ["k6","run"] unchanged.

In `@k6/lib/group.ts`:
- Around line 64-71: The block defining the generator function group has
formatting differences flagged by CI; run the project's formatter (e.g.,
Biome/Prettier) on this file so the export function* group<T>(...), the parent
assignment using GroupContext.expect(), and the return using
GroupContext.with([...parent, name], op) match the repository style; reformat
k6/lib/group.ts and commit the formatted changes.

In `@k6/lib/run.ts`:
- Around line 22-82: The exported function runOperation currently lacks an
explicit return type; update its signature to return Task<T> and add a type-only
import for Task from "effection" (alongside the existing Operation import) so
the public API is annotated. Keep the implementation returning run(op)
unchanged; only add the import "type Task" and annotate function
runOperation<T>(op: () => Operation<T>): Task<T>.
- Around line 46-55: The vuIteration wrapper currently returns void and starts
the Effection operation without returning its Promise, so K6 can finish the VU
iteration before the operation (and its cleanup) completes; update vuIteration
to return a Promise<void> by returning the Promise from run(makeOp) (or make the
returned function async and await run(makeOp)) and keep the error handling to
rethrow inside its catch so K6 sees failures. Also add an explicit return type
annotation to runOperation<T> (e.g., Promise<T> or the appropriate Promise
return type) to satisfy the public API return-type guideline; reference the
functions vuIteration and runOperation in your change.

In `@k6/lib/websocket.ts`:
- Around line 202-205: The onclose handler can fire before onopen leaving
openPromise unresolved; update socket.onclose (and related initialization code
for openPromise/openResolve/openReject) so that when onclose runs it sets isOpen
= false, closes messageSignal, and also settles the openPromise (call the
pending openReject with an Error or otherwise resolve/reject it) if it is still
pending; ensure the onopen path still calls openResolve and that you guard
against double-settling the promise.
- Around line 31-37: The runtime WebSocket errors after open are currently
ignored; create a dedicated error signal via createSignal and, when the socket
opens, spawn a child task (using call or resource) that listens for
socket.onerror and forwards that error into the scope (e.g., signal.throw or
signal.send so the parent Operation/Stream fails) instead of swallowing it; keep
the existing pre-open onerror behavior that rejects the connection promise but
ensure post-open onerror uses the new signal and spawned task to propagate
errors into the structured concurrency context so iterations fail as documented
(update the onopen/onerror handling around the WebSocket resource and the
createSignal/call usage accordingly).

In `@k6/mod.ts`:
- Line 1: CI reports Biome formatting differences in k6/mod.ts; run the
formatter and commit the result: run `biome format` (or `npx biome format` if
using local) targeting this repository (or specifically k6/mod.ts), review the
changes, and commit them so the file (k6/mod.ts) matches Biome's style rules and
the CI passes.

In `@k6/package.json`:
- Around line 1-28: Add a "files" whitelist to package.json that lists the build
output and source artifacts to publish: include "dist", "mod.ts", and the
package source directories (e.g. "conformance" and any "src" folder present) so
only these are published; add a top-level "files" array with entries like
"dist", "mod.ts", and the relevant source folders.

Comment on lines 1 to 28
{
"name": "@effectionx/k6",
"description": "Effection integration for K6 load testing - structured concurrency for K6 scripts",
"version": "0.1.0",
"type": "module",
"main": "./dist/mod.js",
"types": "./dist/mod.d.ts",
"exports": {
".": {
"development": "./mod.ts",
"default": "./dist/mod.js"
},
"./conformance": {
"development": "./conformance/mod.ts",
"default": "./dist/conformance/mod.js"
}
},
"scripts": {
"build:bundle": "node build.js",
"test:conformance": "docker compose run --rm k6-conformance"
},
"peerDependencies": {
"effection": "^4"
},
"devDependencies": {
"@types/k6": "^0.54",
"esbuild": "^0.24"
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add the files whitelist required for publishing.
The package.json is missing the files field that whitelists what gets published. Please add it to include dist, mod.ts, and the package source directories.

💡 Suggested change
   "exports": {
     ".": {
       "development": "./mod.ts",
       "default": "./dist/mod.js"
     },
     "./conformance": {
       "development": "./conformance/mod.ts",
       "default": "./dist/conformance/mod.js"
     }
   },
+  "files": [
+    "dist",
+    "mod.ts",
+    "lib",
+    "conformance",
+    "demos"
+  ],
   "scripts": {

Based on learnings: package.json must include files field containing dist, mod.ts, and source files.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{
"name": "@effectionx/k6",
"description": "Effection integration for K6 load testing - structured concurrency for K6 scripts",
"version": "0.1.0",
"type": "module",
"main": "./dist/mod.js",
"types": "./dist/mod.d.ts",
"exports": {
".": {
"development": "./mod.ts",
"default": "./dist/mod.js"
},
"./conformance": {
"development": "./conformance/mod.ts",
"default": "./dist/conformance/mod.js"
}
},
"scripts": {
"build:bundle": "node build.js",
"test:conformance": "docker compose run --rm k6-conformance"
},
"peerDependencies": {
"effection": "^4"
},
"devDependencies": {
"@types/k6": "^0.54",
"esbuild": "^0.24"
},
{
"name": "@effectionx/k6",
"description": "Effection integration for K6 load testing - structured concurrency for K6 scripts",
"version": "0.1.0",
"type": "module",
"main": "./dist/mod.js",
"types": "./dist/mod.d.ts",
"exports": {
".": {
"development": "./mod.ts",
"default": "./dist/mod.js"
},
"./conformance": {
"development": "./conformance/mod.ts",
"default": "./dist/conformance/mod.js"
}
},
"files": [
"dist",
"mod.ts",
"lib",
"conformance",
"demos"
],
"scripts": {
"build:bundle": "node build.js",
"test:conformance": "docker compose run --rm k6-conformance"
},
"peerDependencies": {
"effection": "^4"
},
"devDependencies": {
"@types/k6": "^0.54",
"esbuild": "^0.24"
},
🤖 Prompt for AI Agents
In `@k6/package.json` around lines 1 - 28, Add a "files" whitelist to package.json
that lists the build output and source artifacts to publish: include "dist",
"mod.ts", and the package source directories (e.g. "conformance" and any "src"
folder present) so only these are published; add a top-level "files" array with
entries like "dist", "mod.ts", and the relevant source folders.

* });
* ```
*/
export function* waitForMessage(
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like takeUntil in #136

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly — same family of helper. I am fine removing it here and relying on stream helpers to avoid duplication.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@k6/lib/run.ts`:
- Around line 46-49: The public function vuIteration currently lacks explicit
return types; add a type-only import for Task and annotate vuIteration to return
() => Task<T> and annotate the inner returned function with : Task<T> as well,
ensuring the generic T flows through; update the signature of
vuIteration(makeOp: () => Operation<T>): () => Task<T> and the returned function
expression to be function iteration(): Task<T> { return run(makeOp); }
(referencing vuIteration, run, Operation, and Task).

k6/lib/run.ts Outdated
Comment on lines 46 to 49
export function vuIteration<T>(makeOp: () => Operation<T>) {
return function iteration() {
return run(makeOp);
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's find and examine the file in question
find . -name "run.ts" -type f | head -20

Repository: thefrontside/effectionx

Length of output: 82


🏁 Script executed:

# Let's also check for Effection version and dependencies
fd package.json | head -5 | xargs -I {} sh -c 'echo "=== {} ===" && cat {}'

Repository: thefrontside/effectionx

Length of output: 4034


🌐 Web query:

Effection v4 Task type run() function return type

💡 Result:

In Effection v4, run() is typed as:

  • function run<T>(operation: () => Operation<T>): Task<T> — i.e., it returns Task<T>. [1]

The docs also note that a Task is both an Effection operation and a promise, so it can be awaited. [2]

Sources:
[1] (frontside.com)
[2] (frontside.com)

Citations:


🏁 Script executed:

cat -n ./k6/lib/run.ts

Repository: thefrontside/effectionx

Length of output: 2726


Add explicit return type annotations for vuIteration and its returned function.

This is a public export that should declare its return type. The function returns another function that invokes run(), which in Effection v4 returns Task<T>. Add a type-only import for Task and annotate both the outer function and inner function.

🛠️ Proposed change
-import { run, type Operation } from "effection";
+import { run, type Operation, type Task } from "effection";

-export function vuIteration<T>(makeOp: () => Operation<T>) {
-  return function iteration() {
-    return run(makeOp);
-  };
-}
+export function vuIteration<T>(makeOp: () => Operation<T>): () => Task<T> {
+  return function iteration(): Task<T> {
+    return run(makeOp);
+  };
+}
🤖 Prompt for AI Agents
In `@k6/lib/run.ts` around lines 46 - 49, The public function vuIteration
currently lacks explicit return types; add a type-only import for Task and
annotate vuIteration to return () => Task<T> and annotate the inner returned
function with : Task<T> as well, ensuring the generic T flows through; update
the signature of vuIteration(makeOp: () => Operation<T>): () => Task<T> and the
returned function expression to be function iteration(): Task<T> { return
run(makeOp); } (referencing vuIteration, run, Operation, and Task).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@k6/conformance/k6-runner.ts`:
- Around line 79-88: The teardown function (teardown) currently uses
allCriticalTestsPassed on the passed-in data.syncResults to print a success
message, but data only contains syncResults so async failures aren’t checked;
either remove the conditional success console logs entirely from teardown or add
a clear comment above teardown stating that teardown only validates syncResults
and does not reflect async test outcomes (or alternatively change setup/teardown
to pass combined results if you want full validation); update references to
allCriticalTestsPassed/data.syncResults accordingly so future readers know the
limitation.

In `@k6/demos/01-group-context.ts`:
- Line 20: The import "sleep" from module "k6" is unused in this file; remove
the unused import by deleting "sleep" from the import statement (the import line
that currently reads import { sleep } from "k6") so the file no longer imports
an unused symbol.

In `@k6/demos/04-cleanup.ts`:
- Around line 25-26: Remove the unused import symbol `http` from the import list
in this demo file; update the import statement that currently reads `import {
main, group, useWebSocket, http } from "../lib/mod.ts";` to drop `http` so only
used symbols (`main`, `group`, `useWebSocket`) are imported, ensuring no other
parts of the file reference `http`.

In `@k6/lib/run.ts`:
- Around line 46-50: Add a type-only import for Task from effection (e.g. import
type { Task } from "effection") and annotate the exported main function and its
returned iteration function with explicit return types: declare main as export
function main<T>(makeOp: () => Operation<T>): () => Task<T> and ensure the inner
function iteration is typed to return Task<T> (e.g. function iteration():
Task<T> { return run(makeOp); }). Reference symbols: main, iteration, Operation,
run, and Task.

In `@k6/README.md`:
- Around line 167-201: The fenced project-structure code block in README.md
lacks a language specifier which fails markdown linting; update the opening
fence for the tree (the block starting with "k6/") to include a language token
such as "text" or "plaintext" (e.g., change "```" to "```text") so the tree
listing is fenced with a language and the linter passes.

Comment on lines 79 to 88
// Teardown function runs once after all iterations
export function teardown(data: { syncResults: ConformanceResults }): void {
const criticalPassed = allCriticalTestsPassed(data.syncResults);

if (criticalPassed) {
console.log("\n=== Conformance testing complete ===");
console.log("The K6/Sobek runtime appears to support Effection.");
console.log("You can proceed with using @effectionx/k6 in your tests.");
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Teardown only validates sync test results.

The teardown function receives only syncResults from setup(), so it cannot verify async test outcomes. If critical async tests fail but all sync tests pass, the teardown message will incorrectly report success. Consider either removing the conditional success message from teardown or noting this limitation in a comment.

💡 Proposed fix - add clarifying comment
 // Teardown function runs once after all iterations
 export function teardown(data: { syncResults: ConformanceResults }): void {
+  // Note: K6 teardown only receives setup data, so we can only check sync results here.
+  // Async test failures are reported in the default function via K6 checks.
   const criticalPassed = allCriticalTestsPassed(data.syncResults);
🤖 Prompt for AI Agents
In `@k6/conformance/k6-runner.ts` around lines 79 - 88, The teardown function
(teardown) currently uses allCriticalTestsPassed on the passed-in
data.syncResults to print a success message, but data only contains syncResults
so async failures aren’t checked; either remove the conditional success console
logs entirely from teardown or add a clear comment above teardown stating that
teardown only validates syncResults and does not reflect async test outcomes (or
alternatively change setup/teardown to pass combined results if you want full
validation); update references to allCriticalTestsPassed/data.syncResults
accordingly so future readers know the limitation.

Comment on lines +25 to +26
import { main, group, useWebSocket, http } from "../lib/mod.ts";
import { resource, spawn, sleep, type Operation } from "effection";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Remove unused http import.

The http import is not used anywhere in this demo file.

🧹 Proposed fix
-import { main, group, useWebSocket, http } from "../lib/mod.ts";
+import { main, group, useWebSocket } from "../lib/mod.ts";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { main, group, useWebSocket, http } from "../lib/mod.ts";
import { resource, spawn, sleep, type Operation } from "effection";
import { main, group, useWebSocket } from "../lib/mod.ts";
import { resource, spawn, sleep, type Operation } from "effection";
🤖 Prompt for AI Agents
In `@k6/demos/04-cleanup.ts` around lines 25 - 26, Remove the unused import symbol
`http` from the import list in this demo file; update the import statement that
currently reads `import { main, group, useWebSocket, http } from
"../lib/mod.ts";` to drop `http` so only used symbols (`main`, `group`,
`useWebSocket`) are imported, ensuring no other parts of the file reference
`http`.

Comment on lines 46 to 50
export function main<T>(makeOp: () => Operation<T>) {
return function iteration() {
return run(makeOp);
};
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add explicit return type annotations for main and the returned iteration function.

The main function is a public export and should declare its return type. In Effection v4, run() returns Task<T>. Add a type-only import for Task and annotate both the outer and inner functions.

🛠️ Proposed fix
-import { run, type Operation } from "effection";
+import { run, type Operation, type Task } from "effection";

-export function main<T>(makeOp: () => Operation<T>) {
-  return function iteration() {
+export function main<T>(makeOp: () => Operation<T>): () => Task<T> {
+  return function iteration(): Task<T> {
     return run(makeOp);
   };
 }

As per coding guidelines: Use explicit return types on public functions.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function main<T>(makeOp: () => Operation<T>) {
return function iteration() {
return run(makeOp);
};
}
import { run, type Operation, type Task } from "effection";
export function main<T>(makeOp: () => Operation<T>): () => Task<T> {
return function iteration(): Task<T> {
return run(makeOp);
};
}
🤖 Prompt for AI Agents
In `@k6/lib/run.ts` around lines 46 - 50, Add a type-only import for Task from
effection (e.g. import type { Task } from "effection") and annotate the exported
main function and its returned iteration function with explicit return types:
declare main as export function main<T>(makeOp: () => Operation<T>): () =>
Task<T> and ensure the inner function iteration is typed to return Task<T> (e.g.
function iteration(): Task<T> { return run(makeOp); }). Reference symbols: main,
iteration, Operation, run, and Task.

Comment on lines 167 to 201
```
k6/
├── conformance/ # Runtime conformance tests
│ ├── 01-symbols.ts # Symbol support
│ ├── 02-generators.ts # Basic generator support
│ ├── 03-yield-delegation.ts # yield* with custom iterables
│ ├── 04-yield-throw.ts # Error propagation
│ ├── 05-yield-return.ts # Cancellation semantics (critical!)
│ ├── 06-promises.ts # Promise support
│ ├── 07-timers.ts # setTimeout/clearTimeout
│ ├── 08-abort-controller.ts # AbortController (optional)
│ ├── k6-runner.ts # K6 test script
│ └── mod.ts # Test runner module
├── lib/ # Core library
│ ├── run.ts # VU iteration wrapper
│ ├── group.ts # Async-aware group context
│ ├── http.ts # HTTP wrappers
│ ├── websocket.ts # WebSocket resource
│ └── mod.ts # Library exports
├── demos/ # Demo scripts
│ ├── 01-group-context.ts
│ ├── 02-websocket.ts
│ ├── 03-error-propagation.ts
│ └── 04-cleanup.ts
├── dist/ # Built bundles
│ ├── lib.js # Library bundle (includes Effection)
│ ├── conformance-bundle.js
│ └── demos/ # Built demo scripts
├── build.js # esbuild configuration
├── docker-compose.yml # Docker test setup
├── Dockerfile # K6 test image (with Sobek fix)
├── mod.ts # Package entry point
├── package.json
└── tsconfig.json
```
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Add language specifier to fenced code block.

The project structure code block should specify a language (e.g., text or plaintext) to satisfy markdown linting rules.

📝 Proposed fix
-```
+```text
 k6/
 ├── conformance/           # Runtime conformance tests
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```
k6/
├── conformance/ # Runtime conformance tests
│ ├── 01-symbols.ts # Symbol support
│ ├── 02-generators.ts # Basic generator support
│ ├── 03-yield-delegation.ts # yield* with custom iterables
│ ├── 04-yield-throw.ts # Error propagation
│ ├── 05-yield-return.ts # Cancellation semantics (critical!)
│ ├── 06-promises.ts # Promise support
│ ├── 07-timers.ts # setTimeout/clearTimeout
│ ├── 08-abort-controller.ts # AbortController (optional)
│ ├── k6-runner.ts # K6 test script
│ └── mod.ts # Test runner module
├── lib/ # Core library
│ ├── run.ts # VU iteration wrapper
│ ├── group.ts # Async-aware group context
│ ├── http.ts # HTTP wrappers
│ ├── websocket.ts # WebSocket resource
│ └── mod.ts # Library exports
├── demos/ # Demo scripts
│ ├── 01-group-context.ts
│ ├── 02-websocket.ts
│ ├── 03-error-propagation.ts
│ └── 04-cleanup.ts
├── dist/ # Built bundles
│ ├── lib.js # Library bundle (includes Effection)
│ ├── conformance-bundle.js
│ └── demos/ # Built demo scripts
├── build.js # esbuild configuration
├── docker-compose.yml # Docker test setup
├── Dockerfile # K6 test image (with Sobek fix)
├── mod.ts # Package entry point
├── package.json
└── tsconfig.json
```
🧰 Tools
🪛 markdownlint-cli2 (0.20.0)

[warning] 167-167: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
In `@k6/README.md` around lines 167 - 201, The fenced project-structure code block
in README.md lacks a language specifier which fails markdown linting; update the
opening fence for the tree (the block starting with "k6/") to include a language
token such as "text" or "plaintext" (e.g., change "```" to "```text") so the
tree listing is fenced with a language and the linter passes.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 15, 2026

Open in StackBlitz

npm i https://pkg.pr.new/thefrontside/effectionx/@effectionx/k6@156

commit: dca7db6

k6/lib/group.ts Outdated
* console.log(yield* useGroups()); // ['outer', 'inner']
* ```
*/
export function* group(name: string): Operation<void> {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return groups here, it should be

function group(name: string) {
   return GroupContext.set([...groups, name])
}

k6/lib/group.ts Outdated
name: string,
op: () => Operation<T>,
): Operation<T> {
const groups = yield* GroupContext.expect();
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be useGroups

k6/lib/group.ts Outdated
* });
* ```
*/
export function* useGroups(): Operation<string[]> {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be function useGroups() { return GroupContext.expect() }

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@k6/lib/websocket.ts`:
- Around line 304-320: The collectMessages generator should be converted to a
stateless-streams-style Operation by returning an object with a generator method
named [Symbol.iterator] instead of declaring export function* collectMessages;
change export function* collectMessages(...) to export function
collectMessages(...) : Operation<WebSocketMessage[]> that returns {
*[Symbol.iterator]() { ... } }, keep the internal logic using ws.messages and
subscription.next and the same messages accumulation and return value, and
ensure types remain Operation<WebSocketMessage[]>, WebSocketResource, and
WebSocketMessage are used unchanged.

Comment on lines 304 to 320
export function* collectMessages(
ws: WebSocketResource,
count: number,
): Operation<WebSocketMessage[]> {
const messages: WebSocketMessage[] = [];
const subscription = yield* ws.messages;

while (messages.length < count) {
const result = yield* subscription.next();
if (result.done) {
break;
}
messages.push(result.value);
}

return messages;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, find the stateless-streams.md policy to understand exact requirements
find . -name "stateless-streams.md" -o -name ".policies" -type d | head -5

Repository: thefrontside/effectionx

Length of output: 111


🏁 Script executed:

# Check how Symbol.iterator is used in the codebase
rg -t ts "Symbol\.iterator" --glob "!node_modules" -B 2 -A 5 | head -80

Repository: thefrontside/effectionx

Length of output: 3735


🏁 Script executed:

# Find the collectMessages function and its usages
rg -t ts "collectMessages" --glob "!node_modules" -B 2 -A 10

Repository: thefrontside/effectionx

Length of output: 3263


🏁 Script executed:

# Check for other stream consumption patterns in websocket.ts
rg -t ts "yield\*.*subscription|yield\*.*messages" --glob "!node_modules" -B 3 -A 8 | head -100

Repository: thefrontside/effectionx

Length of output: 4829


🏁 Script executed:

cat ./.policies/stateless-streams.md

Repository: thefrontside/effectionx

Length of output: 3788


Refactor to use [Symbol.iterator] pattern per stateless-streams policy.

Per the stateless-streams policy, all functions returning Operation<T> that operate on streams must use the *[Symbol.iterator] pattern for consistency and deferred execution. The collectMessages function currently uses function* directly, which is non-compliant.

Refactor as:

Suggested refactor
export function collectMessages(
  ws: WebSocketResource,
  count: number,
): Operation<WebSocketMessage[]> {
  return {
    *[Symbol.iterator]() {
      const messages: WebSocketMessage[] = [];
      const subscription = yield* ws.messages;

      while (messages.length < count) {
        const result = yield* subscription.next();
        if (result.done) {
          break;
        }
        messages.push(result.value);
      }

      return messages;
    },
  };
}
🤖 Prompt for AI Agents
In `@k6/lib/websocket.ts` around lines 304 - 320, The collectMessages generator
should be converted to a stateless-streams-style Operation by returning an
object with a generator method named [Symbol.iterator] instead of declaring
export function* collectMessages; change export function* collectMessages(...)
to export function collectMessages(...) : Operation<WebSocketMessage[]> that
returns { *[Symbol.iterator]() { ... } }, keep the internal logic using
ws.messages and subscription.next and the same messages accumulation and return
value, and ensure types remain Operation<WebSocketMessage[]>, WebSocketResource,
and WebSocketMessage are used unchanged.

@taras taras changed the title feat(k6): add @effectionx/k6 preview package feat(k6): add @effectionx/k6 package Feb 15, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@k6/lib/tags.ts`:
- Around line 73-78: The group generator currently mutates the array returned by
useGroups() via groups.push(name); change this to create a new array to make
intent explicit and avoid mutation—e.g. build an updatedGroups from the existing
groups plus name (using array spread or concat) and then pass that into
TagsContext.set when computing the group tag; update the function group and its
use of useGroups and TagsContext.set accordingly.

k6/lib/tags.ts Outdated
Comment on lines 73 to 78
export function* group(name: string): Operation<void> {
const groups = yield* useGroups();
groups.push(name);
const tags = yield* useTags();
yield* TagsContext.set({ ...tags, group: groups.join(GROUP_SEPARATOR) });
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Array mutation is safe but could be more explicit.

The groups.push(name) mutates the array returned by useGroups(). While this is safe because useGroups() creates a fresh array via split(), constructing a new array would make the intent clearer and avoid potential confusion.

♻️ Suggested improvement for clarity
 export function* group(name: string): Operation<void> {
   const groups = yield* useGroups();
-  groups.push(name);
   const tags = yield* useTags();
-  yield* TagsContext.set({ ...tags, group: groups.join(GROUP_SEPARATOR) });
+  yield* TagsContext.set({ ...tags, group: [...groups, name].join(GROUP_SEPARATOR) });
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function* group(name: string): Operation<void> {
const groups = yield* useGroups();
groups.push(name);
const tags = yield* useTags();
yield* TagsContext.set({ ...tags, group: groups.join(GROUP_SEPARATOR) });
}
export function* group(name: string): Operation<void> {
const groups = yield* useGroups();
const tags = yield* useTags();
yield* TagsContext.set({ ...tags, group: [...groups, name].join(GROUP_SEPARATOR) });
}
🤖 Prompt for AI Agents
In `@k6/lib/tags.ts` around lines 73 - 78, The group generator currently mutates
the array returned by useGroups() via groups.push(name); change this to create a
new array to make intent explicit and avoid mutation—e.g. build an updatedGroups
from the existing groups plus name (using array spread or concat) and then pass
that into TagsContext.set when computing the group tag; update the function
group and its use of useGroups and TagsContext.set accordingly.

@taras taras force-pushed the feat/effectionx-k6-preview branch from 88d7187 to 8e9bf66 Compare February 15, 2026 03:53
Introduce a new @effectionx/k6 workspace package with Effection wrappers, conformance tests, and demo scripts to validate structured concurrency behavior in K6 environments.

Session-ID: unavailable
Return run(makeOp) directly so iteration failures propagate via the returned promise instead of throwing inside a catch callback.

Session-ID: unavailable
Replace vuIteration with main across exports, demos, and docs, and remove runOperation to simplify the public API surface.

Also apply Biome formatting updates in k6 conformance files.

Session-ID: unavailable
Replace scoped group callbacks with composable context primitives: group(name), withGroup(name, op), and useGroups(). Update HTTP tagging and docs/demos to use the new model, and keep k6 module type resolution stable under pnpm.

Session-ID: unavailable
taras added 11 commits February 14, 2026 23:29
Use spread to append instead of push() on the returned array.

Session-ID: unavailable
- Move http.ts to k6/http/mod.ts
- Move websocket.ts to k6/websocket/mod.ts
- Add @effectionx/stream-helpers and @effectionx/node dependencies
- Re-export on, once, forEach, each convenience helpers
- Document why websocket uses callbacks (K6 lacks removeEventListener)
Replace raw Promise construction with Effection's withResolvers for
idiomatic structured concurrency pattern.
Conformance tests are internal tooling for runtime validation,
not part of the public API.
- Rename k6/websocket/ directory to k6/websockets/
- Update import from k6/experimental/websockets to k6/websockets
- Add path mappings for k6/websockets types (still in @types/k6/experimental)
- Add ./http and ./websockets subpath exports to package.json
Ensure the websocket resource waits for onclose to fire before
completing cleanup, guaranteeing proper teardown.
…urn Operation

- Rename WebSocketResource interface to WebSocket
- Change close() to return Operation<void> that resolves when onclose fires
- Update all references throughout the module
These are unnecessary - the resource is only provided after onopen fires,
so it's always open when you have access to it. When it closes, the
messages stream ends.
- WebSocket interface now extends Stream, removing separate messages property
- Use each(ws) instead of each(ws.messages) to iterate messages
- Delegate Symbol.iterator to internal messageSignal
- Update collectMessages and waitForMessage helpers
- Update docs and demo
@taras taras force-pushed the feat/effectionx-k6-preview branch from 7ac13d4 to 1a4861d Compare February 15, 2026 04:30
- Remove WebSocketCloseEvent, WebSocketErrorEvent types (never exposed to users)
- Remove withWebSocket (redundant - useWebSocket provides scoped cleanup)
- Remove collectMessages, waitForMessage (use first/take/each instead)
- Export drain and first from stream-helpers
- Update demos and docs to use new patterns
- Fix esbuild config to resolve immutable package on neutral platform
- Move exec.vu.tags access from module load to runtime
- Create initTags() helper called by main() internally
- Fix Dockerfile to use pre-built dist/ instead of building in container
- Add .gitignore for test-results/
…rn()

Update the Sobek fork from commit 2ff728d (first commit only) to 013550b
(all 6 commits), which includes critical fixes for:

- sp underflow guard preventing panic when generator.return() is called
  from timer/WebSocket callback contexts
- callerSp capture before finally blocks run
- return-in-finally completion handling
- Refactored cleanup paths for consistency

This resolves the panic that occurred in nested generator cleanup
scenarios (demo 04-cleanup.js).

Sobek PR: grafana/sobek#115
Introduce @effectionx/k6/testing with describe/it hooks and testMain so K6 scripts can run Effection-aware BDD tests reported via check(). Add cleanup, error-propagation, group-context, and websocket test suites and bundle outputs for dist/tests.

Session-ID: ses_unknown
Add BDD testing usage for @effectionx/k6/testing, test command examples, and API reference entries. Document current Sobek panic limitation affecting spawned task error propagation tests.

Session-ID: ses_unknown
Add EFFECTIONX_K6_TEST_DEBUG=1 logging hooks in runTests() to emit START/END markers for each test, making runtime early-exit points observable during panic investigation.

Session-ID: ses_unknown
* - Lack of structured cleanup/teardown
*
* @example
* ```typescript
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this example doesn't look correct, we removed withGroup and is http is supposed to come from an export.

Comment on lines 17 to 22
* yield* group("api-tests");
* yield* withGroup("users", function*() {
* // Context is preserved across async boundaries
* const response = yield* http.get("https://api.example.com");
* console.log(yield* useGroups()); // ["api-tests", "users"]
* });
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this example is outdated

- Replace withGroup() with group(name, op) overload for scoped timing
- Add group_duration metric via k6/metrics Trend
- Use k6 native '::' path separator for group tags
- Remove beforeAll from testing module (use resource/ensure instead)
- Fix WebSocket close-before-open race condition
- Fix TypeScript parameter property for Node.js 24 compatibility
- Migrate conformance tests to GitHub Gist
- Improve Dockerfile per golang-pro review (pinned versions, non-root user)
- Simplify docker-compose.yml, add gist reference

Conformance tests: https://gist.github.com/taras/ba692690e1695c44dedcc71a6624880b
taras added a commit to thefrontside/effection that referenced this pull request Feb 15, 2026
Document how @effectionx/k6 solves 20+ open k6 issues related to
structured concurrency gaps: context loss, resource leaks, silent
failures, unpredictable shutdown, and race conditions.

References:
- Sobek PR #115: grafana/sobek#115
- Effectionx PR #156: thefrontside/effectionx#156

Session-ID: ses_39d99b9c2ffeSKxHPZAk9P7E1O
@cowboyd
Copy link
Member

cowboyd commented Feb 18, 2026

This PR description is very difficult to review because it's just a re-listing of the changes made. It doesn't provide much above reading the diffs directly

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants