From ac5ee192af50e7592e40f1341fd5ae221fc6b042 Mon Sep 17 00:00:00 2001 From: Arek Bartnik Date: Mon, 24 Aug 2026 16:52:24 +0200 Subject: [PATCH 1/2] feat(core): split NodeLive from TaskEngine.layer TaskEngine no longer embeds node-redis or NodeCrypto. Node apps use NodeLive.layer. Bun plus node-redis composes TaskEngine.layer with NodeRedisPool.layer and BunCrypto.layer. --- .changeset/dual-runtime-layers.md | 10 +++ CLAUDE.md | 2 +- CONTRIBUTING.md | 6 +- README.md | 69 +++++++++++++++---- apps/docs/app/page.tsx | 4 +- .../content/docs/how-to/operate-redis.mdx | 4 +- .../content/docs/how-to/process-tasks.mdx | 4 +- .../content/docs/how-to/schedule-tasks.mdx | 4 +- .../docs/reference/redis-and-runtime.mdx | 35 +++++++--- .../docs/tutorials/getting-started.mdx | 18 ++--- docs/api-reference.md | 16 +++-- docs/architecture.md | 20 ++++-- .../specs/effect-module-architecture/spec.md | 12 ++-- package.json | 11 +++ pnpm-lock.yaml | 16 +++++ scripts/check-architecture.ts | 11 +++ scripts/soak.ts | 3 +- scripts/verify-package.ts | 2 + src/NodeLive.ts | 66 ++++++++++++++++++ src/PublicContracts.test.ts | 39 +++++++++-- src/TaskEngine.ts | 26 ++----- src/TaskQueue.ts | 4 +- src/index.ts | 7 ++ 23 files changed, 298 insertions(+), 91 deletions(-) create mode 100644 .changeset/dual-runtime-layers.md create mode 100644 src/NodeLive.ts diff --git a/.changeset/dual-runtime-layers.md b/.changeset/dual-runtime-layers.md new file mode 100644 index 0000000..b535714 --- /dev/null +++ b/.changeset/dual-runtime-layers.md @@ -0,0 +1,10 @@ +--- +"@effectmq/core": minor +--- + +Split the Node convenience graph out of `TaskEngine.layer`. + +`TaskEngine.layer` (and `layerNoDeps`) now require an ambient `RedisPool`. They +no longer embed `NodeRedisPool` or `NodeCrypto`. Use `NodeLive.layer` for the +previous zero-requirement Node graph. Bun plus node-redis is +`TaskEngine.layer` composed with `NodeRedisPool.layer` and `BunCrypto.layer`. diff --git a/CLAUDE.md b/CLAUDE.md index 2e520d2..d9bd45f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,7 +47,7 @@ Flat `src/` with a strict layering, top to bottom: - **`TaskEvent.ts`** — public queue lifecycle event schemas. - **`EngineRecord.ts` / `MessagePack.ts` / `RetrySchedule.ts`** — internal Redis record, binary codec, and retry-schedule concepts. -Wiring: `TaskEngine.layer()` is the complete zero-requirement Node live graph and intentionally retains Redis operational services. `TaskEngine.layerNoDeps()` is the custom-client layer that requires `RedisPool`. +Wiring: `TaskEngine.layer()` / `TaskEngine.layerNoDeps()` require `RedisPool` and do not pick a runtime. `NodeLive.layer()` is the Node convenience graph (`NodeRedisPool` + `NodeCrypto`). Bun + node-redis is `TaskEngine.layer()` plus `NodeRedisPool.layer` plus `BunCrypto.layer`. `Worker` provides built-in bounded local concurrency, lease supervision, maintenance, and graceful draining. It does not provide distributed/global diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e3461e9..4dad931 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,9 +37,9 @@ Production modules import supported narrow `effect/*` subpaths. Public Effect-returning functions pin exact success, error, and service channels and use `Effect.fnUntraced` for reusable generator implementations. Services are `Context.Service` classes with `@effectmq/core/` identifiers; optional -fiber-local values are `Context.Reference`s. Use `TaskEngine.layerNoDeps()` for -custom Redis composition and reserve `TaskEngine.layer()` for the complete Node -live graph. +fiber-local values are `Context.Reference`s. Use `TaskEngine.layer()` when the +application supplies `RedisPool`. Use `NodeLive.layer()` for the complete Node +graph. Edit `src/lua/taskEngine.lua`, then run `pnpm gen:lua`; never hand-edit the generated TypeScript module. CI rejects generated drift. Add committed golden diff --git a/README.md b/README.md index 085ff5c..52bb477 100644 --- a/README.md +++ b/README.md @@ -23,16 +23,18 @@ a scheduled report. It provides: ## Install ```bash +# Node pnpm add @effectmq/core@0.3.0-rc.0 effect@4.0.0-beta.107 @effect/platform-node@4.0.0-beta.107 + +# Bun (node-redis is bundled; add platform-bun for BunRuntime and BunCrypto) +bun add @effectmq/core@0.3.0-rc.0 effect@4.0.0-beta.107 @effect/platform-bun@4.0.0-beta.107 ``` > [!IMPORTANT] > effectmq currently targets the Effect 4 beta and is not compatible with the > stable Effect 3 release. Pin the versions shown above. Node.js 22.19 or newer -> is required; CI verifies Node.js 22 and 24. - -The package includes its pooled `NodeRedisPool` implementation. -`@effect/platform-node` is only needed by these examples for `NodeRuntime`. +> is required; CI verifies Node.js 22 and 24. Bun plus node-redis is a +> supported composition. It is not yet a CI platform. --- @@ -43,7 +45,7 @@ Define a task, enqueue work, process it. The whole loop: ```ts import { Effect, Schema } from "effect"; import { NodeRuntime } from "@effect/platform-node"; -import { Task, TaskEngine, TaskQueue } from "@effectmq/core"; +import { NodeLive, Task, TaskQueue } from "@effectmq/core"; const SendEmail = Task.make({ name: "send-email", @@ -63,7 +65,7 @@ const program = Effect.gen(function* () { }); // The engine + its Redis layer: the only wiring you need to run the above. -const AppLayer = TaskEngine.layer({ +const AppLayer = NodeLive.layer({ redis: { url: "redis://localhost:6379" }, }); @@ -78,20 +80,61 @@ Redis startup and expected output, follow the ## Runtime setup -`TaskEngine.layer()` is the complete Node live graph: it provides the engine, -cryptographic identity generation, and the retained Redis pool, role, and -health services: +`TaskEngine` is a function of `RedisPool` (and `Crypto` per operation). It +does not pick a process runtime or a Redis client. Compose those at the +application edge. + +Node, with `NodeRuntime.runMain`: ```ts -import { TaskEngine } from "@effectmq/core"; +import { Effect } from "effect"; +import { NodeRuntime } from "@effect/platform-node"; +import { NodeLive } from "@effectmq/core"; -const AppLayer = TaskEngine.layer({ +const AppLayer = NodeLive.layer({ redis: { url: "redis://localhost:6379" }, }); + +Effect.void.pipe(Effect.provide(AppLayer), NodeRuntime.runMain); +``` + +Bun plus node-redis, with `BunRuntime.runMain`. Bun's built-in `RedisClient` +is not the supported adapter yet. It has no binary `send`. node-redis on Bun +is the supported Bun Redis path: + +```ts +import { Effect, Layer } from "effect"; +import { BunCrypto, BunRuntime } from "@effect/platform-bun"; +import { NodeRedisPool, TaskEngine } from "@effectmq/core"; + +const AppLayer = TaskEngine.layer().pipe( + Layer.provideMerge( + Layer.merge( + NodeRedisPool.layer({ url: "redis://localhost:6379" }), + BunCrypto.layer, + ), + ), +); + +Effect.void.pipe(Effect.provide(AppLayer), BunRuntime.runMain); +``` + +A custom Redis client plus any Crypto layer: + +```ts +import { Layer } from "effect"; +import type * as Crypto from "effect/Crypto"; +import { RedisPool, TaskEngine } from "@effectmq/core"; + +declare const redis: Layer.Layer; +declare const crypto: Layer.Layer; + +const AppLayer = TaskEngine.layer().pipe( + Layer.provideMerge(Layer.merge(redis, crypto)), +); ``` -Use `TaskEngine.layerNoDeps()` when composing a custom `RedisPool` -implementation. `NodeRedisPool.layer()` remains available independently and +`NodeRedisPool.layer()` remains available independently and accepts node-redis client options. It establishes separate producer, worker, and maintenance pools when the Layer starts. It supports standalone Redis and Sentinel; Redis Cluster fails startup because queue transitions use multi-key diff --git a/apps/docs/app/page.tsx b/apps/docs/app/page.tsx index 38db983..3bb97e0 100644 --- a/apps/docs/app/page.tsx +++ b/apps/docs/app/page.tsx @@ -45,8 +45,8 @@ const FEATURES = [ }, { title: "One runtime layer", - body: "TaskEngine.layer wires the Redis pools, health services, cryptographic identity and Lua-backed engine. Provide it once; work through TaskQueue, Worker and Scheduler.", - code: "TaskEngine.layer({ redis })", + body: "NodeLive.layer wires the Redis pools, health services, cryptographic identity and Lua-backed engine. Provide it once; work through TaskQueue, Worker and Scheduler.", + code: "NodeLive.layer({ redis })", }, ]; diff --git a/apps/docs/content/docs/how-to/operate-redis.mdx b/apps/docs/content/docs/how-to/operate-redis.mdx index 199d7c5..076b67f 100644 --- a/apps/docs/content/docs/how-to/operate-redis.mdx +++ b/apps/docs/content/docs/how-to/operate-redis.mdx @@ -11,7 +11,7 @@ production service. Provide the standard live graph with explicit timeouts and pool bounds: ```ts -import { TaskEngine } from "@effectmq/core" +import { NodeLive } from "@effectmq/core" import { Config, Effect, Layer, Redacted } from "effect" const AppLive = Layer.unwrap( @@ -21,7 +21,7 @@ const AppLive = Layer.unwrap( password: Config.redacted("REDIS_PASSWORD") }).pipe( Effect.map(({ password, url, username }) => - TaskEngine.layer({ + NodeLive.layer({ engine: { prefix: "my-service:effectmq:v1", maintenanceBatchSize: 100 diff --git a/apps/docs/content/docs/how-to/process-tasks.mdx b/apps/docs/content/docs/how-to/process-tasks.mdx index 23c261b..21f6b29 100644 --- a/apps/docs/content/docs/how-to/process-tasks.mdx +++ b/apps/docs/content/docs/how-to/process-tasks.mdx @@ -13,7 +13,7 @@ Construct the worker from the same queue descriptor used by producers: ```ts import { NodeRuntime } from "@effect/platform-node" -import { Task, TaskEngine, TaskQueue, Worker } from "@effectmq/core" +import { NodeLive, Task, TaskQueue, Worker } from "@effectmq/core" import { Effect, Schema } from "effect" const SendEmail = Task.make({ @@ -47,7 +47,7 @@ const workerProgram = Worker.run(worker) workerProgram.pipe( Effect.provide( - TaskEngine.layer({ redis: { url: "redis://127.0.0.1:6379" } }) + NodeLive.layer({ redis: { url: "redis://127.0.0.1:6379" } }) ), NodeRuntime.runMain ) diff --git a/apps/docs/content/docs/how-to/schedule-tasks.mdx b/apps/docs/content/docs/how-to/schedule-tasks.mdx index 8089b1b..6bd999d 100644 --- a/apps/docs/content/docs/how-to/schedule-tasks.mdx +++ b/apps/docs/content/docs/how-to/schedule-tasks.mdx @@ -12,7 +12,7 @@ Create one queue whose payload records the nominal schedule time: ```ts import { NodeRuntime } from "@effect/platform-node" -import { Scheduler, Task, TaskEngine, TaskQueue, Worker } from "@effectmq/core" +import { NodeLive, Scheduler, Task, TaskQueue, Worker } from "@effectmq/core" import { Cron, Effect, Schema } from "effect" const GenerateReport = Task.make({ @@ -44,7 +44,7 @@ const program = Effect.gen(function* () { program.pipe( Effect.provide( - TaskEngine.layer({ redis: { url: "redis://127.0.0.1:6379" } }) + NodeLive.layer({ redis: { url: "redis://127.0.0.1:6379" } }) ), NodeRuntime.runMain ) diff --git a/apps/docs/content/docs/reference/redis-and-runtime.mdx b/apps/docs/content/docs/reference/redis-and-runtime.mdx index 190dc4d..a361799 100644 --- a/apps/docs/content/docs/reference/redis-and-runtime.mdx +++ b/apps/docs/content/docs/reference/redis-and-runtime.mdx @@ -6,12 +6,13 @@ description: Live layers, topology configuration, health services, engine config ## `TaskEngine.layer` ```text -TaskEngine.layer({ engine?, redis? }): Layer +TaskEngine.layer(config?): Layer ``` -Provides `TaskEngine`, `RedisPool`, `RedisConnectionRoles`, -`RedisConnectionHealth`, Effect Redis, and Crypto. It is the standard Node.js -live graph. +Requires an ambient `RedisPool`. It does not provide connection roles, health, +Effect Redis, or Crypto. It does not select a process runtime. + +`TaskEngine.layerNoDeps` is an alias of `TaskEngine.layer`. Engine configuration: @@ -21,14 +22,32 @@ Engine configuration: | `prefix` | `~effectmq:v1` | String Redis key prefix. | | `maintenanceBatchSize` | `100` | Safe integer from 1 through 1,000. | -## `TaskEngine.layerNoDeps` +## `NodeLive.layer` ```text -TaskEngine.layerNoDeps(config?): Layer +NodeLive.layer({ engine?, redis? }): Layer ``` -Requires an ambient custom `RedisPool`. It does not provide connection roles, -health, Effect Redis, or Crypto. +Provides `TaskEngine`, `RedisPool`, `RedisConnectionRoles`, +`RedisConnectionHealth`, Effect Redis, and Crypto. It is the standard Node.js +live graph (`NodeRedisPool` plus `NodeCrypto`). + +Bun plus node-redis is a separate composition: + +```ts +import { Layer } from "effect" +import { BunCrypto } from "@effect/platform-bun" +import { NodeRedisPool, TaskEngine } from "@effectmq/core" + +const AppLayer = TaskEngine.layer().pipe( + Layer.provideMerge( + Layer.merge( + NodeRedisPool.layer({ url: "redis://127.0.0.1:6379" }), + BunCrypto.layer + ) + ) +) +``` ## `NodeRedisPool.layer` diff --git a/apps/docs/content/docs/tutorials/getting-started.mdx b/apps/docs/content/docs/tutorials/getting-started.mdx index d343b17..61e3abd 100644 --- a/apps/docs/content/docs/tutorials/getting-started.mdx +++ b/apps/docs/content/docs/tutorials/getting-started.mdx @@ -73,14 +73,8 @@ Create `src/main.ts`: ```ts import { NodeRuntime } from "@effect/platform-node" -import { - NodeRedisPool, - Task, - TaskEngine, - TaskQueue, - Worker -} from "@effectmq/core" -import { Console, Effect, Layer, Schema } from "effect" +import { NodeLive, Task, TaskQueue, Worker } from "@effectmq/core" +import { Console, Effect, Schema } from "effect" const Greet = Task.make({ name: "greet", @@ -93,11 +87,9 @@ const Greet = Task.make({ const greetings = TaskQueue.make("tutorial-greetings", Greet) -const EngineLive = TaskEngine.layer().pipe( - Layer.provideMerge( - NodeRedisPool.layer({ url: "redis://127.0.0.1:6379" }) - ) -) +const EngineLive = NodeLive.layer({ + redis: { url: "redis://127.0.0.1:6379" } +}) const worker = Worker.make( greetings, diff --git a/docs/api-reference.md b/docs/api-reference.md index 8b12134..46de0d2 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -72,10 +72,12 @@ coordination, ordinary removal, and administrative force removal. Prefer `TaskQueue`, `Worker`, and `Scheduler` unless building tooling or an alternate runtime. -- `TaskEngine.layer(config?)` is the zero-requirement Node live graph and retains - Redis operational services in its output. -- `TaskEngine.layerNoDeps(config?)` requires an ambient `RedisPool` for custom - client compositions. +- `TaskEngine.layer(config?)` and `TaskEngine.layerNoDeps(config?)` require an + ambient `RedisPool`. They do not embed a Redis client or Crypto layer. +- `NodeLive.layer({ engine?, redis? })` is the zero-requirement Node graph and + retains Redis operational services plus `NodeCrypto` in its output. +- Bun plus node-redis is `TaskEngine.layer()` composed with + `NodeRedisPool.layer()` and `BunCrypto.layer`. - Invalid configuration and Redis reply shapes use structured typed errors; diagnostic strings are retained only as causes. @@ -93,6 +95,6 @@ version, schema, value, size, and count errors. `Observability` exports Effect metrics for depth/age/backlogs, Redis errors/reconnects/script reloads, ownership loss, and retention failure. -Stable subpaths are `./NodeRedisPool`, `./Observability`, `./RedisPool`, -`./Scheduler`, `./StorageProtocol`, `./Task`, `./TaskEngine`, `./TaskEvent`, -`./TaskQueue`, `./TaskRecord`, and `./Worker`. +Stable subpaths are `./NodeLive`, `./NodeRedisPool`, `./Observability`, +`./RedisPool`, `./Scheduler`, `./StorageProtocol`, `./Task`, `./TaskEngine`, +`./TaskEvent`, `./TaskQueue`, `./TaskRecord`, and `./Worker`. diff --git a/docs/architecture.md b/docs/architecture.md index 8f2fb82..c593cfe 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -8,8 +8,9 @@ package subpaths: - `TaskRecord` owns durable typed task records and `TaskEvent` owns lifecycle events; - `TaskEngine` owns atomic queue storage behavior; -- `RedisPool`, `NodeRedisPool`, `StorageProtocol`, and `Observability` own the - external client, live Node adapter, value protocol, and metrics boundaries. +- `RedisPool`, `NodeRedisPool`, `NodeLive`, `StorageProtocol`, and + `Observability` own the external client, node-redis adapter, Node convenience + graph, value protocol, and metrics boundaries. Internal modules are deliberately not package subpaths. `MessagePack` owns the binary transform, `EngineRecord` owns Redis-facing record schemas, @@ -25,13 +26,18 @@ Worker/Scheduler -> TaskQueue + TaskEngine TaskEngine -> EngineRecord + TaskEvent + MessagePack + RedisPool TaskEvent -> TaskRecord + EngineRecord + MessagePack NodeRedisPool -> RedisPool + RedisReadiness + Observability +NodeLive -> TaskEngine + NodeRedisPool ``` -`TaskEngine.layer()` is the standard zero-requirement Node live graph. It -retains `TaskEngine`, `RedisPool`, `RedisConnectionRoles`, -`RedisConnectionHealth`, Effect Redis, and Crypto services. Custom Redis -integrations provide `RedisPool` to `TaskEngine.layerNoDeps()`. +`TaskEngine.layer()` and `TaskEngine.layerNoDeps()` require an ambient +`RedisPool`. They do not select a runtime. `NodeLive.layer()` is the +zero-requirement Node graph. It retains `TaskEngine`, `RedisPool`, +`RedisConnectionRoles`, `RedisConnectionHealth`, Effect Redis, and Crypto. +Bun plus node-redis is `TaskEngine.layer()` composed with +`NodeRedisPool.layer()` and `BunCrypto.layer`. Custom Redis integrations +provide `RedisPool` to `TaskEngine.layer()`. Public queue declarations use named exact aliases for success, typed failure, and required services. The strict test compiler pins `complete`, `completeOne`, -`decodeTask`, `wait`, `execute`, and both TaskEngine layer modes. +`decodeTask`, `wait`, `execute`, both TaskEngine layer modes, `NodeLive.layer`, +and the Bun plus node-redis composition. diff --git a/openspec/specs/effect-module-architecture/spec.md b/openspec/specs/effect-module-architecture/spec.md index 55a4a5e..355c692 100644 --- a/openspec/specs/effect-module-architecture/spec.md +++ b/openspec/specs/effect-module-architecture/spec.md @@ -41,16 +41,20 @@ Project runtime services SHALL use class-based service declarations with stable - **THEN** it receives the documented absent default without requiring an extra layer ### Requirement: Service layers communicate dependency ownership -A service module SHALL expose `layerNoDeps` for construction that still requires upstream services and `layer` for the standard live composition with those dependencies supplied. The live layer SHALL retain upstream outputs only when they are intentionally part of its documented public service graph. +`TaskEngine.layer` and `TaskEngine.layerNoDeps` SHALL require an ambient `RedisPool` and SHALL NOT select a process runtime or Redis client. The Node convenience graph SHALL live in `NodeLive.layer` and SHALL retain Redis operational services plus Crypto. A Bun plus node-redis application SHALL compose `TaskEngine.layer` with `NodeRedisPool.layer` and `BunCrypto.layer`. #### Scenario: Application supplies a custom Redis pool -- **WHEN** an application uses the dependency-free engine layer constructor +- **WHEN** an application uses `TaskEngine.layer` or `TaskEngine.layerNoDeps` - **THEN** the type system requires the Redis pool service from the application -#### Scenario: Application uses the standard live layer -- **WHEN** an application uses the fully wired engine layer +#### Scenario: Application uses the Node live layer +- **WHEN** an application uses `NodeLive.layer` - **THEN** it receives the documented engine and Redis operational services with no unresolved requirements +#### Scenario: Application uses Bun with node-redis +- **WHEN** an application composes `TaskEngine.layer` with `NodeRedisPool.layer` and `BunCrypto.layer` +- **THEN** the composed layer has no unresolved requirements + ### Requirement: Effect implementation style preserves contracts Reusable Effectful functions SHALL use the project's traceable function wrapper convention and public or recursive functions SHALL declare exact return contracts. Production modules SHALL import Effect APIs through stable narrow subpaths. diff --git a/package.json b/package.json index c1217e5..ba4a330 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,10 @@ "types": "./dist/index.d.ts", "default": "./dist/index.js" }, + "./NodeLive": { + "types": "./dist/NodeLive.d.ts", + "default": "./dist/NodeLive.js" + }, "./NodeRedisPool": { "types": "./dist/NodeRedisPool.d.ts", "default": "./dist/NodeRedisPool.js" @@ -113,11 +117,18 @@ "author": "", "license": "MIT", "peerDependencies": { + "@effect/platform-bun": ">=4.0.0-beta.107", "effect": ">=4.0.0-beta.107" }, + "peerDependenciesMeta": { + "@effect/platform-bun": { + "optional": true + } + }, "devDependencies": { "@biomejs/biome": "2.5.1", "@changesets/cli": "^2.31.0", + "@effect/platform-bun": "4.0.0-beta.107", "@effect/vitest": "4.0.0-beta.107", "@testcontainers/redis": "^12.0.3", "@types/node": "^22.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 41c9a41..f69298c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,6 +24,9 @@ importers: '@changesets/cli': specifier: ^2.31.0 version: 2.31.0(@types/node@22.20.0) + '@effect/platform-bun': + specifier: 4.0.0-beta.107 + version: 4.0.0-beta.107(effect@4.0.0-beta.107) '@effect/vitest': specifier: 4.0.0-beta.107 version: 4.0.0-beta.107(effect@4.0.0-beta.107)(vitest@4.1.9(@types/node@22.20.0)(vite@8.1.0(@types/node@22.20.0)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))) @@ -223,6 +226,11 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} + '@effect/platform-bun@4.0.0-beta.107': + resolution: {integrity: sha512-nDKutCpgr+xHQX7tgN8Cq6JXtj96GqiElKaJ7AwAkZYl2q9f6onc/3aJgl6CyH/DlUoidw6YaxeU+UaTRmMN1g==} + peerDependencies: + effect: ^4.0.0-beta.107 + '@effect/platform-node-shared@4.0.0-rc.110': resolution: {integrity: sha512-P8EZloxS7RtCOSL3VSBPyqSenUm93xLbXf9jkWoy67cibZ2wgZ9nAou9iN8f1i4vzgxUsWtDuy6BEmg67np6Zw==} engines: {node: '>=18.0.0'} @@ -3945,6 +3953,14 @@ snapshots: human-id: 4.2.0 prettier: 2.8.8 + '@effect/platform-bun@4.0.0-beta.107(effect@4.0.0-beta.107)': + dependencies: + '@effect/platform-node-shared': 4.0.0-rc.110(effect@4.0.0-beta.107) + effect: 4.0.0-beta.107 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + '@effect/platform-node-shared@4.0.0-rc.110(effect@4.0.0-beta.107)': dependencies: '@types/ws': 8.18.1 diff --git a/scripts/check-architecture.ts b/scripts/check-architecture.ts index 3148f15..f9f7fcb 100644 --- a/scripts/check-architecture.ts +++ b/scripts/check-architecture.ts @@ -25,6 +25,17 @@ const visit = (directory: string) => { if (/\bDate\.now\(\)|\bcrypto\.randomUUID\(/.test(text)) { failures.push(`${relative(root, path)} reads ambient time or randomness`); } + const rel = relative(root, path); + const mayImportRuntime = + rel === "src/NodeLive.ts" || + rel === "src/NodeRedisPool.ts" || + rel.startsWith("src/cli/"); + if ( + !mayImportRuntime && + /from ["'](?:redis|@effect\/platform-node(?:\/[^"']*)?)["']/.test(text) + ) { + failures.push(`${rel} imports redis or @effect/platform-node`); + } } }; diff --git a/scripts/soak.ts b/scripts/soak.ts index 1339b84..504925f 100644 --- a/scripts/soak.ts +++ b/scripts/soak.ts @@ -1,5 +1,6 @@ import { Effect, Fiber, Ref, Schema } from "effect"; import { + NodeLive, NodeRedisPool, RedisPool, Task, @@ -234,7 +235,7 @@ const run = Effect.scoped( }), ); -const layer = TaskEngine.layer({ +const layer = NodeLive.layer({ engine: { debugMode: true, maintenanceBatchSize: TaskEngine.maxMaintenanceBatchSize, diff --git a/scripts/verify-package.ts b/scripts/verify-package.ts index e3a9620..7896e86 100644 --- a/scripts/verify-package.ts +++ b/scripts/verify-package.ts @@ -35,6 +35,7 @@ try { "package/package.json", "package/dist/index.js", "package/dist/index.d.ts", + "package/dist/NodeLive.js", "package/dist/NodeRedisPool.js", "package/dist/Worker.js", "package/dist/cli/inspect-pre-release-data.js", @@ -67,6 +68,7 @@ try { { cwd: consumer, stdio: "pipe" }, ); const subpaths = [ + "NodeLive", "NodeRedisPool", "Observability", "RedisPool", diff --git a/src/NodeLive.ts b/src/NodeLive.ts new file mode 100644 index 0000000..e44b316 --- /dev/null +++ b/src/NodeLive.ts @@ -0,0 +1,66 @@ +/** + * The standard Node.js live graph: node-redis, NodeCrypto, and the task + * engine. + * + * @module + */ + +import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; +import type * as Crypto from "effect/Crypto"; +import * as Layer from "effect/Layer"; +import type * as Redis from "effect/unstable/persistence/Redis"; +import * as NodeRedisPool from "./NodeRedisPool.js"; +import type { RedisConnectionRoles, RedisPool } from "./RedisPool.js"; +import * as TaskEngine from "./TaskEngine.js"; + +/** + * Configuration for the standard Node.js live service graph. + * + * @category Configuration + * @since 0.3.0 + */ +export interface LiveConfig { + readonly engine?: TaskEngine.TaskEngineConfig; + readonly redis?: NodeRedisPool.RedisConfig; +} + +/** + * Provides a complete Node.js live graph: Redis connections, connection + * roles and health, Crypto, and the task engine. + * + * **Example: Run a program on Node** + * + * ```ts + * import { Effect } from "effect" + * import { NodeRuntime } from "@effect/platform-node" + * import { NodeLive } from "@effectmq/core" + * + * const program = Effect.void + * program.pipe( + * Effect.provide(NodeLive.layer({ redis: { url: "redis://127.0.0.1:6379" } })), + * NodeRuntime.runMain, + * ) + * ``` + * + * @category Layers + * @since 0.3.0 + */ +export const layer = ( + config: LiveConfig = {}, +): Layer.Layer< + | TaskEngine.TaskEngine + | RedisPool + | RedisConnectionRoles + | NodeRedisPool.RedisConnectionHealth + | Redis.Redis + | Crypto.Crypto, + | TaskEngine.TaskEngineConfigurationError + | Redis.RedisError + | NodeRedisPool.UnsupportedRedisTopology + | NodeRedisPool.InvalidRedisConfiguration +> => + TaskEngine.layer(config.engine).pipe( + Layer.provideMerge( + Layer.merge(NodeRedisPool.layer(config.redis), NodeCrypto.layer), + ), + ); diff --git a/src/PublicContracts.test.ts b/src/PublicContracts.test.ts index f72efad..5084318 100644 --- a/src/PublicContracts.test.ts +++ b/src/PublicContracts.test.ts @@ -1,9 +1,14 @@ +import * as BunCrypto from "@effect/platform-bun/BunCrypto"; import * as Context from "effect/Context"; +import type * as Crypto from "effect/Crypto"; import type * as Effect from "effect/Effect"; -import type * as Layer from "effect/Layer"; +import * as Layer from "effect/Layer"; import * as Schema from "effect/Schema"; import type * as Stream from "effect/Stream"; +import type * as Redis from "effect/unstable/persistence/Redis"; import { expect, it } from "vitest"; +import * as NodeLive from "./NodeLive.js"; +import * as NodeRedisPool from "./NodeRedisPool.js"; import type * as RedisPool from "./RedisPool.js"; import * as TaskEngine from "./TaskEngine.js"; import * as TaskQueue from "./TaskQueue.js"; @@ -149,12 +154,33 @@ const compilePublicContracts = () => { >; const layerNoDeps = TaskEngine.layerNoDeps(); - const liveLayer = TaskEngine.layer(); + const engineLayer = TaskEngine.layer(); + const nodeLiveLayer = NodeLive.layer(); + const bunNodeRedisLayer = TaskEngine.layer().pipe( + Layer.provideMerge(Layer.merge(NodeRedisPool.layer(), BunCrypto.layer)), + ); type LayerNoDepsRequirement = Expect< Equal, RedisPool.RedisPool> >; - type LiveLayerRequirement = Expect< - Equal, never> + type EngineLayerRequirement = Expect< + Equal, RedisPool.RedisPool> + >; + type NodeLiveRequirement = Expect< + Equal, never> + >; + type NodeLiveSuccess = Expect< + Equal< + Layer.Success, + | TaskEngine.TaskEngine + | RedisPool.RedisPool + | RedisPool.RedisConnectionRoles + | NodeRedisPool.RedisConnectionHealth + | Redis.Redis + | Crypto.Crypto + > + >; + type BunNodeRedisRequirement = Expect< + Equal, never> >; return undefined as unknown as @@ -179,7 +205,10 @@ const compilePublicContracts = () => { | RejectAnyServices | FailedEventError | LayerNoDepsRequirement - | LiveLayerRequirement; + | EngineLayerRequirement + | NodeLiveRequirement + | NodeLiveSuccess + | BunNodeRedisRequirement; }; it("pins public Effect and Layer channels at compile time", () => { diff --git a/src/TaskEngine.ts b/src/TaskEngine.ts index 5fb0604..6f378c0 100644 --- a/src/TaskEngine.ts +++ b/src/TaskEngine.ts @@ -9,7 +9,6 @@ * @module */ -import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; import * as Context from "effect/Context"; import * as Crypto from "effect/Crypto"; import * as Data from "effect/Data"; @@ -29,7 +28,6 @@ import { } from "./EngineRecord.js"; import taskEngineScript from "./lua/taskEngine.js"; import { UnknownFromMsgpack } from "./MessagePack.js"; -import * as NodeRedisPool from "./NodeRedisPool.js"; import * as Observability from "./Observability.js"; import { RedisPool, type RedisPoolService } from "./RedisPool.js"; import { type Event, EventSchema } from "./TaskEvent.js"; @@ -1346,28 +1344,18 @@ export const make = (config?: TaskEngineConfig) => }); /** - * Provides {@link TaskEngine} from an ambient {@link RedisPool} service. - * Use this for custom Redis implementations and test layers. + * Provides {@link TaskEngine} from an ambient {@link RedisPool}. * * @category Layers * @since 0.1.0 */ -export const layerNoDeps = (config?: TaskEngineConfig) => +export const layer = (config?: TaskEngineConfig) => Layer.effect(TaskEngine, make(config)); -/** Configuration for the standard Node.js live service graph. */ -export interface LiveConfig { - readonly engine?: TaskEngineConfig; - readonly redis?: NodeRedisPool.RedisConfig; -} - /** - * Provides a complete Node.js live graph: Redis connections, connection - * roles and health, Crypto, and the task engine. + * Alias of {@link layer}. + * + * @category Layers + * @since 0.1.0 */ -export const layer = (config: LiveConfig = {}) => - layerNoDeps(config.engine).pipe( - Layer.provideMerge( - Layer.merge(NodeRedisPool.layer(config.redis), NodeCrypto.layer), - ), - ); +export const layerNoDeps = layer; diff --git a/src/TaskQueue.ts b/src/TaskQueue.ts index 5d55284..87e8455 100644 --- a/src/TaskQueue.ts +++ b/src/TaskQueue.ts @@ -457,7 +457,7 @@ const hasBuiltInErrorTag = (value: unknown): boolean => * * ```ts * import { Effect, Schema } from "effect" - * import { Task, TaskEngine, TaskQueue } from "@effectmq/core" + * import { NodeLive, Task, TaskQueue } from "@effectmq/core" * * const resize = Task.make({ * name: "resize-image", @@ -471,7 +471,7 @@ const hasBuiltInErrorTag = (value: unknown): boolean => * return yield* TaskQueue.offer(images, { imageId: "img-42" }).pipe( * Effect.map(({ handle }) => handle) * ) - * }).pipe(Effect.provide(TaskEngine.layer())) + * }).pipe(Effect.provide(NodeLive.layer())) * ``` * * @category Operations diff --git a/src/index.ts b/src/index.ts index 7dae9ba..2c0db4e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,13 @@ * @module */ +/** + * Zero-requirement Node live graph: NodeRedisPool, NodeCrypto, and the engine. + * + * @category Modules + * @since 0.3.0 + */ +export * as NodeLive from "./NodeLive.js"; /** * Scoped node-redis adapters for standalone Redis and Sentinel. * From 7c4f259478009f06c3bafd1cfc28cf47dfa5153d Mon Sep 17 00:00:00 2001 From: Arek Bartnik Date: Mon, 24 Aug 2026 17:52:38 +0200 Subject: [PATCH 2/2] revert(core): keep TaskEngine.layer as the live graph NodeLive named the Node adapter. Callers should keep TaskEngine.layer. layerNoDeps is the compose path for a custom RedisPool or BunCrypto. --- .changeset/dual-runtime-layers.md | 9 ++- CLAUDE.md | 2 +- CONTRIBUTING.md | 6 +- README.md | 45 ++++--------- apps/docs/app/page.tsx | 4 +- .../content/docs/how-to/operate-redis.mdx | 4 +- .../content/docs/how-to/process-tasks.mdx | 4 +- .../content/docs/how-to/schedule-tasks.mdx | 4 +- .../docs/reference/redis-and-runtime.mdx | 22 +++---- .../docs/tutorials/getting-started.mdx | 4 +- docs/api-reference.md | 15 +++-- docs/architecture.md | 24 ++++--- .../specs/effect-module-architecture/spec.md | 8 +-- package.json | 4 -- scripts/check-architecture.ts | 2 +- scripts/soak.ts | 3 +- scripts/verify-package.ts | 2 - src/NodeLive.ts | 66 ------------------- src/PublicContracts.test.ts | 22 +++---- src/TaskEngine.ts | 46 +++++++++++-- src/TaskQueue.ts | 4 +- src/index.ts | 7 -- 22 files changed, 118 insertions(+), 189 deletions(-) delete mode 100644 src/NodeLive.ts diff --git a/.changeset/dual-runtime-layers.md b/.changeset/dual-runtime-layers.md index b535714..4a97b69 100644 --- a/.changeset/dual-runtime-layers.md +++ b/.changeset/dual-runtime-layers.md @@ -2,9 +2,8 @@ "@effectmq/core": minor --- -Split the Node convenience graph out of `TaskEngine.layer`. +Document Bun plus node-redis as a supported composition. -`TaskEngine.layer` (and `layerNoDeps`) now require an ambient `RedisPool`. They -no longer embed `NodeRedisPool` or `NodeCrypto`. Use `NodeLive.layer` for the -previous zero-requirement Node graph. Bun plus node-redis is -`TaskEngine.layer` composed with `NodeRedisPool.layer` and `BunCrypto.layer`. +`TaskEngine.layer` stays the live graph. `TaskEngine.layerNoDeps` is the +compose path for a custom `RedisPool` or `BunCrypto`. Bun's built-in +`RedisClient` is not the supported adapter yet. diff --git a/CLAUDE.md b/CLAUDE.md index d9bd45f..e17d63c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,7 +47,7 @@ Flat `src/` with a strict layering, top to bottom: - **`TaskEvent.ts`** — public queue lifecycle event schemas. - **`EngineRecord.ts` / `MessagePack.ts` / `RetrySchedule.ts`** — internal Redis record, binary codec, and retry-schedule concepts. -Wiring: `TaskEngine.layer()` / `TaskEngine.layerNoDeps()` require `RedisPool` and do not pick a runtime. `NodeLive.layer()` is the Node convenience graph (`NodeRedisPool` + `NodeCrypto`). Bun + node-redis is `TaskEngine.layer()` plus `NodeRedisPool.layer` plus `BunCrypto.layer`. +Wiring: `TaskEngine.layer()` is the live graph (`NodeRedisPool` + `NodeCrypto`). `TaskEngine.layerNoDeps()` requires `RedisPool` for custom clients and for Bun plus `BunCrypto`. Run the program with `NodeRuntime` or `BunRuntime`. `Worker` provides built-in bounded local concurrency, lease supervision, maintenance, and graceful draining. It does not provide distributed/global diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4dad931..3b8a043 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,9 +37,9 @@ Production modules import supported narrow `effect/*` subpaths. Public Effect-returning functions pin exact success, error, and service channels and use `Effect.fnUntraced` for reusable generator implementations. Services are `Context.Service` classes with `@effectmq/core/` identifiers; optional -fiber-local values are `Context.Reference`s. Use `TaskEngine.layer()` when the -application supplies `RedisPool`. Use `NodeLive.layer()` for the complete Node -graph. +fiber-local values are `Context.Reference`s. Use `TaskEngine.layer()` for the +live graph. Use `TaskEngine.layerNoDeps()` when the application supplies +`RedisPool`. Edit `src/lua/taskEngine.lua`, then run `pnpm gen:lua`; never hand-edit the generated TypeScript module. CI rejects generated drift. Add committed golden diff --git a/README.md b/README.md index 52bb477..23fb4f0 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ Define a task, enqueue work, process it. The whole loop: ```ts import { Effect, Schema } from "effect"; import { NodeRuntime } from "@effect/platform-node"; -import { NodeLive, Task, TaskQueue } from "@effectmq/core"; +import { Task, TaskEngine, TaskQueue } from "@effectmq/core"; const SendEmail = Task.make({ name: "send-email", @@ -65,7 +65,7 @@ const program = Effect.gen(function* () { }); // The engine + its Redis layer: the only wiring you need to run the above. -const AppLayer = NodeLive.layer({ +const AppLayer = TaskEngine.layer({ redis: { url: "redis://localhost:6379" }, }); @@ -80,34 +80,29 @@ Redis startup and expected output, follow the ## Runtime setup -`TaskEngine` is a function of `RedisPool` (and `Crypto` per operation). It -does not pick a process runtime or a Redis client. Compose those at the -application edge. - -Node, with `NodeRuntime.runMain`: +`TaskEngine.layer()` is the live graph: engine, cryptographic identity, and +the Redis pool, role, and health services. Run it with `NodeRuntime` or +`BunRuntime`. The Redis adapter stays `NodeRedisPool` on both. ```ts -import { Effect } from "effect"; -import { NodeRuntime } from "@effect/platform-node"; -import { NodeLive } from "@effectmq/core"; +import { TaskEngine } from "@effectmq/core"; -const AppLayer = NodeLive.layer({ +const AppLayer = TaskEngine.layer({ redis: { url: "redis://localhost:6379" }, }); - -Effect.void.pipe(Effect.provide(AppLayer), NodeRuntime.runMain); ``` -Bun plus node-redis, with `BunRuntime.runMain`. Bun's built-in `RedisClient` -is not the supported adapter yet. It has no binary `send`. node-redis on Bun -is the supported Bun Redis path: +Bun plus node-redis uses the same `TaskEngine.layer` call and +`BunRuntime.runMain`. Swap `NodeCrypto` for `BunCrypto` when you compose +yourself through `TaskEngine.layerNoDeps()`. Bun's built-in `RedisClient` +is not the supported adapter yet. It has no binary `send`. ```ts import { Effect, Layer } from "effect"; import { BunCrypto, BunRuntime } from "@effect/platform-bun"; import { NodeRedisPool, TaskEngine } from "@effectmq/core"; -const AppLayer = TaskEngine.layer().pipe( +const AppLayer = TaskEngine.layerNoDeps().pipe( Layer.provideMerge( Layer.merge( NodeRedisPool.layer({ url: "redis://localhost:6379" }), @@ -119,21 +114,7 @@ const AppLayer = TaskEngine.layer().pipe( Effect.void.pipe(Effect.provide(AppLayer), BunRuntime.runMain); ``` -A custom Redis client plus any Crypto layer: - -```ts -import { Layer } from "effect"; -import type * as Crypto from "effect/Crypto"; -import { RedisPool, TaskEngine } from "@effectmq/core"; - -declare const redis: Layer.Layer; -declare const crypto: Layer.Layer; - -const AppLayer = TaskEngine.layer().pipe( - Layer.provideMerge(Layer.merge(redis, crypto)), -); -``` - +Use `TaskEngine.layerNoDeps()` when you bring your own `RedisPool`. `NodeRedisPool.layer()` remains available independently and accepts node-redis client options. It establishes separate producer, worker, and maintenance pools when the Layer starts. It supports standalone Redis and diff --git a/apps/docs/app/page.tsx b/apps/docs/app/page.tsx index 3bb97e0..38db983 100644 --- a/apps/docs/app/page.tsx +++ b/apps/docs/app/page.tsx @@ -45,8 +45,8 @@ const FEATURES = [ }, { title: "One runtime layer", - body: "NodeLive.layer wires the Redis pools, health services, cryptographic identity and Lua-backed engine. Provide it once; work through TaskQueue, Worker and Scheduler.", - code: "NodeLive.layer({ redis })", + body: "TaskEngine.layer wires the Redis pools, health services, cryptographic identity and Lua-backed engine. Provide it once; work through TaskQueue, Worker and Scheduler.", + code: "TaskEngine.layer({ redis })", }, ]; diff --git a/apps/docs/content/docs/how-to/operate-redis.mdx b/apps/docs/content/docs/how-to/operate-redis.mdx index 076b67f..199d7c5 100644 --- a/apps/docs/content/docs/how-to/operate-redis.mdx +++ b/apps/docs/content/docs/how-to/operate-redis.mdx @@ -11,7 +11,7 @@ production service. Provide the standard live graph with explicit timeouts and pool bounds: ```ts -import { NodeLive } from "@effectmq/core" +import { TaskEngine } from "@effectmq/core" import { Config, Effect, Layer, Redacted } from "effect" const AppLive = Layer.unwrap( @@ -21,7 +21,7 @@ const AppLive = Layer.unwrap( password: Config.redacted("REDIS_PASSWORD") }).pipe( Effect.map(({ password, url, username }) => - NodeLive.layer({ + TaskEngine.layer({ engine: { prefix: "my-service:effectmq:v1", maintenanceBatchSize: 100 diff --git a/apps/docs/content/docs/how-to/process-tasks.mdx b/apps/docs/content/docs/how-to/process-tasks.mdx index 21f6b29..23c261b 100644 --- a/apps/docs/content/docs/how-to/process-tasks.mdx +++ b/apps/docs/content/docs/how-to/process-tasks.mdx @@ -13,7 +13,7 @@ Construct the worker from the same queue descriptor used by producers: ```ts import { NodeRuntime } from "@effect/platform-node" -import { NodeLive, Task, TaskQueue, Worker } from "@effectmq/core" +import { Task, TaskEngine, TaskQueue, Worker } from "@effectmq/core" import { Effect, Schema } from "effect" const SendEmail = Task.make({ @@ -47,7 +47,7 @@ const workerProgram = Worker.run(worker) workerProgram.pipe( Effect.provide( - NodeLive.layer({ redis: { url: "redis://127.0.0.1:6379" } }) + TaskEngine.layer({ redis: { url: "redis://127.0.0.1:6379" } }) ), NodeRuntime.runMain ) diff --git a/apps/docs/content/docs/how-to/schedule-tasks.mdx b/apps/docs/content/docs/how-to/schedule-tasks.mdx index 6bd999d..8089b1b 100644 --- a/apps/docs/content/docs/how-to/schedule-tasks.mdx +++ b/apps/docs/content/docs/how-to/schedule-tasks.mdx @@ -12,7 +12,7 @@ Create one queue whose payload records the nominal schedule time: ```ts import { NodeRuntime } from "@effect/platform-node" -import { NodeLive, Scheduler, Task, TaskQueue, Worker } from "@effectmq/core" +import { Scheduler, Task, TaskEngine, TaskQueue, Worker } from "@effectmq/core" import { Cron, Effect, Schema } from "effect" const GenerateReport = Task.make({ @@ -44,7 +44,7 @@ const program = Effect.gen(function* () { program.pipe( Effect.provide( - NodeLive.layer({ redis: { url: "redis://127.0.0.1:6379" } }) + TaskEngine.layer({ redis: { url: "redis://127.0.0.1:6379" } }) ), NodeRuntime.runMain ) diff --git a/apps/docs/content/docs/reference/redis-and-runtime.mdx b/apps/docs/content/docs/reference/redis-and-runtime.mdx index a361799..1897bae 100644 --- a/apps/docs/content/docs/reference/redis-and-runtime.mdx +++ b/apps/docs/content/docs/reference/redis-and-runtime.mdx @@ -6,13 +6,12 @@ description: Live layers, topology configuration, health services, engine config ## `TaskEngine.layer` ```text -TaskEngine.layer(config?): Layer +TaskEngine.layer({ engine?, redis? }): Layer ``` -Requires an ambient `RedisPool`. It does not provide connection roles, health, -Effect Redis, or Crypto. It does not select a process runtime. - -`TaskEngine.layerNoDeps` is an alias of `TaskEngine.layer`. +Provides `TaskEngine`, `RedisPool`, `RedisConnectionRoles`, +`RedisConnectionHealth`, Effect Redis, and Crypto. Run it with `NodeRuntime` +or `BunRuntime`. Engine configuration: @@ -22,24 +21,23 @@ Engine configuration: | `prefix` | `~effectmq:v1` | String Redis key prefix. | | `maintenanceBatchSize` | `100` | Safe integer from 1 through 1,000. | -## `NodeLive.layer` +## `TaskEngine.layerNoDeps` ```text -NodeLive.layer({ engine?, redis? }): Layer +TaskEngine.layerNoDeps(config?): Layer ``` -Provides `TaskEngine`, `RedisPool`, `RedisConnectionRoles`, -`RedisConnectionHealth`, Effect Redis, and Crypto. It is the standard Node.js -live graph (`NodeRedisPool` plus `NodeCrypto`). +Requires an ambient `RedisPool`. It does not provide connection roles, health, +Effect Redis, or Crypto. -Bun plus node-redis is a separate composition: +Bun plus `BunCrypto` is this composition: ```ts import { Layer } from "effect" import { BunCrypto } from "@effect/platform-bun" import { NodeRedisPool, TaskEngine } from "@effectmq/core" -const AppLayer = TaskEngine.layer().pipe( +const AppLayer = TaskEngine.layerNoDeps().pipe( Layer.provideMerge( Layer.merge( NodeRedisPool.layer({ url: "redis://127.0.0.1:6379" }), diff --git a/apps/docs/content/docs/tutorials/getting-started.mdx b/apps/docs/content/docs/tutorials/getting-started.mdx index 61e3abd..6143457 100644 --- a/apps/docs/content/docs/tutorials/getting-started.mdx +++ b/apps/docs/content/docs/tutorials/getting-started.mdx @@ -73,7 +73,7 @@ Create `src/main.ts`: ```ts import { NodeRuntime } from "@effect/platform-node" -import { NodeLive, Task, TaskQueue, Worker } from "@effectmq/core" +import { Task, TaskEngine, TaskQueue, Worker } from "@effectmq/core" import { Console, Effect, Schema } from "effect" const Greet = Task.make({ @@ -87,7 +87,7 @@ const Greet = Task.make({ const greetings = TaskQueue.make("tutorial-greetings", Greet) -const EngineLive = NodeLive.layer({ +const EngineLive = TaskEngine.layer({ redis: { url: "redis://127.0.0.1:6379" } }) diff --git a/docs/api-reference.md b/docs/api-reference.md index 46de0d2..568c59e 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -72,12 +72,13 @@ coordination, ordinary removal, and administrative force removal. Prefer `TaskQueue`, `Worker`, and `Scheduler` unless building tooling or an alternate runtime. -- `TaskEngine.layer(config?)` and `TaskEngine.layerNoDeps(config?)` require an - ambient `RedisPool`. They do not embed a Redis client or Crypto layer. -- `NodeLive.layer({ engine?, redis? })` is the zero-requirement Node graph and - retains Redis operational services plus `NodeCrypto` in its output. -- Bun plus node-redis is `TaskEngine.layer()` composed with - `NodeRedisPool.layer()` and `BunCrypto.layer`. +- `TaskEngine.layer({ engine?, redis? })` is the live graph and retains Redis + operational services plus Crypto in its output. +- `TaskEngine.layerNoDeps(config?)` requires an ambient `RedisPool`. Use it for + a custom client or for Bun plus `BunCrypto`. +- Bun plus node-redis is `TaskEngine.layer` plus `BunRuntime`, or + `TaskEngine.layerNoDeps` composed with `NodeRedisPool.layer` and + `BunCrypto.layer`. - Invalid configuration and Redis reply shapes use structured typed errors; diagnostic strings are retained only as causes. @@ -95,6 +96,6 @@ version, schema, value, size, and count errors. `Observability` exports Effect metrics for depth/age/backlogs, Redis errors/reconnects/script reloads, ownership loss, and retention failure. -Stable subpaths are `./NodeLive`, `./NodeRedisPool`, `./Observability`, +Stable subpaths are `./NodeRedisPool`, `./Observability`, `./RedisPool`, `./Scheduler`, `./StorageProtocol`, `./Task`, `./TaskEngine`, `./TaskEvent`, `./TaskQueue`, `./TaskRecord`, and `./Worker`. diff --git a/docs/architecture.md b/docs/architecture.md index c593cfe..11c2785 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -7,10 +7,10 @@ package subpaths: processing, and scheduling APIs; - `TaskRecord` owns durable typed task records and `TaskEvent` owns lifecycle events; -- `TaskEngine` owns atomic queue storage behavior; -- `RedisPool`, `NodeRedisPool`, `NodeLive`, `StorageProtocol`, and - `Observability` own the external client, node-redis adapter, Node convenience - graph, value protocol, and metrics boundaries. +- `TaskEngine` owns atomic queue storage behavior and the live graph; +- `RedisPool`, `NodeRedisPool`, `StorageProtocol`, and + `Observability` own the external client, node-redis adapter, value + protocol, and metrics boundaries. Internal modules are deliberately not package subpaths. `MessagePack` owns the binary transform, `EngineRecord` owns Redis-facing record schemas, @@ -26,18 +26,16 @@ Worker/Scheduler -> TaskQueue + TaskEngine TaskEngine -> EngineRecord + TaskEvent + MessagePack + RedisPool TaskEvent -> TaskRecord + EngineRecord + MessagePack NodeRedisPool -> RedisPool + RedisReadiness + Observability -NodeLive -> TaskEngine + NodeRedisPool ``` -`TaskEngine.layer()` and `TaskEngine.layerNoDeps()` require an ambient -`RedisPool`. They do not select a runtime. `NodeLive.layer()` is the -zero-requirement Node graph. It retains `TaskEngine`, `RedisPool`, +`TaskEngine.layer()` is the live graph. It retains `TaskEngine`, `RedisPool`, `RedisConnectionRoles`, `RedisConnectionHealth`, Effect Redis, and Crypto. -Bun plus node-redis is `TaskEngine.layer()` composed with -`NodeRedisPool.layer()` and `BunCrypto.layer`. Custom Redis integrations -provide `RedisPool` to `TaskEngine.layer()`. +`TaskEngine.layerNoDeps()` requires an ambient `RedisPool`. Bun plus +node-redis uses `TaskEngine.layer` with `BunRuntime`, or `layerNoDeps` +composed with `NodeRedisPool.layer` and `BunCrypto.layer`. Custom Redis +integrations provide `RedisPool` to `TaskEngine.layerNoDeps()`. Public queue declarations use named exact aliases for success, typed failure, and required services. The strict test compiler pins `complete`, `completeOne`, -`decodeTask`, `wait`, `execute`, both TaskEngine layer modes, `NodeLive.layer`, -and the Bun plus node-redis composition. +`decodeTask`, `wait`, `execute`, both TaskEngine layer modes, and the Bun plus +node-redis composition. diff --git a/openspec/specs/effect-module-architecture/spec.md b/openspec/specs/effect-module-architecture/spec.md index 355c692..b494fde 100644 --- a/openspec/specs/effect-module-architecture/spec.md +++ b/openspec/specs/effect-module-architecture/spec.md @@ -41,18 +41,18 @@ Project runtime services SHALL use class-based service declarations with stable - **THEN** it receives the documented absent default without requiring an extra layer ### Requirement: Service layers communicate dependency ownership -`TaskEngine.layer` and `TaskEngine.layerNoDeps` SHALL require an ambient `RedisPool` and SHALL NOT select a process runtime or Redis client. The Node convenience graph SHALL live in `NodeLive.layer` and SHALL retain Redis operational services plus Crypto. A Bun plus node-redis application SHALL compose `TaskEngine.layer` with `NodeRedisPool.layer` and `BunCrypto.layer`. +`TaskEngine.layer` SHALL provide the live graph and SHALL retain Redis operational services plus Crypto. `TaskEngine.layerNoDeps` SHALL require an ambient `RedisPool` and SHALL NOT select a process runtime or Redis client. A Bun plus node-redis application SHALL run `TaskEngine.layer` under `BunRuntime`, or compose `TaskEngine.layerNoDeps` with `NodeRedisPool.layer` and `BunCrypto.layer`. #### Scenario: Application supplies a custom Redis pool - **WHEN** an application uses `TaskEngine.layer` or `TaskEngine.layerNoDeps` - **THEN** the type system requires the Redis pool service from the application -#### Scenario: Application uses the Node live layer -- **WHEN** an application uses `NodeLive.layer` +#### Scenario: Application uses the live layer +- **WHEN** an application uses `TaskEngine.layer` - **THEN** it receives the documented engine and Redis operational services with no unresolved requirements #### Scenario: Application uses Bun with node-redis -- **WHEN** an application composes `TaskEngine.layer` with `NodeRedisPool.layer` and `BunCrypto.layer` +- **WHEN** an application composes `TaskEngine.layerNoDeps` with `NodeRedisPool.layer` and `BunCrypto.layer` - **THEN** the composed layer has no unresolved requirements ### Requirement: Effect implementation style preserves contracts diff --git a/package.json b/package.json index ba4a330..6e73287 100644 --- a/package.json +++ b/package.json @@ -23,10 +23,6 @@ "types": "./dist/index.d.ts", "default": "./dist/index.js" }, - "./NodeLive": { - "types": "./dist/NodeLive.d.ts", - "default": "./dist/NodeLive.js" - }, "./NodeRedisPool": { "types": "./dist/NodeRedisPool.d.ts", "default": "./dist/NodeRedisPool.js" diff --git a/scripts/check-architecture.ts b/scripts/check-architecture.ts index f9f7fcb..2a1d333 100644 --- a/scripts/check-architecture.ts +++ b/scripts/check-architecture.ts @@ -27,7 +27,7 @@ const visit = (directory: string) => { } const rel = relative(root, path); const mayImportRuntime = - rel === "src/NodeLive.ts" || + rel === "src/TaskEngine.ts" || rel === "src/NodeRedisPool.ts" || rel.startsWith("src/cli/"); if ( diff --git a/scripts/soak.ts b/scripts/soak.ts index 504925f..1339b84 100644 --- a/scripts/soak.ts +++ b/scripts/soak.ts @@ -1,6 +1,5 @@ import { Effect, Fiber, Ref, Schema } from "effect"; import { - NodeLive, NodeRedisPool, RedisPool, Task, @@ -235,7 +234,7 @@ const run = Effect.scoped( }), ); -const layer = NodeLive.layer({ +const layer = TaskEngine.layer({ engine: { debugMode: true, maintenanceBatchSize: TaskEngine.maxMaintenanceBatchSize, diff --git a/scripts/verify-package.ts b/scripts/verify-package.ts index 7896e86..e3a9620 100644 --- a/scripts/verify-package.ts +++ b/scripts/verify-package.ts @@ -35,7 +35,6 @@ try { "package/package.json", "package/dist/index.js", "package/dist/index.d.ts", - "package/dist/NodeLive.js", "package/dist/NodeRedisPool.js", "package/dist/Worker.js", "package/dist/cli/inspect-pre-release-data.js", @@ -68,7 +67,6 @@ try { { cwd: consumer, stdio: "pipe" }, ); const subpaths = [ - "NodeLive", "NodeRedisPool", "Observability", "RedisPool", diff --git a/src/NodeLive.ts b/src/NodeLive.ts deleted file mode 100644 index e44b316..0000000 --- a/src/NodeLive.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * The standard Node.js live graph: node-redis, NodeCrypto, and the task - * engine. - * - * @module - */ - -import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; -import type * as Crypto from "effect/Crypto"; -import * as Layer from "effect/Layer"; -import type * as Redis from "effect/unstable/persistence/Redis"; -import * as NodeRedisPool from "./NodeRedisPool.js"; -import type { RedisConnectionRoles, RedisPool } from "./RedisPool.js"; -import * as TaskEngine from "./TaskEngine.js"; - -/** - * Configuration for the standard Node.js live service graph. - * - * @category Configuration - * @since 0.3.0 - */ -export interface LiveConfig { - readonly engine?: TaskEngine.TaskEngineConfig; - readonly redis?: NodeRedisPool.RedisConfig; -} - -/** - * Provides a complete Node.js live graph: Redis connections, connection - * roles and health, Crypto, and the task engine. - * - * **Example: Run a program on Node** - * - * ```ts - * import { Effect } from "effect" - * import { NodeRuntime } from "@effect/platform-node" - * import { NodeLive } from "@effectmq/core" - * - * const program = Effect.void - * program.pipe( - * Effect.provide(NodeLive.layer({ redis: { url: "redis://127.0.0.1:6379" } })), - * NodeRuntime.runMain, - * ) - * ``` - * - * @category Layers - * @since 0.3.0 - */ -export const layer = ( - config: LiveConfig = {}, -): Layer.Layer< - | TaskEngine.TaskEngine - | RedisPool - | RedisConnectionRoles - | NodeRedisPool.RedisConnectionHealth - | Redis.Redis - | Crypto.Crypto, - | TaskEngine.TaskEngineConfigurationError - | Redis.RedisError - | NodeRedisPool.UnsupportedRedisTopology - | NodeRedisPool.InvalidRedisConfiguration -> => - TaskEngine.layer(config.engine).pipe( - Layer.provideMerge( - Layer.merge(NodeRedisPool.layer(config.redis), NodeCrypto.layer), - ), - ); diff --git a/src/PublicContracts.test.ts b/src/PublicContracts.test.ts index 5084318..d017f10 100644 --- a/src/PublicContracts.test.ts +++ b/src/PublicContracts.test.ts @@ -7,7 +7,6 @@ import * as Schema from "effect/Schema"; import type * as Stream from "effect/Stream"; import type * as Redis from "effect/unstable/persistence/Redis"; import { expect, it } from "vitest"; -import * as NodeLive from "./NodeLive.js"; import * as NodeRedisPool from "./NodeRedisPool.js"; import type * as RedisPool from "./RedisPool.js"; import * as TaskEngine from "./TaskEngine.js"; @@ -154,23 +153,19 @@ const compilePublicContracts = () => { >; const layerNoDeps = TaskEngine.layerNoDeps(); - const engineLayer = TaskEngine.layer(); - const nodeLiveLayer = NodeLive.layer(); - const bunNodeRedisLayer = TaskEngine.layer().pipe( + const liveLayer = TaskEngine.layer(); + const bunNodeRedisLayer = TaskEngine.layerNoDeps().pipe( Layer.provideMerge(Layer.merge(NodeRedisPool.layer(), BunCrypto.layer)), ); type LayerNoDepsRequirement = Expect< Equal, RedisPool.RedisPool> >; - type EngineLayerRequirement = Expect< - Equal, RedisPool.RedisPool> + type LiveLayerRequirement = Expect< + Equal, never> >; - type NodeLiveRequirement = Expect< - Equal, never> - >; - type NodeLiveSuccess = Expect< + type LiveLayerSuccess = Expect< Equal< - Layer.Success, + Layer.Success, | TaskEngine.TaskEngine | RedisPool.RedisPool | RedisPool.RedisConnectionRoles @@ -205,9 +200,8 @@ const compilePublicContracts = () => { | RejectAnyServices | FailedEventError | LayerNoDepsRequirement - | EngineLayerRequirement - | NodeLiveRequirement - | NodeLiveSuccess + | LiveLayerRequirement + | LiveLayerSuccess | BunNodeRedisRequirement; }; diff --git a/src/TaskEngine.ts b/src/TaskEngine.ts index 6f378c0..cbe0494 100644 --- a/src/TaskEngine.ts +++ b/src/TaskEngine.ts @@ -9,6 +9,7 @@ * @module */ +import * as NodeCrypto from "@effect/platform-node/NodeCrypto"; import * as Context from "effect/Context"; import * as Crypto from "effect/Crypto"; import * as Data from "effect/Data"; @@ -19,6 +20,7 @@ import * as Metric from "effect/Metric"; import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; import * as Stream from "effect/Stream"; +import type * as Redis from "effect/unstable/persistence/Redis"; import { type EngineTask, type EngineTaskInsert, @@ -29,7 +31,12 @@ import { import taskEngineScript from "./lua/taskEngine.js"; import { UnknownFromMsgpack } from "./MessagePack.js"; import * as Observability from "./Observability.js"; -import { RedisPool, type RedisPoolService } from "./RedisPool.js"; +import * as NodeRedisPool from "./NodeRedisPool.js"; +import { + type RedisConnectionRoles, + RedisPool, + type RedisPoolService, +} from "./RedisPool.js"; import { type Event, EventSchema } from "./TaskEvent.js"; const TypeId = "~effectmq/TaskEngine" as const; @@ -1345,17 +1352,48 @@ export const make = (config?: TaskEngineConfig) => /** * Provides {@link TaskEngine} from an ambient {@link RedisPool}. + * Use this for custom Redis implementations, tests, and Bun plus node-redis. * * @category Layers * @since 0.1.0 */ -export const layer = (config?: TaskEngineConfig) => +export const layerNoDeps = (config?: TaskEngineConfig) => Layer.effect(TaskEngine, make(config)); /** - * Alias of {@link layer}. + * Configuration for the standard live service graph. + * + * @category Configuration + * @since 0.1.0 + */ +export interface LiveConfig { + readonly engine?: TaskEngineConfig; + readonly redis?: NodeRedisPool.RedisConfig; +} + +/** + * Provides the live graph: Redis connections, connection roles and health, + * Crypto, and the task engine. * * @category Layers * @since 0.1.0 */ -export const layerNoDeps = layer; +export const layer = ( + config: LiveConfig = {}, +): Layer.Layer< + | TaskEngine + | RedisPool + | RedisConnectionRoles + | NodeRedisPool.RedisConnectionHealth + | Redis.Redis + | Crypto.Crypto, + | TaskEngineConfigurationError + | Redis.RedisError + | NodeRedisPool.UnsupportedRedisTopology + | NodeRedisPool.InvalidRedisConfiguration +> => + layerNoDeps(config.engine).pipe( + Layer.provideMerge( + Layer.merge(NodeRedisPool.layer(config.redis), NodeCrypto.layer), + ), + ); diff --git a/src/TaskQueue.ts b/src/TaskQueue.ts index 87e8455..5d55284 100644 --- a/src/TaskQueue.ts +++ b/src/TaskQueue.ts @@ -457,7 +457,7 @@ const hasBuiltInErrorTag = (value: unknown): boolean => * * ```ts * import { Effect, Schema } from "effect" - * import { NodeLive, Task, TaskQueue } from "@effectmq/core" + * import { Task, TaskEngine, TaskQueue } from "@effectmq/core" * * const resize = Task.make({ * name: "resize-image", @@ -471,7 +471,7 @@ const hasBuiltInErrorTag = (value: unknown): boolean => * return yield* TaskQueue.offer(images, { imageId: "img-42" }).pipe( * Effect.map(({ handle }) => handle) * ) - * }).pipe(Effect.provide(NodeLive.layer())) + * }).pipe(Effect.provide(TaskEngine.layer())) * ``` * * @category Operations diff --git a/src/index.ts b/src/index.ts index 2c0db4e..7dae9ba 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,13 +8,6 @@ * @module */ -/** - * Zero-requirement Node live graph: NodeRedisPool, NodeCrypto, and the engine. - * - * @category Modules - * @since 0.3.0 - */ -export * as NodeLive from "./NodeLive.js"; /** * Scoped node-redis adapters for standalone Redis and Sentinel. *