@effect/platform-cloudflare 0.0.0 → 4.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +434 -0
- package/CLAUDE.md +434 -0
- package/LICENSE +21 -0
- package/README.md +506 -0
- package/ai-docs/README.md +44 -0
- package/ai-docs/package.json +36 -0
- package/ai-docs/src/01_effect/01_basics/01_effect-gen.ts +30 -0
- package/ai-docs/src/01_effect/01_basics/02_effect-fn.ts +52 -0
- package/ai-docs/src/01_effect/01_basics/10_creating-effects.ts +74 -0
- package/ai-docs/src/01_effect/01_basics/index.md +7 -0
- package/ai-docs/src/01_effect/02_schema/10_schema-basics.ts +43 -0
- package/ai-docs/src/01_effect/02_schema/index.md +7 -0
- package/ai-docs/src/01_effect/03_services/01_service.ts +45 -0
- package/ai-docs/src/01_effect/03_services/10_reference.ts +10 -0
- package/ai-docs/src/01_effect/03_services/20_layer-composition.ts +70 -0
- package/ai-docs/src/01_effect/03_services/20_layer-unwrap.ts +66 -0
- package/ai-docs/src/01_effect/03_services/index.md +5 -0
- package/ai-docs/src/01_effect/04_errors/01_error-handling.ts +30 -0
- package/ai-docs/src/01_effect/04_errors/10_catch-tags.ts +24 -0
- package/ai-docs/src/01_effect/04_errors/20_reason-errors.ts +64 -0
- package/ai-docs/src/01_effect/04_errors/index.md +1 -0
- package/ai-docs/src/01_effect/05_resources/10_acquire-release.ts +105 -0
- package/ai-docs/src/01_effect/05_resources/20_layer-side-effects.ts +31 -0
- package/ai-docs/src/01_effect/05_resources/30_layer-map.ts +86 -0
- package/ai-docs/src/01_effect/05_resources/index.md +3 -0
- package/ai-docs/src/01_effect/06_running/10_run-main.ts +30 -0
- package/ai-docs/src/01_effect/06_running/20_layer-launch.ts +27 -0
- package/ai-docs/src/01_effect/06_running/index.md +1 -0
- package/ai-docs/src/01_effect/07_pubsub/10_pubsub.ts +56 -0
- package/ai-docs/src/01_effect/07_pubsub/index.md +3 -0
- package/ai-docs/src/03_stream/10_creating-streams.ts +103 -0
- package/ai-docs/src/03_stream/20_consuming-streams.ts +137 -0
- package/ai-docs/src/03_stream/30_encoding.ts +163 -0
- package/ai-docs/src/03_stream/index.md +4 -0
- package/ai-docs/src/04_integration/10_managed-runtime.ts +129 -0
- package/ai-docs/src/04_integration/index.md +5 -0
- package/ai-docs/src/05_batching/10_request-resolver.ts +89 -0
- package/ai-docs/src/05_batching/index.md +3 -0
- package/ai-docs/src/06_schedule/10_schedules.ts +111 -0
- package/ai-docs/src/06_schedule/index.md +3 -0
- package/ai-docs/src/07_datetime/10_creating-and-formatting.ts +30 -0
- package/ai-docs/src/07_datetime/20_time-zones.ts +44 -0
- package/ai-docs/src/07_datetime/index.md +5 -0
- package/ai-docs/src/08_observability/10_logging.ts +66 -0
- package/ai-docs/src/08_observability/20_otlp-tracing.ts +95 -0
- package/ai-docs/src/08_observability/index.md +7 -0
- package/ai-docs/src/09_testing/10_effect-tests.ts +55 -0
- package/ai-docs/src/09_testing/20_layer-tests.ts +138 -0
- package/ai-docs/src/09_testing/index.md +1 -0
- package/ai-docs/src/10_predicate/01_basics.ts +14 -0
- package/ai-docs/src/10_predicate/index.md +9 -0
- package/ai-docs/src/40_sql/10_basics.ts +175 -0
- package/ai-docs/src/40_sql/index.md +6 -0
- package/ai-docs/src/50_http-client/10_basics.ts +102 -0
- package/ai-docs/src/50_http-client/index.md +3 -0
- package/ai-docs/src/51_http-server/10_basics.ts +118 -0
- package/ai-docs/src/51_http-server/20_testing.ts +100 -0
- package/ai-docs/src/51_http-server/fixtures/api/Api.ts +14 -0
- package/ai-docs/src/51_http-server/fixtures/api/Authorization.ts +36 -0
- package/ai-docs/src/51_http-server/fixtures/api/System.ts +10 -0
- package/ai-docs/src/51_http-server/fixtures/api/Users.ts +107 -0
- package/ai-docs/src/51_http-server/fixtures/domain/User.ts +21 -0
- package/ai-docs/src/51_http-server/fixtures/domain/UserErrors.ts +22 -0
- package/ai-docs/src/51_http-server/fixtures/server/Authorization.ts +37 -0
- package/ai-docs/src/51_http-server/fixtures/server/Users/http.ts +80 -0
- package/ai-docs/src/51_http-server/fixtures/server/Users.ts +183 -0
- package/ai-docs/src/51_http-server/index.md +3 -0
- package/ai-docs/src/52_graphql/10_basics.ts +117 -0
- package/ai-docs/src/52_graphql/20_testing.ts +45 -0
- package/ai-docs/src/52_graphql/30_batching.ts +118 -0
- package/ai-docs/src/52_graphql/40_aliased-batching.ts +124 -0
- package/ai-docs/src/52_graphql/50_errors-and-retries.ts +74 -0
- package/ai-docs/src/52_graphql/index.md +6 -0
- package/ai-docs/src/60_child-process/10_working-with-child-processes.ts +117 -0
- package/ai-docs/src/60_child-process/index.md +3 -0
- package/ai-docs/src/70_cli/10_basics.ts +162 -0
- package/ai-docs/src/70_cli/index.md +5 -0
- package/ai-docs/src/71_ai/10_language-model.ts +156 -0
- package/ai-docs/src/71_ai/20_tools.ts +226 -0
- package/ai-docs/src/71_ai/30_chat.ts +158 -0
- package/ai-docs/src/71_ai/fixtures/domain/LaunchPlan.ts +9 -0
- package/ai-docs/src/71_ai/index.md +5 -0
- package/ai-docs/src/80_cluster/10_entities.ts +97 -0
- package/ai-docs/src/80_cluster/index.md +4 -0
- package/ai-docs/src/index.md +10 -0
- package/ai-docs/tsconfig.json +28 -0
- package/dist/AlchemyCloudflareCluster.d.ts +134 -0
- package/dist/AlchemyCloudflareCluster.d.ts.map +1 -0
- package/dist/AlchemyCloudflareCluster.js +111 -0
- package/dist/AlchemyCloudflareCluster.js.map +1 -0
- package/dist/CloudflareCluster.d.ts +196 -0
- package/dist/CloudflareCluster.d.ts.map +1 -0
- package/dist/CloudflareCluster.js +533 -0
- package/dist/CloudflareCluster.js.map +1 -0
- package/dist/CloudflareDurableObjectPrograms.d.ts +279 -0
- package/dist/CloudflareDurableObjectPrograms.d.ts.map +1 -0
- package/dist/CloudflareDurableObjectPrograms.js +295 -0
- package/dist/CloudflareDurableObjectPrograms.js.map +1 -0
- package/dist/CloudflareDurableObjects.d.ts +149 -0
- package/dist/CloudflareDurableObjects.d.ts.map +1 -0
- package/dist/CloudflareDurableObjects.js +337 -0
- package/dist/CloudflareDurableObjects.js.map +1 -0
- package/dist/CloudflarePersistedQueue.d.ts +36 -0
- package/dist/CloudflarePersistedQueue.d.ts.map +1 -0
- package/dist/CloudflarePersistedQueue.js +122 -0
- package/dist/CloudflarePersistedQueue.js.map +1 -0
- package/dist/CloudflareWorkflowEngine.d.ts +72 -0
- package/dist/CloudflareWorkflowEngine.d.ts.map +1 -0
- package/dist/CloudflareWorkflowEngine.js +187 -0
- package/dist/CloudflareWorkflowEngine.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/alchemyCluster.d.ts +2 -0
- package/dist/internal/alchemyCluster.d.ts.map +1 -0
- package/dist/internal/alchemyCluster.js +75 -0
- package/dist/internal/alchemyCluster.js.map +1 -0
- package/dist/internal/boundedMap.d.ts +3 -0
- package/dist/internal/boundedMap.d.ts.map +1 -0
- package/dist/internal/boundedMap.js +15 -0
- package/dist/internal/boundedMap.js.map +1 -0
- package/dist/internal/cluster.d.ts +57 -0
- package/dist/internal/cluster.d.ts.map +1 -0
- package/dist/internal/cluster.js +2 -0
- package/dist/internal/cluster.js.map +1 -0
- package/dist/internal/clusterName.d.ts +2 -0
- package/dist/internal/clusterName.d.ts.map +1 -0
- package/dist/internal/clusterName.js +16 -0
- package/dist/internal/clusterName.js.map +1 -0
- package/dist/internal/entityKeepAlive.d.ts +2 -0
- package/dist/internal/entityKeepAlive.d.ts.map +1 -0
- package/dist/internal/entityKeepAlive.js +73 -0
- package/dist/internal/entityKeepAlive.js.map +1 -0
- package/dist/internal/entityMailbox.d.ts +2 -0
- package/dist/internal/entityMailbox.d.ts.map +1 -0
- package/dist/internal/entityMailbox.js +160 -0
- package/dist/internal/entityMailbox.js.map +1 -0
- package/dist/internal/entityRegistry.d.ts +16 -0
- package/dist/internal/entityRegistry.d.ts.map +1 -0
- package/dist/internal/entityRegistry.js +22 -0
- package/dist/internal/entityRegistry.js.map +1 -0
- package/dist/internal/entityReply.d.ts +2 -0
- package/dist/internal/entityReply.d.ts.map +1 -0
- package/dist/internal/entityReply.js +35 -0
- package/dist/internal/entityReply.js.map +1 -0
- package/dist/internal/entityRuntime.d.ts +2 -0
- package/dist/internal/entityRuntime.d.ts.map +1 -0
- package/dist/internal/entityRuntime.js +823 -0
- package/dist/internal/entityRuntime.js.map +1 -0
- package/dist/internal/entitySql.d.ts +2 -0
- package/dist/internal/entitySql.d.ts.map +1 -0
- package/dist/internal/entitySql.js +29 -0
- package/dist/internal/entitySql.js.map +1 -0
- package/dist/internal/entityStorage.d.ts +2 -0
- package/dist/internal/entityStorage.d.ts.map +1 -0
- package/dist/internal/entityStorage.js +98 -0
- package/dist/internal/entityStorage.js.map +1 -0
- package/dist/internal/entityWire.d.ts +2 -0
- package/dist/internal/entityWire.d.ts.map +1 -0
- package/dist/internal/entityWire.js +146 -0
- package/dist/internal/entityWire.js.map +1 -0
- package/dist/internal/queueRuntime.d.ts +2 -0
- package/dist/internal/queueRuntime.d.ts.map +1 -0
- package/dist/internal/queueRuntime.js +88 -0
- package/dist/internal/queueRuntime.js.map +1 -0
- package/dist/internal/queueStorage.d.ts +2 -0
- package/dist/internal/queueStorage.d.ts.map +1 -0
- package/dist/internal/queueStorage.js +129 -0
- package/dist/internal/queueStorage.js.map +1 -0
- package/dist/internal/registry.d.ts +10 -0
- package/dist/internal/registry.d.ts.map +1 -0
- package/dist/internal/registry.js +24 -0
- package/dist/internal/registry.js.map +1 -0
- package/dist/internal/singletonRegistry.d.ts +2 -0
- package/dist/internal/singletonRegistry.d.ts.map +1 -0
- package/dist/internal/singletonRegistry.js +9 -0
- package/dist/internal/singletonRegistry.js.map +1 -0
- package/dist/internal/singletonRuntime.d.ts +2 -0
- package/dist/internal/singletonRuntime.d.ts.map +1 -0
- package/dist/internal/singletonRuntime.js +42 -0
- package/dist/internal/singletonRuntime.js.map +1 -0
- package/dist/internal/singletonStorage.d.ts +2 -0
- package/dist/internal/singletonStorage.d.ts.map +1 -0
- package/dist/internal/singletonStorage.js +34 -0
- package/dist/internal/singletonStorage.js.map +1 -0
- package/dist/internal/transport.d.ts +2 -0
- package/dist/internal/transport.d.ts.map +1 -0
- package/dist/internal/transport.js +49 -0
- package/dist/internal/transport.js.map +1 -0
- package/dist/internal/workflowRegistry.d.ts +2 -0
- package/dist/internal/workflowRegistry.d.ts.map +1 -0
- package/dist/internal/workflowRegistry.js +29 -0
- package/dist/internal/workflowRegistry.js.map +1 -0
- package/dist/internal/workflowRuntime.d.ts +2 -0
- package/dist/internal/workflowRuntime.d.ts.map +1 -0
- package/dist/internal/workflowRuntime.js +322 -0
- package/dist/internal/workflowRuntime.js.map +1 -0
- package/dist/internal/workflowStorage.d.ts +2 -0
- package/dist/internal/workflowStorage.d.ts.map +1 -0
- package/dist/internal/workflowStorage.js +113 -0
- package/dist/internal/workflowStorage.js.map +1 -0
- package/dist/internal/workflowWire.d.ts +2 -0
- package/dist/internal/workflowWire.d.ts.map +1 -0
- package/dist/internal/workflowWire.js +52 -0
- package/dist/internal/workflowWire.js.map +1 -0
- package/package.json +77 -1
- package/src/AlchemyCloudflareCluster.ts +200 -0
- package/src/CloudflareCluster.ts +704 -0
- package/src/CloudflareDurableObjectPrograms.ts +543 -0
- package/src/CloudflareDurableObjects.ts +430 -0
- package/src/CloudflarePersistedQueue.ts +189 -0
- package/src/CloudflareWorkflowEngine.ts +220 -0
- package/src/index.ts +38 -0
- package/src/internal/alchemyCluster.ts +120 -0
- package/src/internal/boundedMap.ts +15 -0
- package/src/internal/cluster.ts +57 -0
- package/src/internal/clusterName.ts +19 -0
- package/src/internal/entityKeepAlive.ts +121 -0
- package/src/internal/entityMailbox.ts +324 -0
- package/src/internal/entityRegistry.ts +46 -0
- package/src/internal/entityReply.ts +54 -0
- package/src/internal/entityRuntime.ts +1318 -0
- package/src/internal/entitySql.ts +60 -0
- package/src/internal/entityStorage.ts +168 -0
- package/src/internal/entityWire.ts +204 -0
- package/src/internal/queueRuntime.ts +174 -0
- package/src/internal/queueStorage.ts +255 -0
- package/src/internal/registry.ts +31 -0
- package/src/internal/singletonRegistry.ts +21 -0
- package/src/internal/singletonRuntime.ts +88 -0
- package/src/internal/singletonStorage.ts +64 -0
- package/src/internal/transport.ts +67 -0
- package/src/internal/workflowRegistry.ts +78 -0
- package/src/internal/workflowRuntime.ts +443 -0
- package/src/internal/workflowStorage.ts +234 -0
- package/src/internal/workflowWire.ts +123 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Using ManagedRuntime with Hono
|
|
3
|
+
*
|
|
4
|
+
* Use `ManagedRuntime` to run Effect programs from external frameworks while keeping your domain logic in services and Layers.
|
|
5
|
+
*/
|
|
6
|
+
import { Context, Effect, Layer, ManagedRuntime, Ref, Schema } from "effect"
|
|
7
|
+
import { Hono } from "hono"
|
|
8
|
+
|
|
9
|
+
class Todo extends Schema.Class<Todo>("Todo")({
|
|
10
|
+
id: Schema.Int,
|
|
11
|
+
title: Schema.String,
|
|
12
|
+
completed: Schema.Boolean
|
|
13
|
+
}) {}
|
|
14
|
+
|
|
15
|
+
class CreateTodoPayload extends Schema.Class<CreateTodoPayload>("CreateTodoPayload")({
|
|
16
|
+
title: Schema.String
|
|
17
|
+
}) {}
|
|
18
|
+
|
|
19
|
+
class TodoNotFound extends Schema.TaggedError<TodoNotFound>()("TodoNotFound", {
|
|
20
|
+
id: Schema.Int
|
|
21
|
+
}) {}
|
|
22
|
+
|
|
23
|
+
export class TodoRepo extends Context.Service<TodoRepo, {
|
|
24
|
+
readonly getAll: Effect.Effect<ReadonlyArray<Todo>>
|
|
25
|
+
getById(id: number): Effect.Effect<Todo, TodoNotFound>
|
|
26
|
+
create(payload: CreateTodoPayload): Effect.Effect<Todo>
|
|
27
|
+
}>()("app/TodoRepo") {
|
|
28
|
+
static readonly layer = Layer.effect(
|
|
29
|
+
TodoRepo,
|
|
30
|
+
Effect.gen(function*() {
|
|
31
|
+
const store = new Map<number, Todo>()
|
|
32
|
+
const nextId = yield* Ref.make(1)
|
|
33
|
+
|
|
34
|
+
const getAll = Effect.gen(function*() {
|
|
35
|
+
return Array.from(store.values())
|
|
36
|
+
}).pipe(
|
|
37
|
+
Effect.withSpan("TodoRepo.getAll")
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
const getById = Effect.fn("TodoRepo.getById")(function*(id: number) {
|
|
41
|
+
const todo = store.get(id)
|
|
42
|
+
if (todo === undefined) {
|
|
43
|
+
return yield* new TodoNotFound({ id })
|
|
44
|
+
}
|
|
45
|
+
return todo
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const create = Effect.fn("TodoRepo.create")(function*(payload: CreateTodoPayload) {
|
|
49
|
+
const id = yield* Ref.getAndUpdate(nextId, (current) => current + 1)
|
|
50
|
+
const todo = new Todo({ id, title: payload.title, completed: false })
|
|
51
|
+
store.set(id, todo)
|
|
52
|
+
return todo
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
return TodoRepo.of({ getAll, getById, create })
|
|
56
|
+
})
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Create a global memo map that can be shared across the app. This is necessary
|
|
61
|
+
// for memoization to work correctly across ManagedRuntime instances.
|
|
62
|
+
export const appMemoMap = Layer.makeMemoMapUnsafe()
|
|
63
|
+
|
|
64
|
+
// Create a ManagedRuntime for the TodoRepo layer. This runtime can be shared
|
|
65
|
+
// across all handlers in the app, and it will manage the lifecycle of the
|
|
66
|
+
// TodoRepo service and any resources it uses.
|
|
67
|
+
export const runtime = ManagedRuntime.make(TodoRepo.layer, {
|
|
68
|
+
memoMap: appMemoMap
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
export const app = new Hono()
|
|
72
|
+
|
|
73
|
+
app.get("/todos", async (context) => {
|
|
74
|
+
const todos = await runtime.runPromise(
|
|
75
|
+
TodoRepo.use((repo) => repo.getAll)
|
|
76
|
+
)
|
|
77
|
+
return context.json(todos)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
app.get("/todos/:id", async (context) => {
|
|
81
|
+
const id = Number(context.req.param("id"))
|
|
82
|
+
if (!Number.isFinite(id)) {
|
|
83
|
+
return context.json({ message: "Todo id must be a number" }, 400)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const todo = await runtime.runPromise(
|
|
87
|
+
TodoRepo.use((repo) => repo.getById(id)).pipe(
|
|
88
|
+
Effect.catchTag("TodoNotFound", () => Effect.succeed(null))
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
if (todo === null) {
|
|
93
|
+
return context.json({ message: "Todo not found" }, 404)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return context.json(todo)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const decodeCreateTodoPayload = Schema.decodeUnknownSync(CreateTodoPayload)
|
|
100
|
+
|
|
101
|
+
app.post("/todos", async (context) => {
|
|
102
|
+
const body = await context.req.json()
|
|
103
|
+
|
|
104
|
+
let payload: CreateTodoPayload
|
|
105
|
+
try {
|
|
106
|
+
payload = decodeCreateTodoPayload(body)
|
|
107
|
+
} catch {
|
|
108
|
+
return context.json({ message: "Invalid request body" }, 400)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const todo = await runtime.runPromise(
|
|
112
|
+
TodoRepo.use((repo) => repo.create(payload))
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return context.json(todo, 201)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
// The same bridge pattern works for Express, Fastify, Koa, and other frameworks.
|
|
119
|
+
// Use `runtime.runSync` for synchronous edges or `runtime.runCallback` for
|
|
120
|
+
// callback-only APIs.
|
|
121
|
+
|
|
122
|
+
// When the process receives a shutdown signal, dispose the runtime to clean up
|
|
123
|
+
// any resources used by the TodoRepo service and its dependencies.
|
|
124
|
+
const shutdown = () => {
|
|
125
|
+
void runtime.dispose()
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
process.once("SIGINT", shutdown)
|
|
129
|
+
process.once("SIGTERM", shutdown)
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
## Integrating Effect into existing applications
|
|
2
|
+
|
|
3
|
+
`ManagedRuntime` bridges Effect programs with non-Effect code. Build one runtime
|
|
4
|
+
from your application Layer, then use it anywhere you need imperative execution,
|
|
5
|
+
like web handlers, framework hooks, worker queues, or legacy callback APIs.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Batching requests with RequestResolver
|
|
3
|
+
*
|
|
4
|
+
* Define request types with `Request.Class`, resolve them in batches with `RequestResolver`.
|
|
5
|
+
*/
|
|
6
|
+
import { Context, Effect, Exit, Layer, Request, RequestResolver, Schema, Tracer } from "effect"
|
|
7
|
+
|
|
8
|
+
export class User extends Schema.Class<User>("User")({
|
|
9
|
+
id: Schema.Int,
|
|
10
|
+
name: Schema.String,
|
|
11
|
+
email: Schema.String
|
|
12
|
+
}) {}
|
|
13
|
+
|
|
14
|
+
export class UserNotFound extends Schema.TaggedError<UserNotFound>()("UserNotFound", {
|
|
15
|
+
id: Schema.Int
|
|
16
|
+
}) {}
|
|
17
|
+
|
|
18
|
+
export class Users extends Context.Service<Users, {
|
|
19
|
+
getUserById(id: number): Effect.Effect<User, UserNotFound>
|
|
20
|
+
}>()("app/Users") {
|
|
21
|
+
static readonly layer = Layer.effect(
|
|
22
|
+
Users,
|
|
23
|
+
Effect.gen(function*() {
|
|
24
|
+
// Request classes model a single external lookup.
|
|
25
|
+
class GetUserById extends Request.Class<
|
|
26
|
+
{ readonly id: number },
|
|
27
|
+
User, // The success type of the request
|
|
28
|
+
UserNotFound, // The error type of the request
|
|
29
|
+
never // The requirements type of the request, if any
|
|
30
|
+
> {}
|
|
31
|
+
|
|
32
|
+
// Simulate an external data source that supports batched lookup.
|
|
33
|
+
const usersTable = new Map<number, User>([
|
|
34
|
+
[1, new User({ id: 1, name: "Ada Lovelace", email: "ada@acme.dev" })],
|
|
35
|
+
[2, new User({ id: 2, name: "Alan Turing", email: "alan@acme.dev" })],
|
|
36
|
+
[3, new User({ id: 3, name: "Grace Hopper", email: "grace@acme.dev" })]
|
|
37
|
+
])
|
|
38
|
+
|
|
39
|
+
const resolver = yield* RequestResolver.make<GetUserById>(Effect.fn(function*(entries) {
|
|
40
|
+
for (const entry of entries) {
|
|
41
|
+
const user = usersTable.get(entry.request.id)
|
|
42
|
+
|
|
43
|
+
// If the request had requirements, you can access them with
|
|
44
|
+
// `entry.context`
|
|
45
|
+
const requestSpan = Context.getOption(entry.context, Tracer.ParentSpan)
|
|
46
|
+
console.log("Request span", requestSpan)
|
|
47
|
+
|
|
48
|
+
if (user) {
|
|
49
|
+
// Complete requests with .completeUnsafe and pass in an Exit value
|
|
50
|
+
entry.completeUnsafe(Exit.succeed(user))
|
|
51
|
+
} else {
|
|
52
|
+
entry.completeUnsafe(Exit.fail(new UserNotFound({ id: entry.request.id })))
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
})).pipe(
|
|
56
|
+
// Control the delay before the resolver is executed. This allows more
|
|
57
|
+
// requests to be batched together, but also adds latency to the first
|
|
58
|
+
// request.
|
|
59
|
+
RequestResolver.setDelay("10 millis"),
|
|
60
|
+
// RequestResolver.withSpan adds a span around the resolver execution,
|
|
61
|
+
// and also sets up span links for each request
|
|
62
|
+
RequestResolver.withSpan("Users.getUserById.resolver"),
|
|
63
|
+
// RequestResolver.withCache adds a simple LRU cache to avoid repeated
|
|
64
|
+
// lookups for the same ID.
|
|
65
|
+
RequestResolver.withCache({ capacity: 1024 })
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
// Wrap the resolver in a service method. The resolver batches calls to
|
|
69
|
+
// `getUserById` that occur within the delay window.
|
|
70
|
+
const getUserById = (id: number) =>
|
|
71
|
+
Effect.request(new GetUserById({ id }), resolver).pipe(
|
|
72
|
+
Effect.withSpan("Users.getUserById", { attributes: { userId: id } })
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return { getUserById } as const
|
|
76
|
+
})
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Run multiple lookups concurrently. The resolver receives one batch and
|
|
81
|
+
// internally deduplicates repeated IDs for the external call.
|
|
82
|
+
export const batchedLookupExample = Effect.gen(function*() {
|
|
83
|
+
const { getUserById } = yield* Users
|
|
84
|
+
|
|
85
|
+
// This will only trigger a single call to the resolver with the unique IDs [1, 2, 3].
|
|
86
|
+
yield* Effect.forEach([1, 2, 1, 3, 2], getUserById, {
|
|
87
|
+
concurrency: "unbounded"
|
|
88
|
+
})
|
|
89
|
+
})
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Working with the Schedule module
|
|
3
|
+
*
|
|
4
|
+
* Build schedules, compose them, and use them with `Effect.retry` and `Effect.repeat`.
|
|
5
|
+
*/
|
|
6
|
+
import { Duration, Effect, Random, Schedule, Schema } from "effect"
|
|
7
|
+
|
|
8
|
+
// Production pattern: capped exponential backoff with jitter and at most 5 retries (6 attempts).
|
|
9
|
+
// Base delays start at 250ms and grow exponentially, capped at 10s before jitter.
|
|
10
|
+
export const productionRetrySchedule = Schedule.min([
|
|
11
|
+
Schedule.exponential("250 millis"),
|
|
12
|
+
// Cap the delay at 10 seconds to avoid excessively long waits.
|
|
13
|
+
Schedule.spaced("10 seconds")
|
|
14
|
+
]).pipe(
|
|
15
|
+
Schedule.jittered,
|
|
16
|
+
Schedule.upTo({ times: 5 }),
|
|
17
|
+
Schedule.setInputType<HttpError>(),
|
|
18
|
+
Schedule.while(({ input }) => input.retryable)
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
export const fetchUserProfile = Effect.fn("fetchUserProfile")(
|
|
22
|
+
function*(userId: string) {
|
|
23
|
+
const random = yield* Random.next
|
|
24
|
+
const status = random > 0.7
|
|
25
|
+
? 200
|
|
26
|
+
: random > 0.3
|
|
27
|
+
? 503
|
|
28
|
+
: 401
|
|
29
|
+
|
|
30
|
+
if (status !== 200) {
|
|
31
|
+
return yield* new HttpError({
|
|
32
|
+
message: `Request for ${userId} failed`,
|
|
33
|
+
status,
|
|
34
|
+
retryable: status >= 500
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
id: userId,
|
|
40
|
+
name: "Ada Lovelace"
|
|
41
|
+
} as const
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
// Use the schedule with `Effect.retry` to retry failures.
|
|
46
|
+
export const loadUserWithRetry = fetchUserProfile("user-123").pipe(
|
|
47
|
+
Effect.retry(productionRetrySchedule),
|
|
48
|
+
// Turn the final error into a defect, either after exhausting the retries
|
|
49
|
+
// or immediately for a non-retryable error.
|
|
50
|
+
Effect.orDie
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
export const loadUserWithInferredInput = fetchUserProfile("user-123").pipe(
|
|
54
|
+
// You can also pass a schedule builder function that assists with inferring
|
|
55
|
+
// the input type. This is especially useful when the schedule needs to
|
|
56
|
+
// inspect the error to determine retryability.
|
|
57
|
+
Effect.retry(($) =>
|
|
58
|
+
$(Schedule.spaced("1 seconds")).pipe(
|
|
59
|
+
Schedule.while(({ input }) => input.retryable)
|
|
60
|
+
)
|
|
61
|
+
),
|
|
62
|
+
Effect.orDie
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
export class HttpError extends Schema.TaggedError<HttpError>()("HttpError", {
|
|
66
|
+
message: Schema.String,
|
|
67
|
+
status: Schema.Int,
|
|
68
|
+
retryable: Schema.Boolean
|
|
69
|
+
}) {}
|
|
70
|
+
|
|
71
|
+
// Start with a few schedule constructors.
|
|
72
|
+
export const maxRetries = Schedule.recurs(5)
|
|
73
|
+
export const spacedPolling = Schedule.spaced("30 seconds")
|
|
74
|
+
export const exponentialBackoff = Schedule.exponential("200 millis")
|
|
75
|
+
|
|
76
|
+
// `Schedule.max` continues only while all schedules continue and outputs
|
|
77
|
+
// the slowest delay. It is useful for combining a delay pattern with a hard
|
|
78
|
+
// attempt cap.
|
|
79
|
+
export const retryBackoffWithLimit = Schedule.max([
|
|
80
|
+
Schedule.exponential("250 millis"),
|
|
81
|
+
Schedule.recurs(6)
|
|
82
|
+
])
|
|
83
|
+
|
|
84
|
+
// `Schedule.min` continues while any schedule continues and outputs
|
|
85
|
+
// the fastest delay. It is useful for fallback behavior.
|
|
86
|
+
//
|
|
87
|
+
// This schedule will start at 500ms, exponentially increase, and will be capped
|
|
88
|
+
// at 2 seconds.
|
|
89
|
+
export const keepTryingUpToTwoSeconds = Schedule.min([
|
|
90
|
+
Schedule.exponential("500 millis"),
|
|
91
|
+
Schedule.spaced("2 seconds")
|
|
92
|
+
])
|
|
93
|
+
|
|
94
|
+
// Use `Schedule.while` to continue only for retryable failures.
|
|
95
|
+
// This lets non-retryable errors fail fast, even if attempts remain.
|
|
96
|
+
export const retryableOnly = Schedule.exponential("200 millis").pipe(
|
|
97
|
+
// You can use `setInputType` to specify the type of input the schedule will
|
|
98
|
+
// receive.
|
|
99
|
+
Schedule.setInputType<HttpError>(),
|
|
100
|
+
Schedule.while(({ input }) => input.retryable)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
// `tap` is useful for performing side effects like logging or metrics.
|
|
104
|
+
export const instrumentedRetrySchedule = retryableOnly.pipe(
|
|
105
|
+
Schedule.setInputType<HttpError>(),
|
|
106
|
+
Schedule.tap((meta) =>
|
|
107
|
+
Effect.logDebug(
|
|
108
|
+
`Retrying after ${meta.input.status}: ${meta.input.message}. Next retry in ${Duration.toMillis(meta.duration)}ms`
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Creating and formatting DateTime values
|
|
3
|
+
*
|
|
4
|
+
* Parse incoming date values safely, use Clock-powered current time, and format
|
|
5
|
+
* instants for API payloads or user-facing labels.
|
|
6
|
+
*/
|
|
7
|
+
import { DateTime, Effect, Option } from "effect"
|
|
8
|
+
|
|
9
|
+
Effect.gen(function*() {
|
|
10
|
+
// Use DateTime.now to get the current time from Effect's Clock service.
|
|
11
|
+
// Using the Clock service ensures tests can use the `TestClock` module to
|
|
12
|
+
// control time.
|
|
13
|
+
const now = yield* DateTime.now
|
|
14
|
+
|
|
15
|
+
// Use DateTime.make to parse a date input, such as a user-entered string or a
|
|
16
|
+
// epoch timestamp. It returns an Option depending on whether the input was
|
|
17
|
+
// valid.
|
|
18
|
+
const parsedOption: Option.Option<DateTime.Utc> = DateTime.make("2024-06-15T14:30:00.000Z")
|
|
19
|
+
|
|
20
|
+
// you can then use the Option apis to unwrap the value
|
|
21
|
+
Option.getOrUndefined(parsedOption)
|
|
22
|
+
|
|
23
|
+
// Calendar/date-time math returns a new DateTime value; the original value is
|
|
24
|
+
// immutable.
|
|
25
|
+
const endsAt = now.pipe(DateTime.add({ hours: 2 }))
|
|
26
|
+
|
|
27
|
+
// The DateTime.format* functions can be used to convert a DateTime value to
|
|
28
|
+
// differen formats.
|
|
29
|
+
yield* Effect.log("ISO string:", DateTime.formatIso(endsAt))
|
|
30
|
+
})
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Working with time zones
|
|
3
|
+
*
|
|
4
|
+
* Attach IANA zones to instants, render zoned ISO strings, and provide a
|
|
5
|
+
* CurrentTimeZone service for code that should use the workspace/user zone.
|
|
6
|
+
*/
|
|
7
|
+
import { NodeRuntime } from "@effect/platform-node"
|
|
8
|
+
import { DateTime, Effect, Option } from "effect"
|
|
9
|
+
|
|
10
|
+
Effect.gen(function*() {
|
|
11
|
+
// Use DateTime.now to get the current time from Effect's Clock service.
|
|
12
|
+
const now = yield* DateTime.now
|
|
13
|
+
|
|
14
|
+
// To attach a named IANA zone to a DateTime value
|
|
15
|
+
const nowInAuckland = now.pipe(
|
|
16
|
+
// Use DateTime.setZoneNamedUnsafe when you know the zone is valid.
|
|
17
|
+
DateTime.setZoneNamedUnsafe("Pacific/Auckland")
|
|
18
|
+
)
|
|
19
|
+
yield* Effect.log("Now in Auckland:", nowInAuckland)
|
|
20
|
+
|
|
21
|
+
// Use DateTime.setZoneNamed when you don't know the zone is valid.
|
|
22
|
+
const nowInSydneyOption: Option.Option<DateTime.Zoned> = now.pipe(
|
|
23
|
+
DateTime.setZoneNamed("Australia/Sydney")
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
yield* Effect.log("Now in Sydney:", Option.getOrUndefined(nowInSydneyOption))
|
|
27
|
+
|
|
28
|
+
// To generate a `DateTime.Zoned` in the `DateTime.CurrentTimeZone`
|
|
29
|
+
const nowInNewYork = yield* DateTime.nowInCurrentZone
|
|
30
|
+
yield* Effect.log("Now in New York:", nowInNewYork)
|
|
31
|
+
|
|
32
|
+
// If you have a date string that you know is in a particular IANA zone, you
|
|
33
|
+
// can convert it to a DateTime.Zoned to ensure the instant is correct
|
|
34
|
+
const dateInAuckland: DateTime.Zoned = DateTime.makeZonedUnsafe("2026-06-05", {
|
|
35
|
+
timeZone: "Pacific/Auckland",
|
|
36
|
+
// adjustForTimeZone will adjust the input to the given zone, otherwise it
|
|
37
|
+
// will be treated as UTC.
|
|
38
|
+
adjustForTimeZone: true
|
|
39
|
+
})
|
|
40
|
+
yield* Effect.log("Date in Auckland:", dateInAuckland)
|
|
41
|
+
}).pipe(
|
|
42
|
+
Effect.provide(DateTime.layerCurrentZoneNamed("America/New_York")),
|
|
43
|
+
NodeRuntime.runMain
|
|
44
|
+
)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Customizing logging
|
|
3
|
+
*
|
|
4
|
+
* Configure loggers & log-level filtering for production applications.
|
|
5
|
+
*/
|
|
6
|
+
import { NodeFileSystem } from "@effect/platform-node"
|
|
7
|
+
import { Config, Effect, Layer, Logger, References } from "effect"
|
|
8
|
+
|
|
9
|
+
// Build a logger layer that emits one JSON line per log entry.
|
|
10
|
+
export const JsonLoggerLayer = Logger.layer([Logger.consoleJson])
|
|
11
|
+
|
|
12
|
+
// Raise the minimum level to "Warn" to skip debug/info logs.
|
|
13
|
+
export const WarnAndAbove = Layer.succeed(References.MinimumLogLevel, "Warn")
|
|
14
|
+
|
|
15
|
+
// There is a built-in logger for writing to a file
|
|
16
|
+
export const FileLoggerLayer = Logger.layer([
|
|
17
|
+
Logger.toFile(Logger.formatSimple, "app.log")
|
|
18
|
+
]).pipe(
|
|
19
|
+
Layer.provide(NodeFileSystem.layer)
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
// Define a custom logger for app-specific formatting and routing.
|
|
23
|
+
export const appLogger = Effect.gen(function*() {
|
|
24
|
+
// Here you could initialize a connection to an external logging service, set
|
|
25
|
+
// up log file rotation, etc.
|
|
26
|
+
yield* Effect.logDebug("initializing app logger")
|
|
27
|
+
|
|
28
|
+
return yield* Logger.batched(Logger.formatStructured, {
|
|
29
|
+
window: "1 second",
|
|
30
|
+
flush: Effect.fn(function*(batch) {
|
|
31
|
+
// In a real implementation, this is where you would send the batch of log entries to an external logging service or write them to a file.
|
|
32
|
+
console.log(`Flushing ${batch.length} log entries`)
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
export const AppLoggerLayer = Logger.layer([appLogger]).pipe(
|
|
38
|
+
Layer.provideMerge(WarnAndAbove) // Start with "Warn" level for the app logger.
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
// Create a logger layer that uses the default logger for development, and the
|
|
42
|
+
// custom logger for production
|
|
43
|
+
export const LoggerLayer = Layer.unwrap(Effect.gen(function*() {
|
|
44
|
+
const env = yield* Config.String("NODE_ENV").pipe(Config.withDefault("development"))
|
|
45
|
+
if (env === "production") {
|
|
46
|
+
return AppLoggerLayer
|
|
47
|
+
}
|
|
48
|
+
return Logger.layer([Logger.defaultLogger])
|
|
49
|
+
}))
|
|
50
|
+
|
|
51
|
+
// Example effect that logs at various levels during a checkout flow.
|
|
52
|
+
export const logCheckoutFlow = Effect.gen(function*() {
|
|
53
|
+
yield* Effect.logDebug("loading checkout state")
|
|
54
|
+
|
|
55
|
+
yield* Effect.logInfo("validating cart")
|
|
56
|
+
yield* Effect.logWarning("inventory is low for one line item")
|
|
57
|
+
yield* Effect.logError("payment provider timeout")
|
|
58
|
+
}).pipe(
|
|
59
|
+
// Attach structured metadata to all log lines emitted by this effect.
|
|
60
|
+
Effect.annotateLogs({
|
|
61
|
+
service: "checkout-api",
|
|
62
|
+
route: "POST /checkout"
|
|
63
|
+
}),
|
|
64
|
+
// Add a duration span so each log line includes checkout=<N>ms metadata.
|
|
65
|
+
Effect.withLogSpan("checkout")
|
|
66
|
+
)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Setting up tracing with Otlp modules
|
|
3
|
+
*
|
|
4
|
+
* Configure Otlp tracing + log export with a reusable observability layer.
|
|
5
|
+
*/
|
|
6
|
+
import { NodeRuntime } from "@effect/platform-node"
|
|
7
|
+
import { Context, Effect, Layer } from "effect"
|
|
8
|
+
import { FetchHttpClient } from "effect/http"
|
|
9
|
+
import { OtlpLogger, OtlpSerialization, OtlpTracer } from "effect/observability"
|
|
10
|
+
|
|
11
|
+
// Configure OTLP span export.
|
|
12
|
+
export const OtlpTracingLayer = OtlpTracer.layer({
|
|
13
|
+
url: "http://localhost:4318/v1/traces",
|
|
14
|
+
resource: {
|
|
15
|
+
serviceName: "checkout-api",
|
|
16
|
+
serviceVersion: "1.0.0",
|
|
17
|
+
attributes: {
|
|
18
|
+
"deployment.environment": "staging"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
// Configure OTLP log export.
|
|
24
|
+
export const OtlpLoggingLayer = OtlpLogger.layer({
|
|
25
|
+
url: "http://localhost:4318/v1/logs",
|
|
26
|
+
resource: {
|
|
27
|
+
serviceName: "checkout-api",
|
|
28
|
+
serviceVersion: "1.0.0"
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// Reusable app-wide observability layer.
|
|
33
|
+
//
|
|
34
|
+
// - OtlpTracer/OtlpLogger require an OTLP serializer and an HttpClient.
|
|
35
|
+
// - FetchHttpClient.layer provides the HttpClient used by the exporter.
|
|
36
|
+
export const ObservabilityLayer = Layer.merge(OtlpTracingLayer, OtlpLoggingLayer).pipe(
|
|
37
|
+
Layer.provide(OtlpSerialization.layerJson),
|
|
38
|
+
Layer.provide(FetchHttpClient.layer)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
export class Checkout extends Context.Service<Checkout, {
|
|
42
|
+
processCheckout(orderId: string): Effect.Effect<void>
|
|
43
|
+
}>()("acme/Checkout") {
|
|
44
|
+
static readonly layer = Layer.effect(
|
|
45
|
+
Checkout,
|
|
46
|
+
Effect.gen(function*() {
|
|
47
|
+
yield* Effect.logInfo("setting up checkout service")
|
|
48
|
+
|
|
49
|
+
return Checkout.of({
|
|
50
|
+
processCheckout: Effect.fn("Checkout.processCheckout")(function*(orderId: string) {
|
|
51
|
+
yield* Effect.logInfo("starting checkout", { orderId })
|
|
52
|
+
|
|
53
|
+
yield* Effect.sleep("50 millis").pipe(
|
|
54
|
+
Effect.withSpan("checkout.charge-card"),
|
|
55
|
+
Effect.annotateSpans({
|
|
56
|
+
"checkout.order_id": orderId,
|
|
57
|
+
"checkout.provider": "acme-pay"
|
|
58
|
+
})
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
yield* Effect.sleep("20 millis").pipe(
|
|
62
|
+
Effect.withSpan("checkout.persist-order")
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
yield* Effect.logInfo("checkout completed", { orderId })
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Example usage of the Checkout service.
|
|
73
|
+
const CheckoutTest = Layer.effectDiscard(
|
|
74
|
+
Effect.gen(function*() {
|
|
75
|
+
const checkout = yield* Checkout
|
|
76
|
+
yield* checkout.processCheckout("ord_123")
|
|
77
|
+
}).pipe(
|
|
78
|
+
Effect.withSpan("checkout-test-run")
|
|
79
|
+
)
|
|
80
|
+
).pipe(
|
|
81
|
+
// You can also attach spans to Layers
|
|
82
|
+
Layer.withSpan("checkout-test"),
|
|
83
|
+
Layer.provide(Checkout.layer)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
const Main = CheckoutTest.pipe(
|
|
87
|
+
// Provide the observability layer at the very end, so that all spans created
|
|
88
|
+
// by the app are exported.
|
|
89
|
+
Layer.provide(ObservabilityLayer)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
// Launch the app
|
|
93
|
+
Layer.launch(Main).pipe(
|
|
94
|
+
NodeRuntime.runMain
|
|
95
|
+
)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
## Observability
|
|
2
|
+
|
|
3
|
+
Effect has built-in support for structured logging, distributed tracing, and
|
|
4
|
+
metrics. For exporting telemetry, use the lightweight Otlp modules from
|
|
5
|
+
`effect/observability` in new projects, or use
|
|
6
|
+
`@effect/opentelemetry` NodeSdk when integrating with an existing OpenTelemetry
|
|
7
|
+
setup.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Writing Effect tests with @effect/vitest
|
|
3
|
+
*
|
|
4
|
+
* Using `it.effect` for Effect-based tests.
|
|
5
|
+
*/
|
|
6
|
+
import { assert, describe, it } from "@effect/vitest"
|
|
7
|
+
import { Effect, Fiber, Schema } from "effect"
|
|
8
|
+
import { TestClock } from "effect/testing"
|
|
9
|
+
|
|
10
|
+
describe("@effect/vitest basics", () => {
|
|
11
|
+
it.effect("runs Effect code with assert helpers", () =>
|
|
12
|
+
Effect.gen(function*() {
|
|
13
|
+
const upper = ["ada", "lin"].map((name) => name.toUpperCase())
|
|
14
|
+
assert.deepStrictEqual(upper, ["ADA", "LIN"])
|
|
15
|
+
assert.strictEqual(upper.length, 2)
|
|
16
|
+
assert.isTrue(upper.includes("ADA"))
|
|
17
|
+
}))
|
|
18
|
+
|
|
19
|
+
it.effect.each([
|
|
20
|
+
{ input: " Ada ", expected: "ada" },
|
|
21
|
+
{ input: " Lin ", expected: "lin" },
|
|
22
|
+
{ input: " Nia ", expected: "nia" }
|
|
23
|
+
])("parameterized normalization %#", ({ input, expected }) =>
|
|
24
|
+
Effect.gen(function*() {
|
|
25
|
+
assert.strictEqual(input.trim().toLowerCase(), expected)
|
|
26
|
+
}))
|
|
27
|
+
|
|
28
|
+
it.effect("controls time with TestClock", () =>
|
|
29
|
+
Effect.gen(function*() {
|
|
30
|
+
const fiber = yield* Effect.forkChild(
|
|
31
|
+
Effect.sleep(60_000).pipe(Effect.as("done" as const))
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
// Move virtual time forward to complete sleeping fibers immediately.
|
|
35
|
+
yield* TestClock.adjust(60_000)
|
|
36
|
+
|
|
37
|
+
const value = yield* Fiber.join(fiber)
|
|
38
|
+
assert.strictEqual(value, "done")
|
|
39
|
+
}))
|
|
40
|
+
|
|
41
|
+
it.live("uses real runtime services", () =>
|
|
42
|
+
Effect.gen(function*() {
|
|
43
|
+
const startedAt = Date.now()
|
|
44
|
+
yield* Effect.sleep(1)
|
|
45
|
+
assert.isTrue(Date.now() >= startedAt)
|
|
46
|
+
}))
|
|
47
|
+
|
|
48
|
+
// For property-based testing, use `it.effect.prop` with Schema-based
|
|
49
|
+
// arbitraries
|
|
50
|
+
it.effect.prop("reversing twice is identity", [Schema.String], ([value]) =>
|
|
51
|
+
Effect.gen(function*() {
|
|
52
|
+
const reversedTwice = value.split("").reverse().reverse().join("")
|
|
53
|
+
assert.strictEqual(reversedTwice, value)
|
|
54
|
+
}))
|
|
55
|
+
})
|