diff --git a/context-api/mod.ts b/context-api/mod.ts
index 6deb673e..2fff28f9 100644
--- a/context-api/mod.ts
+++ b/context-api/mod.ts
@@ -1,5 +1,5 @@
import { type Middleware, combine } from "@effectionx/middleware";
-import { type Operation, createContext } from "effection";
+import { type Operation, type Scope, createContext, useScope } from "effection";
export type { Middleware };
@@ -44,38 +44,22 @@ export type Operations = {
: Operation;
};
-/**
- * Per-field middleware layers: two immutable arrays for priority ordering
- * plus a pre-composed middleware function.
- */
-type FieldMiddleware = {
+type ScopeMiddleware = {
+ max: Partial>[];
+ min: Partial>[];
+};
+
+type MiddlewareStack = {
max: Middleware[];
min: Middleware[];
- composed: Middleware | undefined;
};
-/**
- * Maps each API field to its middleware layers.
- */
-type MiddlewareRegistry = Record;
-
export function createApi(name: string, handler: A): Api {
let fields = Object.keys(handler) as (keyof A)[];
-
- let initial = fields.reduce(
- (sum, field) => {
- return Object.assign(sum, {
- [field]: {
- max: [],
- min: [],
- composed: undefined,
- } satisfies FieldMiddleware,
- });
- },
- {} as MiddlewareRegistry,
- );
-
- let context = createContext>(`$api:${name}`, initial);
+ let context = createContext>(`$api:${name}`, {
+ max: [],
+ min: [],
+ });
let operations = fields.reduce(
(api, field) => {
@@ -85,9 +69,10 @@ export function createApi(name: string, handler: A): Api {
return Object.assign(api, {
[field]: (...args: any[]) => ({
*[Symbol.iterator]() {
- let state = yield* context.expect();
- let { composed } = state[field as keyof A];
- let result = composed ? composed(args, fn) : fn(...args);
+ let scope = yield* useScope();
+ let { max, min } = collectMiddleware(scope, context, field);
+ let stack = combine([...max, ...min]);
+ let result = stack(args, fn);
return isOperation(result) ? yield* result : result;
},
}),
@@ -96,9 +81,10 @@ export function createApi(name: string, handler: A): Api {
return Object.assign(api, {
[field]: {
*[Symbol.iterator]() {
- let state = yield* context.expect();
- let { composed } = state[field as keyof A];
- let result = composed ? composed([], () => handle) : handle;
+ let scope = yield* useScope();
+ let { max, min } = collectMiddleware(scope, context, field);
+ let stack = combine([...max, ...min]);
+ let result = stack([], () => handle);
return isOperation(result) ? yield* result : result;
},
},
@@ -111,42 +97,109 @@ export function createApi(name: string, handler: A): Api {
middlewares: Partial>,
options: { at: "min" | "max" } = { at: "max" },
): Operation {
- let current = yield* context.expect();
+ let hasAny = fields.some((field) => Boolean((middlewares as any)[field]));
+ if (!hasAny) {
+ return;
+ }
+
+ let scope = yield* useScope();
+ let current = scope.hasOwn(context)
+ ? scope.expect(context)
+ : { max: [], min: [] };
+
+ let next: ScopeMiddleware = {
+ max: [...current.max],
+ min: [...current.min],
+ };
+
+ if (options.at === "min") {
+ next.min = [middlewares, ...next.min];
+ } else {
+ next.max.push(middlewares);
+ }
+
+ scope.set(context, next);
+ }
+
+ return { operations, around };
+}
+
+function collectMiddleware(
+ scope: Scope,
+ context: { name?: string; key?: string },
+ field: keyof A,
+): MiddlewareStack {
+ let key = contextName(context);
+ let window = contextWindow(scope);
+
+ return reducePrototypeChain(
+ window,
+ (sum, current) => {
+ if (!Object.prototype.hasOwnProperty.call(current, key)) {
+ return sum;
+ }
+
+ let state = current[key] as ScopeMiddleware;
- let next = fields.reduce(
- (sum, field) => {
- let middleware = (middlewares as any)[field] as
+ let max = state.max.flatMap((around) => {
+ let middleware = (around as any)[field] as
| Middleware
| undefined;
- let fieldState = current[field as keyof A];
-
- if (middleware) {
- // Clone arrays — never mutate in place (scope isolation)
- let max = [...fieldState.max];
- let min = [...fieldState.min];
+ return middleware ? [middleware] : [];
+ });
+ let min = state.min.flatMap((around) => {
+ let middleware = (around as any)[field] as
+ | Middleware
+ | undefined;
+ return middleware ? [middleware] : [];
+ });
- if (options.at === "min") {
- min = [middleware, ...min];
- } else {
- max = [...max, middleware];
- }
+ sum.max.unshift(...max);
+ sum.min.push(...min);
+ return sum;
+ },
+ { max: [], min: [] } as MiddlewareStack,
+ );
+}
- let composed = combine([...max, ...min]);
+function reducePrototypeChain(
+ start: Record,
+ reducer: (sum: T, current: Record) => T,
+ initial: T,
+): T {
+ let sum = initial;
+ let current: Record | null = start;
+ while (current) {
+ sum = reducer(sum, current);
+ current = Object.getPrototypeOf(current);
+ }
+ return sum;
+}
- return Object.assign(sum, {
- [field]: { max, min, composed },
- });
- }
+function contextName(context: { name?: string; key?: string }): string {
+ return context.name ?? context.key ?? "";
+}
- return Object.assign(sum, { [field]: fieldState });
- },
- {} as MiddlewareRegistry,
- );
+function contextWindow(scope: Scope): Record {
+ let maybe = scope as Scope & {
+ contexts?: unknown;
+ frame?: { context?: unknown };
+ };
- yield* context.set(next);
+ if (isRecord(maybe.contexts)) {
+ return maybe.contexts;
+ }
+ if (isRecord(maybe.frame?.context)) {
+ return maybe.frame.context;
}
- return { operations, around };
+ throw new Error(
+ "Unsupported Effection scope internals: expected scope.contexts (v4) or scope.frame.context (v3)",
+ );
+}
+
+function isRecord(value: unknown): value is Record {
+ return typeof value === "object" && value !== null;
}
/**
diff --git a/context-api/scope-middleware.test.ts b/context-api/scope-middleware.test.ts
new file mode 100644
index 00000000..c9029992
--- /dev/null
+++ b/context-api/scope-middleware.test.ts
@@ -0,0 +1,898 @@
+import { describe, it } from "@effectionx/bdd";
+import { createApi } from "@effectionx/context-api";
+import { type Operation, scoped, spawn, withResolvers } from "effection";
+import { expect } from "expect";
+
+describe("scope middleware", () => {
+ describe("inheritance", () => {
+ it("inherits parent middleware into a spawned child", function* () {
+ const api = createApi("inherit.spawn", {
+ *value(): Operation {
+ return "core";
+ },
+ });
+
+ const log: string[] = [];
+
+ yield* api.around({
+ *value(args, next) {
+ log.push("parent:enter");
+ const result = yield* next(...args);
+ log.push("parent:exit");
+ return result;
+ },
+ });
+
+ const task = yield* spawn(function* () {
+ return yield* api.operations.value();
+ });
+
+ const result = yield* task;
+ expect(result).toEqual("core");
+ expect(log).toEqual(["parent:enter", "parent:exit"]);
+ });
+
+ it("child max middleware extends parent max instead of replacing it", function* () {
+ const api = createApi("inherit.max", {
+ *value(): Operation {
+ return "core";
+ },
+ });
+
+ const log: string[] = [];
+
+ yield* api.around({
+ *value(args, next) {
+ log.push("max-a:enter");
+ const result = yield* next(...args);
+ log.push("max-a:exit");
+ return result;
+ },
+ });
+
+ yield* scoped(function* () {
+ yield* api.around({
+ *value(args, next) {
+ log.push("max-b:enter");
+ const result = yield* next(...args);
+ log.push("max-b:exit");
+ return result;
+ },
+ });
+
+ yield* api.operations.value();
+ });
+
+ expect(log).toEqual([
+ "max-a:enter",
+ "max-b:enter",
+ "max-b:exit",
+ "max-a:exit",
+ ]);
+
+ log.length = 0;
+ yield* api.operations.value();
+ expect(log).toEqual(["max-a:enter", "max-a:exit"]);
+ });
+
+ it("child min middleware extends parent min instead of replacing it", function* () {
+ const api = createApi("inherit.min", {
+ *value(): Operation {
+ return "core";
+ },
+ });
+
+ const log: string[] = [];
+
+ yield* api.around(
+ {
+ *value(args, next) {
+ log.push("min-a:enter");
+ const result = yield* next(...args);
+ log.push("min-a:exit");
+ return result;
+ },
+ },
+ { at: "min" },
+ );
+
+ yield* scoped(function* () {
+ yield* api.around(
+ {
+ *value(args, next) {
+ log.push("min-b:enter");
+ const result = yield* next(...args);
+ log.push("min-b:exit");
+ return result;
+ },
+ },
+ { at: "min" },
+ );
+
+ yield* api.operations.value();
+ });
+
+ expect(log).toEqual([
+ "min-b:enter",
+ "min-a:enter",
+ "min-a:exit",
+ "min-b:exit",
+ ]);
+
+ log.length = 0;
+ yield* api.operations.value();
+ expect(log).toEqual(["min-a:enter", "min-a:exit"]);
+ });
+
+ it("child with both max and min composes with parent max and min", function* () {
+ const api = createApi("inherit.both", {
+ *value(): Operation {
+ return "core";
+ },
+ });
+
+ const log: string[] = [];
+
+ yield* api.around({
+ *value(args, next) {
+ log.push("max-a:enter");
+ const result = yield* next(...args);
+ log.push("max-a:exit");
+ return result;
+ },
+ });
+
+ yield* api.around(
+ {
+ *value(args, next) {
+ log.push("min-a:enter");
+ const result = yield* next(...args);
+ log.push("min-a:exit");
+ return result;
+ },
+ },
+ { at: "min" },
+ );
+
+ yield* scoped(function* () {
+ yield* api.around({
+ *value(args, next) {
+ log.push("max-b:enter");
+ const result = yield* next(...args);
+ log.push("max-b:exit");
+ return result;
+ },
+ });
+
+ yield* api.around(
+ {
+ *value(args, next) {
+ log.push("min-b:enter");
+ const result = yield* next(...args);
+ log.push("min-b:exit");
+ return result;
+ },
+ },
+ { at: "min" },
+ );
+
+ yield* api.operations.value();
+ });
+
+ expect(log).toEqual([
+ "max-a:enter",
+ "max-b:enter",
+ "min-b:enter",
+ "min-a:enter",
+ "min-a:exit",
+ "min-b:exit",
+ "max-b:exit",
+ "max-a:exit",
+ ]);
+ });
+ });
+
+ describe("ordering", () => {
+ it("max wraps outside min with full enter/exit order", function* () {
+ const api = createApi("order.maxmin", {
+ *value(): Operation {
+ return "core";
+ },
+ });
+
+ const log: string[] = [];
+
+ yield* api.around({
+ *value(args, next) {
+ log.push("max:enter");
+ const result = yield* next(...args);
+ log.push("max:exit");
+ return result;
+ },
+ });
+
+ yield* api.around(
+ {
+ *value(args, next) {
+ log.push("min:enter");
+ const result = yield* next(...args);
+ log.push("min:exit");
+ return result;
+ },
+ },
+ { at: "min" },
+ );
+
+ yield* api.operations.value();
+ expect(log).toEqual(["max:enter", "min:enter", "min:exit", "max:exit"]);
+ });
+
+ it("parent max wraps outside child max with enter/exit order", function* () {
+ const api = createApi("order.outermax", {
+ *value(): Operation {
+ return "core";
+ },
+ });
+
+ const log: string[] = [];
+
+ yield* api.around({
+ *value(args, next) {
+ log.push("max-a:enter");
+ const result = yield* next(...args);
+ log.push("max-a:exit");
+ return result;
+ },
+ });
+
+ yield* scoped(function* () {
+ yield* api.around({
+ *value(args, next) {
+ log.push("max-b:enter");
+ const result = yield* next(...args);
+ log.push("max-b:exit");
+ return result;
+ },
+ });
+
+ yield* api.operations.value();
+ });
+
+ expect(log).toEqual([
+ "max-a:enter",
+ "max-b:enter",
+ "max-b:exit",
+ "max-a:exit",
+ ]);
+ });
+
+ it("child min runs inside parent min with enter/exit order", function* () {
+ const api = createApi("order.innermin", {
+ *value(): Operation {
+ return "core";
+ },
+ });
+
+ const log: string[] = [];
+
+ yield* api.around(
+ {
+ *value(args, next) {
+ log.push("min-a:enter");
+ const result = yield* next(...args);
+ log.push("min-a:exit");
+ return result;
+ },
+ },
+ { at: "min" },
+ );
+
+ yield* scoped(function* () {
+ yield* api.around(
+ {
+ *value(args, next) {
+ log.push("min-b:enter");
+ const result = yield* next(...args);
+ log.push("min-b:exit");
+ return result;
+ },
+ },
+ { at: "min" },
+ );
+
+ yield* api.operations.value();
+ });
+
+ expect(log).toEqual([
+ "min-b:enter",
+ "min-a:enter",
+ "min-a:exit",
+ "min-b:exit",
+ ]);
+ });
+
+ it("mixed parent/child min/max ordering is stable", function* () {
+ const api = createApi("order.mixed", {
+ *value(): Operation {
+ return "core";
+ },
+ });
+
+ const log: string[] = [];
+
+ yield* api.around({
+ *value(args, next) {
+ log.push("max-a:enter");
+ const result = yield* next(...args);
+ log.push("max-a:exit");
+ return result;
+ },
+ });
+
+ yield* api.around(
+ {
+ *value(args, next) {
+ log.push("min-a:enter");
+ const result = yield* next(...args);
+ log.push("min-a:exit");
+ return result;
+ },
+ },
+ { at: "min" },
+ );
+
+ yield* scoped(function* () {
+ yield* api.around({
+ *value(args, next) {
+ log.push("max-b:enter");
+ const result = yield* next(...args);
+ log.push("max-b:exit");
+ return result;
+ },
+ });
+
+ yield* api.around(
+ {
+ *value(args, next) {
+ log.push("min-b:enter");
+ const result = yield* next(...args);
+ log.push("min-b:exit");
+ return result;
+ },
+ },
+ { at: "min" },
+ );
+
+ yield* api.operations.value();
+ });
+
+ expect(log).toEqual([
+ "max-a:enter",
+ "max-b:enter",
+ "min-b:enter",
+ "min-a:enter",
+ "min-a:exit",
+ "min-b:exit",
+ "max-b:exit",
+ "max-a:exit",
+ ]);
+ });
+ });
+
+ describe("isolation", () => {
+ it("child middleware does not leak to parent after scope exit", function* () {
+ const api = createApi("iso.leak", {
+ five: 5,
+ });
+
+ yield* api.around({
+ five: (args, next) => next(...args) * 2,
+ });
+
+ const childResult = yield* scoped(function* () {
+ yield* api.around({
+ five: (args, next) => next(...args) + 10,
+ });
+ return yield* api.operations.five;
+ });
+
+ expect(childResult).toEqual((5 + 10) * 2);
+
+ const parentResult = yield* api.operations.five;
+ expect(parentResult).toEqual(5 * 2);
+ });
+
+ it("sibling scopes do not share local middleware", function* () {
+ const api = createApi("iso.sibling", {
+ value: () => 1 as number,
+ });
+
+ yield* api.around({
+ value: (args, next) => next(...args) * 2,
+ });
+
+ const resultA = yield* scoped(function* () {
+ yield* api.around({
+ value: (args, next) => next(...args) + 100,
+ });
+ return yield* api.operations.value();
+ });
+
+ expect(resultA).toEqual((1 + 100) * 2);
+
+ const resultB = yield* scoped(function* () {
+ yield* api.around({
+ value: (args, next) => next(...args) + 200,
+ });
+ return yield* api.operations.value();
+ });
+
+ expect(resultB).toEqual((1 + 200) * 2);
+
+ const parentResult = yield* api.operations.value();
+ expect(parentResult).toEqual(1 * 2);
+ });
+ });
+
+ describe("spawn semantics", () => {
+ it("spawned task sees live middleware from parent scope", function* () {
+ const api = createApi("spawn.capture", {
+ *value(): Operation {
+ return "core";
+ },
+ });
+
+ const log: string[] = [];
+
+ yield* api.around({
+ *value(args, next) {
+ log.push("a:enter");
+ const result = yield* next(...args);
+ log.push("a:exit");
+ return result;
+ },
+ });
+
+ // Gate: child waits until middleware-b is installed
+ const gate = withResolvers();
+
+ const task = yield* spawn(function* () {
+ yield* gate.operation;
+ return yield* api.operations.value();
+ });
+
+ // Install middleware-b AFTER spawning the child
+ yield* api.around({
+ *value(args, next) {
+ log.push("b:enter");
+ const result = yield* next(...args);
+ log.push("b:exit");
+ return result;
+ },
+ });
+
+ // Ungate the child — it now reads context with both middlewares installed
+ gate.resolve();
+
+ const result = yield* task;
+ expect(result).toEqual("core");
+ // Spawned tasks share parent scope context — they see live updates,
+ // not a snapshot from spawn time. Middleware-b added after spawn is visible.
+ expect(log).toEqual(["a:enter", "b:enter", "b:exit", "a:exit"]);
+ });
+
+ it("child extension remains live with later parent max middleware", function* () {
+ const api = createApi("spawn.child-extends-parent", {
+ *value(): Operation {
+ return "core";
+ },
+ });
+
+ const log: string[] = [];
+ const childReady = withResolvers();
+ const parentUpdated = withResolvers();
+
+ yield* api.around({
+ *value(args, next) {
+ log.push("max-a:enter");
+ const result = yield* next(...args);
+ log.push("max-a:exit");
+ return result;
+ },
+ });
+
+ const task = yield* spawn(function* () {
+ yield* api.around({
+ *value(args, next) {
+ log.push("max-b:enter");
+ const result = yield* next(...args);
+ log.push("max-b:exit");
+ return result;
+ },
+ });
+
+ childReady.resolve();
+ yield* parentUpdated.operation;
+ return yield* api.operations.value();
+ });
+
+ yield* childReady.operation;
+
+ // Parent adds a second max AFTER child has already extended locally.
+ yield* api.around({
+ *value(args, next) {
+ log.push("max-c:enter");
+ const result = yield* next(...args);
+ log.push("max-c:exit");
+ return result;
+ },
+ });
+
+ parentUpdated.resolve();
+
+ const childResult = yield* task;
+ expect(childResult).toEqual("core");
+ expect(log).toEqual([
+ "max-a:enter",
+ "max-c:enter",
+ "max-b:enter",
+ "max-b:exit",
+ "max-c:exit",
+ "max-a:exit",
+ ]);
+
+ log.length = 0;
+ expect(yield* api.operations.value()).toEqual("core");
+ expect(log).toEqual([
+ "max-a:enter",
+ "max-c:enter",
+ "max-c:exit",
+ "max-a:exit",
+ ]);
+ });
+
+ it("child extension remains live with later parent min middleware", function* () {
+ const api = createApi("spawn.child-extends-parent-min", {
+ *value(): Operation {
+ return "core";
+ },
+ });
+
+ const log: string[] = [];
+ const childReady = withResolvers();
+ const parentUpdated = withResolvers();
+
+ yield* api.around(
+ {
+ *value(args, next) {
+ log.push("min-a:enter");
+ const result = yield* next(...args);
+ log.push("min-a:exit");
+ return result;
+ },
+ },
+ { at: "min" },
+ );
+
+ const task = yield* spawn(function* () {
+ yield* api.around(
+ {
+ *value(args, next) {
+ log.push("min-b:enter");
+ const result = yield* next(...args);
+ log.push("min-b:exit");
+ return result;
+ },
+ },
+ { at: "min" },
+ );
+
+ childReady.resolve();
+ yield* parentUpdated.operation;
+ return yield* api.operations.value();
+ });
+
+ yield* childReady.operation;
+
+ // Parent adds a second min AFTER child has already extended locally.
+ yield* api.around(
+ {
+ *value(args, next) {
+ log.push("min-c:enter");
+ const result = yield* next(...args);
+ log.push("min-c:exit");
+ return result;
+ },
+ },
+ { at: "min" },
+ );
+
+ parentUpdated.resolve();
+
+ const childResult = yield* task;
+ expect(childResult).toEqual("core");
+ expect(log).toEqual([
+ "min-b:enter",
+ "min-c:enter",
+ "min-a:enter",
+ "min-a:exit",
+ "min-c:exit",
+ "min-b:exit",
+ ]);
+
+ log.length = 0;
+ expect(yield* api.operations.value()).toEqual("core");
+ expect(log).toEqual([
+ "min-c:enter",
+ "min-a:enter",
+ "min-a:exit",
+ "min-c:exit",
+ ]);
+ });
+
+ it("child extension remains live with later parent mixed max and min middleware", function* () {
+ const api = createApi("spawn.child-extends-parent-mixed", {
+ *value(): Operation {
+ return "core";
+ },
+ });
+
+ const log: string[] = [];
+ const childReady = withResolvers();
+ const parentUpdated = withResolvers();
+
+ yield* api.around({
+ *value(args, next) {
+ log.push("max-a:enter");
+ const result = yield* next(...args);
+ log.push("max-a:exit");
+ return result;
+ },
+ });
+ yield* api.around(
+ {
+ *value(args, next) {
+ log.push("min-a:enter");
+ const result = yield* next(...args);
+ log.push("min-a:exit");
+ return result;
+ },
+ },
+ { at: "min" },
+ );
+
+ const task = yield* spawn(function* () {
+ yield* api.around({
+ *value(args, next) {
+ log.push("max-b:enter");
+ const result = yield* next(...args);
+ log.push("max-b:exit");
+ return result;
+ },
+ });
+ yield* api.around(
+ {
+ *value(args, next) {
+ log.push("min-b:enter");
+ const result = yield* next(...args);
+ log.push("min-b:exit");
+ return result;
+ },
+ },
+ { at: "min" },
+ );
+
+ childReady.resolve();
+ yield* parentUpdated.operation;
+ return yield* api.operations.value();
+ });
+
+ yield* childReady.operation;
+
+ yield* api.around({
+ *value(args, next) {
+ log.push("max-c:enter");
+ const result = yield* next(...args);
+ log.push("max-c:exit");
+ return result;
+ },
+ });
+ yield* api.around(
+ {
+ *value(args, next) {
+ log.push("min-c:enter");
+ const result = yield* next(...args);
+ log.push("min-c:exit");
+ return result;
+ },
+ },
+ { at: "min" },
+ );
+
+ parentUpdated.resolve();
+
+ const childResult = yield* task;
+ expect(childResult).toEqual("core");
+ expect(log).toEqual([
+ "max-a:enter",
+ "max-c:enter",
+ "max-b:enter",
+ "min-b:enter",
+ "min-c:enter",
+ "min-a:enter",
+ "min-a:exit",
+ "min-c:exit",
+ "min-b:exit",
+ "max-b:exit",
+ "max-c:exit",
+ "max-a:exit",
+ ]);
+
+ log.length = 0;
+ expect(yield* api.operations.value()).toEqual("core");
+ expect(log).toEqual([
+ "max-a:enter",
+ "max-c:enter",
+ "min-c:enter",
+ "min-a:enter",
+ "min-a:exit",
+ "min-c:exit",
+ "max-c:exit",
+ "max-a:exit",
+ ]);
+ });
+
+ it("grandchild inherits accumulated middleware through spawned tasks", function* () {
+ const api = createApi("spawn.grandchild", {
+ *value(): Operation {
+ return "core";
+ },
+ });
+
+ const log: string[] = [];
+
+ yield* api.around({
+ *value(args, next) {
+ log.push("a:enter");
+ const result = yield* next(...args);
+ log.push("a:exit");
+ return result;
+ },
+ });
+
+ const outer = yield* spawn(function* () {
+ yield* api.around({
+ *value(args, next) {
+ log.push("b:enter");
+ const result = yield* next(...args);
+ log.push("b:exit");
+ return result;
+ },
+ });
+
+ const inner = yield* spawn(function* () {
+ yield* api.around({
+ *value(args, next) {
+ log.push("c:enter");
+ const result = yield* next(...args);
+ log.push("c:exit");
+ return result;
+ },
+ });
+
+ return yield* api.operations.value();
+ });
+
+ return yield* inner;
+ });
+
+ yield* outer;
+
+ expect(log).toEqual([
+ "a:enter",
+ "b:enter",
+ "c:enter",
+ "c:exit",
+ "b:exit",
+ "a:exit",
+ ]);
+ });
+ });
+
+ describe("args and results", () => {
+ it("parent and child both transform args and results cumulatively", function* () {
+ const api = createApi("transform", {
+ *add(a: number, b: number): Operation {
+ return a + b;
+ },
+ });
+
+ yield* api.around({
+ *add([a, b], next) {
+ const result = yield* next(a + 1, b + 1);
+ return result + 100;
+ },
+ });
+
+ const childResult = yield* scoped(function* () {
+ yield* api.around({
+ *add([a, b], next) {
+ const result = yield* next(a * 2, b * 2);
+ return result + 1000;
+ },
+ });
+
+ // parent: [3,4] -> [4,5], child: [4,5] -> [8,10], core: 18, child: +1000=1018, parent: +100=1118
+ return yield* api.operations.add(3, 4);
+ });
+
+ expect(childResult).toEqual(1118);
+
+ // parent only: [3,4] -> [4,5], core: 9, parent: +100=109
+ const parentResult = yield* api.operations.add(3, 4);
+ expect(parentResult).toEqual(109);
+ });
+ });
+
+ describe("handler-shape coverage across scopes", () => {
+ it("cross-scope composition works for all handler types", function* () {
+ const api = createApi("shapes", {
+ constVal: 10,
+ *opFn(): Operation {
+ return 10;
+ },
+ opObj: {
+ *[Symbol.iterator]() {
+ return 10;
+ },
+ } as Operation,
+ syncFn: () => 10 as number,
+ });
+
+ // Parent: multiply by 2
+ yield* api.around({
+ constVal: (args, next) => next(...args) * 2,
+ *opFn(args, next) {
+ return (yield* next(...args)) * 2;
+ },
+ *opObj(args, next) {
+ return (yield* next(...args)) * 2;
+ },
+ syncFn: (args, next) => next(...args) * 2,
+ });
+
+ const childResults = yield* scoped(function* () {
+ // Child: add 5
+ yield* api.around({
+ constVal: (args, next) => next(...args) + 5,
+ *opFn(args, next) {
+ return (yield* next(...args)) + 5;
+ },
+ *opObj(args, next) {
+ return (yield* next(...args)) + 5;
+ },
+ syncFn: (args, next) => next(...args) + 5,
+ });
+
+ return {
+ constVal: yield* api.operations.constVal,
+ opFn: yield* api.operations.opFn(),
+ opObj: yield* api.operations.opObj,
+ syncFn: yield* api.operations.syncFn(),
+ };
+ });
+
+ // Child: parent wraps outside child -> (10 + 5) * 2 = 30
+ expect(childResults.constVal).toEqual(30);
+ expect(childResults.opFn).toEqual(30);
+ expect(childResults.opObj).toEqual(30);
+ expect(childResults.syncFn).toEqual(30);
+
+ // Parent after child exits: 10 * 2 = 20
+ expect(yield* api.operations.constVal).toEqual(20);
+ expect(yield* api.operations.opFn()).toEqual(20);
+ expect(yield* api.operations.opObj).toEqual(20);
+ expect(yield* api.operations.syncFn()).toEqual(20);
+ });
+ });
+});