diff --git a/core/deno.json b/core/deno.json index cdbf87a..c1fca5d 100644 --- a/core/deno.json +++ b/core/deno.json @@ -7,7 +7,7 @@ "imports": { "@executablemd/durable-streams": "../durable-streams/mod.ts", "@executablemd/runtime": "../runtime/mod.ts", - "@effectionx/context-api": "../vendor/context-api/mod.ts", + "@effectionx/context-api": "npm:@effectionx/context-api@0.6.0", "acorn": "npm:acorn@^8.16.0", "gray-matter": "npm:gray-matter@^4.0.3", "magic-string": "npm:magic-string@^0.30.21", diff --git a/core/package.json b/core/package.json index 5161419..424c13c 100644 --- a/core/package.json +++ b/core/package.json @@ -8,6 +8,6 @@ "dependencies": { "@executablemd/durable-streams": "workspace:*", "@executablemd/runtime": "workspace:*", - "@effectionx/context-api": "workspace:*" + "@effectionx/context-api": "0.6.0" } } diff --git a/core/src/run-document.ts b/core/src/run-document.ts index 74b1845..0ba5217 100644 --- a/core/src/run-document.ts +++ b/core/src/run-document.ts @@ -49,6 +49,7 @@ import { daemonFactory } from "./modifiers/daemon.ts"; import { EvalEnvCtx, EvalScopeCtx } from "./eval-env.ts"; import type { EvalEnv } from "./eval-env.ts"; import { useEvalScope } from "@effectionx/scope-eval"; +import { Stdio } from "@effectionx/process"; // Re-export gray-matter — we use it for YAML frontmatter extraction import matter from "gray-matter"; @@ -416,6 +417,11 @@ export function* runDocument(options: RunDocumentOptions): Operation( // parent combinator (race/all) will cancel this child as part of // normal structured concurrency teardown, just like the original // run. The Close(cancelled) event already exists in the journal, - // so we skip re-emitting it (the finally block checks for this). + // so we skip re-emitting it (the ensure teardown checks for this). // // INVARIANT: This branch is only reachable when a parent combinator // (durableRace or durableAll with a failed sibling) will cancel this @@ -91,16 +98,31 @@ function* runDurableChild( childCounter: 0, }); - // Track whether we completed normally or via error, so that - // the finally block can detect cancellation (the remaining case). let closeEvent: Close | undefined; + yield* ensure(function* () { + // closeEvent still undefined means the child was cancelled before the + // normal-return or catch path ran. + if (!closeEvent) { + closeEvent = { + type: "close", + coroutineId: childId, + result: { status: "cancelled" }, + }; + } + + // Don't re-emit a Close event if one already exists in the journal + // (e.g., a cancelled child being replayed via suspend()). + if (!replayIndex.hasClose(childId)) { + yield* stream.append(closeEvent!); + } + }); + try { // Run the child workflow. DurableEffects inside the child read // DurableCtx from the scope, so they'll use childId. const result: T = yield* childWorkflow(); - // Record Close(ok) — will be appended in finally closeEvent = { type: "close", coroutineId: childId, @@ -109,7 +131,6 @@ function* runDurableChild( return result; } catch (error) { - // Record Close(err) — will be appended in finally closeEvent = { type: "close", coroutineId: childId, @@ -120,24 +141,6 @@ function* runDurableChild( }; throw error; - } finally { - // If closeEvent is still undefined, the generator was cancelled - // (Effection called iterator.return(), skipping both the normal - // return path and the catch block). - if (!closeEvent) { - closeEvent = { - type: "close", - coroutineId: childId, - result: { status: "cancelled" }, - }; - } - - // Don't re-emit a Close event if one already exists in the journal - // (e.g., a cancelled child being replayed via suspend()). - if (!replayIndex.hasClose(childId)) { - // Append the Close event. - yield* stream.append(closeEvent!); - } } } diff --git a/durable-streams/demo/observe.ts b/durable-streams/demo/observe.ts index 9ed2c4b..0b7c59b 100644 --- a/durable-streams/demo/observe.ts +++ b/durable-streams/demo/observe.ts @@ -14,7 +14,7 @@ import { FetchError, stream as fetchStream } from "@durable-streams/client"; import { DurableStreamTestServer } from "@durable-streams/server"; -import { call, createChannel, each, main, resource, spawn } from "effection"; +import { call, createChannel, each, ensure, main, resource, spawn, until } from "effection"; import type { Operation, Stream } from "effection"; import process from "node:process"; import type { DurableEvent } from "../mod.ts"; @@ -53,11 +53,8 @@ function useDurableStreamTestServer(): Operation { return resource(function* (provide) { const server = new DurableStreamTestServer(); yield* call(() => server.start()); - try { - yield* provide(server); - } finally { - yield* call(() => server.stop()); - } + yield* ensure(() => until(server.stop())); + yield* provide(server); }); } diff --git a/package.json b/package.json index a742f2b..8f079d2 100644 --- a/package.json +++ b/package.json @@ -25,16 +25,17 @@ "dependencies": { "@durable-streams/client": "^0.2.2", "effection": "4.1.0-alpha.7", - "@effectionx/converge": "^0.1.3", - "@effectionx/fetch": "^0.1.2", - "@effectionx/fs": "0.2.2", - "@effectionx/middleware": "0.1.0", - "@effectionx/node": "^0.2.2", - "@effectionx/process": "^0.7.3", - "@effectionx/scope-eval": "^0.1.2", - "@effectionx/stream-helpers": "^0.8.1", - "@effectionx/test-adapter": "^0.7.3", - "@effectionx/timebox": "^0.4.2", + "@effectionx/context-api": "0.6.0", + "@effectionx/converge": "0.1.4", + "@effectionx/fetch": "0.2.0", + "@effectionx/fs": "0.3.0", + "@effectionx/middleware": "0.1.1", + "@effectionx/node": "0.2.4", + "@effectionx/process": "0.8.1", + "@effectionx/scope-eval": "0.1.3", + "@effectionx/stream-helpers": "0.8.3", + "@effectionx/test-adapter": "0.7.4", + "@effectionx/timebox": "0.4.3", "acorn": "^8.16.0", "gray-matter": "^4.0.3", "magic-string": "^0.30.21", @@ -59,15 +60,14 @@ "test:node": "tsx --tsconfig tsconfig.node.json --test durable-streams/tests/*.test.ts core/tests/frontmatter.test.ts core/tests/heal.test.ts core/tests/scanner.test.ts core/tests/eval-transform.test.ts core/tests/eval-interpolate.test.ts core/tests/ema-api.test.ts core/tests/output-normalize.test.ts core/tests/output-terminal.test.ts", "test:bun": "bun test durable-streams/tests/ core/tests/frontmatter.test.ts core/tests/heal.test.ts core/tests/scanner.test.ts core/tests/eval-transform.test.ts core/tests/eval-interpolate.test.ts core/tests/ema-api.test.ts core/tests/output-normalize.test.ts core/tests/output-terminal.test.ts", "test:deno": "deno task test", - "lint": "oxlint -c .oxlintrc.json core/src/ cli/src/ durable-streams/ runtime/ && oxfmt --check core/src/ cli/src/ durable-streams/ runtime/ vendor/ test-support/", - "fmt": "oxfmt --write core/src/ cli/src/ durable-streams/ runtime/ vendor/ test-support/ core/tests/ durable-streams/tests/ packages/code-review-agent/" + "lint": "oxlint -c .oxlintrc.json core/src/ cli/src/ durable-streams/ runtime/ && oxfmt --check core/src/ cli/src/ durable-streams/ runtime/ test-support/", + "fmt": "oxfmt --write core/src/ cli/src/ durable-streams/ runtime/ test-support/ core/tests/ durable-streams/tests/ packages/code-review-agent/" }, "workspaces": [ "core", "cli", "durable-streams", "runtime", - "vendor/context-api", "packages/code-review-agent", "test-support", ".internal", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8d5f98c..58d0a30 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,36 +11,39 @@ importers: '@durable-streams/client': specifier: ^0.2.2 version: 0.2.2 + '@effectionx/context-api': + specifier: 0.6.0 + version: 0.6.0(effection@4.1.0-alpha.7) '@effectionx/converge': - specifier: ^0.1.3 - version: 0.1.3(effection@4.1.0-alpha.7) + specifier: 0.1.4 + version: 0.1.4(effection@4.1.0-alpha.7) '@effectionx/fetch': - specifier: ^0.1.2 - version: 0.1.2(effection@4.1.0-alpha.7) + specifier: 0.2.0 + version: 0.2.0(effection@4.1.0-alpha.7) '@effectionx/fs': - specifier: 0.2.2 - version: 0.2.2(effection@4.1.0-alpha.7) + specifier: 0.3.0 + version: 0.3.0(effection@4.1.0-alpha.7) '@effectionx/middleware': - specifier: 0.1.0 - version: 0.1.0 + specifier: 0.1.1 + version: 0.1.1 '@effectionx/node': - specifier: ^0.2.2 - version: 0.2.3(effection@4.1.0-alpha.7) + specifier: 0.2.4 + version: 0.2.4(effection@4.1.0-alpha.7) '@effectionx/process': - specifier: ^0.7.3 - version: 0.7.3(effection@4.1.0-alpha.7) + specifier: 0.8.1 + version: 0.8.1(effection@4.1.0-alpha.7) '@effectionx/scope-eval': - specifier: ^0.1.2 - version: 0.1.2(effection@4.1.0-alpha.7) + specifier: 0.1.3 + version: 0.1.3(effection@4.1.0-alpha.7) '@effectionx/stream-helpers': - specifier: ^0.8.1 - version: 0.8.1(effection@4.1.0-alpha.7) + specifier: 0.8.3 + version: 0.8.3(effection@4.1.0-alpha.7) '@effectionx/test-adapter': - specifier: ^0.7.3 - version: 0.7.3(effection@4.1.0-alpha.7) + specifier: 0.7.4 + version: 0.7.4(effection@4.1.0-alpha.7) '@effectionx/timebox': - specifier: ^0.4.2 - version: 0.4.2(effection@4.1.0-alpha.7) + specifier: 0.4.3 + version: 0.4.3(effection@4.1.0-alpha.7) acorn: specifier: ^8.16.0 version: 8.16.0 @@ -123,8 +126,8 @@ importers: core: dependencies: '@effectionx/context-api': - specifier: workspace:* - version: link:../vendor/context-api + specifier: 0.6.0 + version: 0.6.0(effection@4.1.0-alpha.7) '@executablemd/durable-streams': specifier: workspace:* version: link:../durable-streams @@ -139,8 +142,8 @@ importers: runtime: dependencies: '@effectionx/context-api': - specifier: workspace:* - version: link:../vendor/context-api + specifier: 0.6.0 + version: 0.6.0(effection@4.1.0-alpha.7) test-support: dependencies: @@ -148,8 +151,6 @@ importers: specifier: ^30.0.0 version: 30.3.0 - vendor/context-api: {} - packages: '@babel/code-frame@7.29.0': @@ -169,73 +170,61 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - '@effectionx/converge@0.1.3': - resolution: {integrity: sha512-LPuDslL53kA8etLgO/sMmqv9v66txOAO5d6DUWLnwZa6CwA2MTYkHEnZAoIGCnQYbjKTbPxlKatHgF15ihB4zA==} - engines: {node: '>= 22'} + '@effectionx/context-api@0.6.0': + resolution: {integrity: sha512-t004qvlkJDMB6EhHP1lOQ97PeIn90m7cv4+wsRPnx4YBem+pJzTL+Sm1KWbKMjMeFJz4oqllUWuBJZsCi+nuTw==} peerDependencies: effection: ^3 || ^4 - '@effectionx/fetch@0.1.2': - resolution: {integrity: sha512-CzoWkuulsEbROkYziCL58JYrUxLB+W1ALj1mCc3jwcX2Tt5OY2Aag06ofg8pEoXEQV/a9+05qQAi5Sq8uT07Sw==} - engines: {node: '>= 22'} + '@effectionx/converge@0.1.4': + resolution: {integrity: sha512-M11KU7jK3gOFGI4RT+Bf6ITMJntzzoGEO2KPx9r8uBoR+Rgwiyeuq+OeIHkuiHLUC+bB1g93KOj7aKR/G0F1/Q==} peerDependencies: effection: ^3 || ^4 - '@effectionx/fs@0.2.2': - resolution: {integrity: sha512-aTT7Rbl8r0/0EgX+qT36gvfJnzAfjQ/+sROPhV2Ms4VcPONqwbunClWwX7B6m0jCHsER/gg4E45s60h6yatVFw==} - engines: {node: '>= 22'} + '@effectionx/fetch@0.2.0': + resolution: {integrity: sha512-0W15D9WuFUI6PEApaimxppacgk1WvJNxYwQ+P/tjWTzrPOUK8wrGlj6bqLrgfEaXqyOpdHbFI1eEz7wq1BDubw==} peerDependencies: effection: ^3 || ^4 - '@effectionx/middleware@0.1.0': - resolution: {integrity: sha512-xPpQIf1PQaox+H/e/yxZtop4kLxQkrEb8BUtmqcnmzVVbV8iJLNQeRzuSEAWQLAnVu8xa5SjGzLpb5UGYTLYgg==} - engines: {node: '>= 22'} - - '@effectionx/node@0.2.2': - resolution: {integrity: sha512-bUwnCqzBsVERGzKZRTc6XMZ6yDLkRPgcxSvM6eAkuc2D5A7L0+nSu4J/x60y6geSYrAVv2UZBgAktukNtB6LxA==} - engines: {node: '>= 22'} + '@effectionx/fs@0.3.0': + resolution: {integrity: sha512-I8RSIU80twN1WdeYRPD3utRD+niORiS08U0+LPNphwum94rwa/wHQNQUtNe19Bfbll+3GQxvJF+M95dYZSVxdQ==} peerDependencies: effection: ^3 || ^4 - '@effectionx/node@0.2.3': - resolution: {integrity: sha512-M4hWrJYzRoYJyvdIk+J8uO2yLgw2Ywf9ddZygcdsiy4hvOxC1GpIrvKv/jYS0Ua71QagN46uEuPX7RlYVW3ueA==} - engines: {node: '>= 22'} + '@effectionx/middleware@0.1.1': + resolution: {integrity: sha512-ss/bZRkt/xzJNE59r8NR1+0K/xQcIyCm0y9n8FYC8jKdFn51SPe3m3t7EfPcK8zkdjCoTOU7k1UpIXRl26asYA==} + + '@effectionx/node@0.2.4': + resolution: {integrity: sha512-cPnp3fvfBKjGWekmBHdhZr5ScAr3Mg+x5IXpO8uKFe7AZ8EPAT9Di6skuB4kuGFJtRtS0Z1e5G4+2eJyapKhYA==} peerDependencies: effection: ^3 || ^4 - '@effectionx/process@0.7.3': - resolution: {integrity: sha512-ic+cv9aNeq0UgF+SO61jd8nYLiF3jFT2c6n+nOYstc4pmtUez70yFb0H5Rzppu21wql5QYCbQayVrRRUa5YRQQ==} - engines: {node: '>= 22'} + '@effectionx/process@0.8.1': + resolution: {integrity: sha512-xyXlFja0Ill80lQ3IYfksXtJkqVmWuUOogRn/qlHWCAGlZj+MGGF8gOFbyzk/3Kx4pj14riVGgF/cyT5XCzqDw==} peerDependencies: effection: ^3 || ^4 - '@effectionx/scope-eval@0.1.2': - resolution: {integrity: sha512-Jcy0vGdD3/8EGKf4FSICOKfJkjg3VnrxhM21pdum26du/odZ0ytX66/UcyvRW3b88E8HxsFPL1e6+7QRcJliDg==} - engines: {node: '>= 22'} + '@effectionx/scope-eval@0.1.3': + resolution: {integrity: sha512-Acn45lb3H94WYhNVHXYtXOZYzjpBGDPPlsyW1Talb/vYjQzCzus5lkxxOlPyphzvi7d+7mGNXiIVt4JLSZmLnQ==} peerDependencies: effection: ^3 || ^4 - '@effectionx/signals@0.5.2': - resolution: {integrity: sha512-ftWU0/+LTafPjFQ4mOfwIBHT75BtA2T44YJP7+nR/d8S2kURCzkR04a2NHVbk274XRfh354qpukBULDTchZlQw==} - engines: {node: '>= 22'} + '@effectionx/signals@0.5.4': + resolution: {integrity: sha512-c3tnlq9qqjb4fb/4g+uf03ABbvaDbgcXqWZRuifXOFNeTyhnBy5t86Fpk0tVKBLQI2M9sStV0c62xxHXPhH2Pw==} peerDependencies: effection: ^3 || ^4 - '@effectionx/stream-helpers@0.8.1': - resolution: {integrity: sha512-17rocf3av2VId8uqZJxBREhQRSaowA7+MKAbu27P3ZboIhpuIu8jqoQ1+YfKWdMy/ORrApeIljgoJl+1hJ3fvQ==} - engines: {node: '>= 22'} + '@effectionx/stream-helpers@0.8.3': + resolution: {integrity: sha512-3yNSrdmW3ahOAXrEnBGmlzxp7k9Yzixtqzij98y167srYdAE8sm+V/WNtMLHYScShzw2hl6E8uCm9ghhQw7zKA==} peerDependencies: effection: ^3 || ^4 - '@effectionx/test-adapter@0.7.3': - resolution: {integrity: sha512-OgGzsIhBN8XYsFDAETbeGqwwrW2sNdQukxHRNNngjJE0G6LiKkwDrXs8ANGzarPihoE02YANVMbyWAruBuUnYg==} - engines: {node: '>= 22'} + '@effectionx/test-adapter@0.7.4': + resolution: {integrity: sha512-zPA5dz2SKG1N/1WxIxYy4zBB1qjviLZy6o/u8PEQo82Tnjtch4rS+2zIrHriWGKX6KY+5VjPlryjtzcBaxvD3Q==} peerDependencies: effection: ^3 || ^4 - '@effectionx/timebox@0.4.2': - resolution: {integrity: sha512-CT1XCCXmYgxFjIJmz0kzBbDG0W1Jc/i8l4r2Hecu+gYXp22DW8bAkVPKYIDdR4acAC1flqevlWA8xWaNBUfb6w==} - engines: {node: '>= 22'} + '@effectionx/timebox@0.4.3': + resolution: {integrity: sha512-cc7SLpL3svAYK8M5NS8kLQuL0lrZNoQb+Hi9NSaWOudzAW1HoewuDfUtfXLemPJnnLqLYhbghRhmpVqCm4Xg3Q==} peerDependencies: effection: ^3 || ^4 @@ -1258,59 +1247,64 @@ snapshots: '@microsoft/fetch-event-source': 2.0.1 fastq: 1.20.1 - '@effectionx/converge@0.1.3(effection@4.1.0-alpha.7)': + '@effectionx/context-api@0.6.0(effection@4.1.0-alpha.7)': dependencies: - '@effectionx/timebox': 0.4.2(effection@4.1.0-alpha.7) + '@effectionx/middleware': 0.1.1 effection: 4.1.0-alpha.7 - '@effectionx/fetch@0.1.2(effection@4.1.0-alpha.7)': + '@effectionx/converge@0.1.4(effection@4.1.0-alpha.7)': dependencies: + '@effectionx/timebox': 0.4.3(effection@4.1.0-alpha.7) effection: 4.1.0-alpha.7 - '@effectionx/fs@0.2.2(effection@4.1.0-alpha.7)': + '@effectionx/fetch@0.2.0(effection@4.1.0-alpha.7)': dependencies: + '@effectionx/context-api': 0.6.0(effection@4.1.0-alpha.7) effection: 4.1.0-alpha.7 - '@effectionx/middleware@0.1.0': {} - - '@effectionx/node@0.2.2(effection@4.1.0-alpha.7)': + '@effectionx/fs@0.3.0(effection@4.1.0-alpha.7)': dependencies: + '@effectionx/context-api': 0.6.0(effection@4.1.0-alpha.7) effection: 4.1.0-alpha.7 - '@effectionx/node@0.2.3(effection@4.1.0-alpha.7)': + '@effectionx/middleware@0.1.1': {} + + '@effectionx/node@0.2.4(effection@4.1.0-alpha.7)': dependencies: effection: 4.1.0-alpha.7 - '@effectionx/process@0.7.3(effection@4.1.0-alpha.7)': + '@effectionx/process@0.8.1(effection@4.1.0-alpha.7)': dependencies: - '@effectionx/node': 0.2.2(effection@4.1.0-alpha.7) + '@effectionx/context-api': 0.6.0(effection@4.1.0-alpha.7) + '@effectionx/node': 0.2.4(effection@4.1.0-alpha.7) + '@effectionx/scope-eval': 0.1.3(effection@4.1.0-alpha.7) cross-spawn: 7.0.6 ctrlc-windows: 2.2.0 effection: 4.1.0-alpha.7 shellwords-ts: 3.0.1 - '@effectionx/scope-eval@0.1.2(effection@4.1.0-alpha.7)': + '@effectionx/scope-eval@0.1.3(effection@4.1.0-alpha.7)': dependencies: effection: 4.1.0-alpha.7 - '@effectionx/signals@0.5.2(effection@4.1.0-alpha.7)': + '@effectionx/signals@0.5.4(effection@4.1.0-alpha.7)': dependencies: effection: 4.1.0-alpha.7 immutable: 5.1.5 - '@effectionx/stream-helpers@0.8.1(effection@4.1.0-alpha.7)': + '@effectionx/stream-helpers@0.8.3(effection@4.1.0-alpha.7)': dependencies: - '@effectionx/signals': 0.5.2(effection@4.1.0-alpha.7) - '@effectionx/timebox': 0.4.2(effection@4.1.0-alpha.7) + '@effectionx/signals': 0.5.4(effection@4.1.0-alpha.7) + '@effectionx/timebox': 0.4.3(effection@4.1.0-alpha.7) effection: 4.1.0-alpha.7 immutable: 5.1.5 remeda: 2.33.6 - '@effectionx/test-adapter@0.7.3(effection@4.1.0-alpha.7)': + '@effectionx/test-adapter@0.7.4(effection@4.1.0-alpha.7)': dependencies: effection: 4.1.0-alpha.7 - '@effectionx/timebox@0.4.2(effection@4.1.0-alpha.7)': + '@effectionx/timebox@0.4.3(effection@4.1.0-alpha.7)': dependencies: effection: 4.1.0-alpha.7 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 362e55d..1c288aa 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,7 +3,6 @@ packages: - cli - durable-streams - runtime - - vendor/context-api - packages/code-review-agent - test-support - .internal diff --git a/runtime/package.json b/runtime/package.json index 487b406..1e7f24a 100644 --- a/runtime/package.json +++ b/runtime/package.json @@ -7,6 +7,6 @@ "./test": "./test/mod.ts" }, "dependencies": { - "@effectionx/context-api": "workspace:*" + "@effectionx/context-api": "0.6.0" } } diff --git a/tsconfig.node.json b/tsconfig.node.json index 88e7dd2..3e07701 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -17,7 +17,6 @@ "@executablemd/runtime/test": ["./runtime/test/mod.ts"], "@executablemd/durable-streams": ["./durable-streams/mod.ts"], "@executablemd/code-review-agent": ["./packages/code-review-agent/mod.ts"], - "@effectionx/context-api": ["./vendor/context-api/mod.ts"], "@effectionx/bdd/node": ["./test-support/bdd.ts"], "@effectionx/bdd/expect": ["./test-support/expect.ts"] } @@ -26,7 +25,6 @@ "core/**/*.ts", "durable-streams/**/*.ts", "runtime/**/*.ts", - "vendor/**/*.ts", "packages/**/*.ts", "test-support/**/*.ts" ], diff --git a/vendor/context-api/mod.ts b/vendor/context-api/mod.ts deleted file mode 100644 index 97be7eb..0000000 --- a/vendor/context-api/mod.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { type Middleware, combine } from "@effectionx/middleware"; -import { type Operation, createContext } from "effection"; - -export type { Middleware }; - -/** - * The shape of middlewares that can surround a particular {@link Api}. - * - * Members that are functions get middleware matching their signature. - * Members that are values get middleware wrapping a no-arg accessor. - */ -export type Around = { - [K in keyof A]: A[K] extends (...args: infer TArgs) => infer TReturn - ? Middleware - : Middleware<[], A[K]>; -}; - -export interface Api { - operations: Operations; - around: (around: Partial>, options?: { at: "min" | "max" }) => Operation; -} - -/** - * Maps each member of an API core to its lifted Operation form. - * - * - `Operation` -> pass-through as `Operation` - * - `(...args) => Operation` -> pass-through - * - `(...args) => T` (sync) -> lifted to `(...args) => Operation` - * - `T` (constant) -> lifted to `Operation` - * - * The `Operation` check comes first to prevent Operations - * (which have `[Symbol.iterator]`) from being misclassified as functions. - */ -export type Operations = { - [K in keyof A]: A[K] extends Operation - ? A[K] - : A[K] extends (...args: infer TArgs) => infer TReturn - ? TReturn extends Operation - ? A[K] - : (...args: TArgs) => Operation - : Operation; -}; - -/** - * Per-field middleware layers: two immutable arrays for priority ordering - * plus a pre-composed middleware function. - */ -type FieldMiddleware = { - 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 operations = fields.reduce((api, field) => { - let handle = handler[field]; - if (typeof handle === "function") { - let fn = handle as (...args: any[]) => any; - 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); - return isOperation(result) ? yield* result : result; - }, - }), - }); - } - 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; - return isOperation(result) ? yield* result : result; - }, - }, - }); - }, {} as Operations); - - function* around( - middlewares: Partial>, - options: { at: "min" | "max" } = { at: "max" }, - ): Operation { - let current = yield* context.expect(); - - let next = fields.reduce((sum, field) => { - let middleware = (middlewares as any)[field] as Middleware | undefined; - let fieldState = current[field as keyof A]; - - if (middleware) { - let max = [...fieldState.max]; - let min = [...fieldState.min]; - - if (options.at === "min") { - min = [middleware, ...min]; - } else { - max = [...max, middleware]; - } - - let composed = combine([...max, ...min]); - - return Object.assign(sum, { - [field]: { max, min, composed }, - }); - } - - return Object.assign(sum, { [field]: fieldState }); - }, {} as MiddlewareRegistry); - - yield* context.set(next); - } - - return { operations, around }; -} - -/** - * Check if a value is an Effection Operation at runtime. - * - * Excludes native iterables (strings, arrays, Maps, Sets) which have - * `[Symbol.iterator]` but are not Operations. - */ -function isOperation(target: Operation | T): target is Operation { - return ( - target != null && - !isNativeIterable(target) && - typeof (target as Operation)[Symbol.iterator] === "function" - ); -} - -function isNativeIterable(target: unknown): boolean { - return ( - typeof target === "string" || - Array.isArray(target) || - target instanceof Map || - target instanceof Set - ); -} diff --git a/vendor/context-api/package.json b/vendor/context-api/package.json deleted file mode 100644 index 8f5d9f9..0000000 --- a/vendor/context-api/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "@effectionx/context-api", - "version": "0.5.0-vendored", - "type": "module", - "exports": "./mod.ts" -}