@durable-effect/task ships with in-memory implementations of the Storage and Alarm services so you can test task logic without deploying to Cloudflare.
Test stack overview
| Component | Returns | Purpose |
|---|
makeInMemoryStorage() | { layer: Layer<Storage>, handle: InMemoryStorageHandle } | In-memory key-value storage with an inspection handle |
makeInMemoryAlarm() | { layer: Layer<Alarm>, handle: InMemoryAlarmHandle } | In-memory alarm with an inspection handle |
buildRegistryLayer(config) | Layer<TaskRegistry> | Builds a registry from a name-to-task map |
registerTask(definition) | RegisteredTask | Registers a definition whose R is already never |
registerTaskWithLayer(def, layer) | RegisteredTask | Registers a definition that requires a service layer |
TaskRunnerLive | Layer<TaskRunner, never, TaskRegistry | Storage | Alarm> | Wires TaskRunner to the registry, storage, and alarm |
Building a test stack
Call makeInMemoryStorage() and makeInMemoryAlarm() to get layers and handles, then assemble them with buildRegistryLayer and TaskRunnerLive:
import {
TaskRunnerLive,
makeInMemoryStorage,
makeInMemoryAlarm,
buildRegistryLayer,
registerTask,
} from "@durable-effect/task"
import { Layer } from "effect"
function makeTestStack() {
const { layer: storageLayer, handle: storageHandle } = makeInMemoryStorage()
const { layer: alarmLayer, handle: alarmHandle } = makeInMemoryAlarm()
const registryLayer = buildRegistryLayer({
counter: registerTask(counterTask),
})
const fullLayer = Layer.provide(
TaskRunnerLive,
Layer.mergeAll(registryLayer, storageLayer, alarmLayer),
)
return { fullLayer, storageHandle, alarmHandle }
}
Running the TaskRunner
Provide the full layer to your test program and use TaskRunner to dispatch events or trigger alarms:
import { Effect } from "effect"
import { TaskRunner } from "@durable-effect/task"
const program = Effect.gen(function* () {
const runner = yield* TaskRunner
yield* runner.handleEvent("counter", "c-1", { _tag: "Increment", amount: 5 })
yield* runner.handleEvent("counter", "c-1", { _tag: "Increment", amount: 3 })
})
await Effect.runPromise(Effect.provide(program, fullLayer))
TaskRunner methods
| Method | Signature | Description |
|---|
handleEvent(name, id, event) | (name: string, id: string, event: unknown) => Effect<void, TaskNotFoundError | TaskValidationError | TaskExecutionError> | Dispatch an event to a named task instance. |
handleAlarm(name, id) | (name: string, id: string) => Effect<void, TaskNotFoundError | TaskExecutionError> | Trigger the alarm handler for a named task instance. |
Inspecting state and alarms
After running your program, use the handles to assert on stored state and alarm scheduling.
InMemoryStorageHandle
| Method | Returns | Description |
|---|
getData() | Map<string, unknown> | Returns the full in-memory storage map. State is stored under the key "t:state". |
has(key) | boolean | Check whether a key exists in storage. |
keys() | ReadonlyArray<string> | List all stored keys. |
clear() | void | Clear all stored data. |
InMemoryAlarmHandle
| Method | Returns | Description |
|---|
isScheduled() | boolean | Whether an alarm is currently scheduled. |
getScheduledTime() | number | null | The scheduled alarm timestamp in milliseconds, or null if none. |
clear() | void | Clear the scheduled alarm. |
Complete example
import { Effect, Layer, Schema } from "effect"
import {
Task,
TaskRunner,
registerTask,
buildRegistryLayer,
TaskRunnerLive,
makeInMemoryStorage,
makeInMemoryAlarm,
} from "@durable-effect/task"
// -- Define the task --
const CounterState = Schema.Struct({ count: Schema.Number })
const IncrementEvent = Schema.Struct({
_tag: Schema.Literal("Increment"),
amount: Schema.Number,
})
const counterTask = Task.define({
state: CounterState,
event: IncrementEvent,
onEvent: (ctx, event) =>
Effect.gen(function* () {
const current = yield* ctx.recall()
const count = current ? current.count : 0
yield* ctx.save({ count: count + event.amount })
}),
onAlarm: () => Effect.void,
})
// -- Build the test stack --
function makeTestStack() {
const { layer: storageLayer, handle: storageHandle } = makeInMemoryStorage()
const { layer: alarmLayer, handle: alarmHandle } = makeInMemoryAlarm()
const registryLayer = buildRegistryLayer({
counter: registerTask(counterTask),
})
const fullLayer = Layer.provide(
TaskRunnerLive,
Layer.mergeAll(registryLayer, storageLayer, alarmLayer),
)
return { fullLayer, storageHandle, alarmHandle }
}
// -- Run the test --
const { fullLayer, storageHandle, alarmHandle } = makeTestStack()
const program = Effect.gen(function* () {
const runner = yield* TaskRunner
yield* runner.handleEvent("counter", "c-1", { _tag: "Increment", amount: 5 })
yield* runner.handleEvent("counter", "c-1", { _tag: "Increment", amount: 3 })
})
await Effect.runPromise(Effect.provide(program, fullLayer))
// Assert on stored state
storageHandle.getData().get("t:state") // => { count: 8 }
// Assert on alarm state
alarmHandle.isScheduled() // => false (no alarm was set in this task)
alarmHandle.getScheduledTime() // => null
registerTask(definition) requires that the definition’s R type parameter is already never. If your task depends on services, use registerTaskWithLayer(definition, layer) or call withServices(definition, layer) before registering.
Tasks with services in tests
For tasks that use Effect services, provide the layer at registration time using registerTaskWithLayer:
import { registerTaskWithLayer, buildRegistryLayer } from "@durable-effect/task"
const registryLayer = buildRegistryLayer({
myTask: registerTaskWithLayer(myTask, MyServiceLive),
})