@effect/platform-cloudflare 0.0.0-stage → 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.
Files changed (237) hide show
  1. package/AGENTS.md +434 -0
  2. package/CLAUDE.md +434 -0
  3. package/LICENSE +21 -0
  4. package/README.md +505 -2
  5. package/ai-docs/README.md +44 -0
  6. package/ai-docs/package.json +36 -0
  7. package/ai-docs/src/01_effect/01_basics/01_effect-gen.ts +30 -0
  8. package/ai-docs/src/01_effect/01_basics/02_effect-fn.ts +52 -0
  9. package/ai-docs/src/01_effect/01_basics/10_creating-effects.ts +74 -0
  10. package/ai-docs/src/01_effect/01_basics/index.md +7 -0
  11. package/ai-docs/src/01_effect/02_schema/10_schema-basics.ts +43 -0
  12. package/ai-docs/src/01_effect/02_schema/index.md +7 -0
  13. package/ai-docs/src/01_effect/03_services/01_service.ts +45 -0
  14. package/ai-docs/src/01_effect/03_services/10_reference.ts +10 -0
  15. package/ai-docs/src/01_effect/03_services/20_layer-composition.ts +70 -0
  16. package/ai-docs/src/01_effect/03_services/20_layer-unwrap.ts +66 -0
  17. package/ai-docs/src/01_effect/03_services/index.md +5 -0
  18. package/ai-docs/src/01_effect/04_errors/01_error-handling.ts +30 -0
  19. package/ai-docs/src/01_effect/04_errors/10_catch-tags.ts +24 -0
  20. package/ai-docs/src/01_effect/04_errors/20_reason-errors.ts +64 -0
  21. package/ai-docs/src/01_effect/04_errors/index.md +1 -0
  22. package/ai-docs/src/01_effect/05_resources/10_acquire-release.ts +105 -0
  23. package/ai-docs/src/01_effect/05_resources/20_layer-side-effects.ts +31 -0
  24. package/ai-docs/src/01_effect/05_resources/30_layer-map.ts +86 -0
  25. package/ai-docs/src/01_effect/05_resources/index.md +3 -0
  26. package/ai-docs/src/01_effect/06_running/10_run-main.ts +30 -0
  27. package/ai-docs/src/01_effect/06_running/20_layer-launch.ts +27 -0
  28. package/ai-docs/src/01_effect/06_running/index.md +1 -0
  29. package/ai-docs/src/01_effect/07_pubsub/10_pubsub.ts +56 -0
  30. package/ai-docs/src/01_effect/07_pubsub/index.md +3 -0
  31. package/ai-docs/src/03_stream/10_creating-streams.ts +103 -0
  32. package/ai-docs/src/03_stream/20_consuming-streams.ts +137 -0
  33. package/ai-docs/src/03_stream/30_encoding.ts +163 -0
  34. package/ai-docs/src/03_stream/index.md +4 -0
  35. package/ai-docs/src/04_integration/10_managed-runtime.ts +129 -0
  36. package/ai-docs/src/04_integration/index.md +5 -0
  37. package/ai-docs/src/05_batching/10_request-resolver.ts +89 -0
  38. package/ai-docs/src/05_batching/index.md +3 -0
  39. package/ai-docs/src/06_schedule/10_schedules.ts +111 -0
  40. package/ai-docs/src/06_schedule/index.md +3 -0
  41. package/ai-docs/src/07_datetime/10_creating-and-formatting.ts +30 -0
  42. package/ai-docs/src/07_datetime/20_time-zones.ts +44 -0
  43. package/ai-docs/src/07_datetime/index.md +5 -0
  44. package/ai-docs/src/08_observability/10_logging.ts +66 -0
  45. package/ai-docs/src/08_observability/20_otlp-tracing.ts +95 -0
  46. package/ai-docs/src/08_observability/index.md +7 -0
  47. package/ai-docs/src/09_testing/10_effect-tests.ts +55 -0
  48. package/ai-docs/src/09_testing/20_layer-tests.ts +138 -0
  49. package/ai-docs/src/09_testing/index.md +1 -0
  50. package/ai-docs/src/10_predicate/01_basics.ts +14 -0
  51. package/ai-docs/src/10_predicate/index.md +9 -0
  52. package/ai-docs/src/40_sql/10_basics.ts +175 -0
  53. package/ai-docs/src/40_sql/index.md +6 -0
  54. package/ai-docs/src/50_http-client/10_basics.ts +102 -0
  55. package/ai-docs/src/50_http-client/index.md +3 -0
  56. package/ai-docs/src/51_http-server/10_basics.ts +118 -0
  57. package/ai-docs/src/51_http-server/20_testing.ts +100 -0
  58. package/ai-docs/src/51_http-server/fixtures/api/Api.ts +14 -0
  59. package/ai-docs/src/51_http-server/fixtures/api/Authorization.ts +36 -0
  60. package/ai-docs/src/51_http-server/fixtures/api/System.ts +10 -0
  61. package/ai-docs/src/51_http-server/fixtures/api/Users.ts +107 -0
  62. package/ai-docs/src/51_http-server/fixtures/domain/User.ts +21 -0
  63. package/ai-docs/src/51_http-server/fixtures/domain/UserErrors.ts +22 -0
  64. package/ai-docs/src/51_http-server/fixtures/server/Authorization.ts +37 -0
  65. package/ai-docs/src/51_http-server/fixtures/server/Users/http.ts +80 -0
  66. package/ai-docs/src/51_http-server/fixtures/server/Users.ts +183 -0
  67. package/ai-docs/src/51_http-server/index.md +3 -0
  68. package/ai-docs/src/52_graphql/10_basics.ts +117 -0
  69. package/ai-docs/src/52_graphql/20_testing.ts +45 -0
  70. package/ai-docs/src/52_graphql/30_batching.ts +118 -0
  71. package/ai-docs/src/52_graphql/40_aliased-batching.ts +124 -0
  72. package/ai-docs/src/52_graphql/50_errors-and-retries.ts +74 -0
  73. package/ai-docs/src/52_graphql/index.md +6 -0
  74. package/ai-docs/src/60_child-process/10_working-with-child-processes.ts +117 -0
  75. package/ai-docs/src/60_child-process/index.md +3 -0
  76. package/ai-docs/src/70_cli/10_basics.ts +162 -0
  77. package/ai-docs/src/70_cli/index.md +5 -0
  78. package/ai-docs/src/71_ai/10_language-model.ts +156 -0
  79. package/ai-docs/src/71_ai/20_tools.ts +226 -0
  80. package/ai-docs/src/71_ai/30_chat.ts +158 -0
  81. package/ai-docs/src/71_ai/fixtures/domain/LaunchPlan.ts +9 -0
  82. package/ai-docs/src/71_ai/index.md +5 -0
  83. package/ai-docs/src/80_cluster/10_entities.ts +97 -0
  84. package/ai-docs/src/80_cluster/index.md +4 -0
  85. package/ai-docs/src/index.md +10 -0
  86. package/ai-docs/tsconfig.json +28 -0
  87. package/dist/AlchemyCloudflareCluster.d.ts +134 -0
  88. package/dist/AlchemyCloudflareCluster.d.ts.map +1 -0
  89. package/dist/AlchemyCloudflareCluster.js +111 -0
  90. package/dist/AlchemyCloudflareCluster.js.map +1 -0
  91. package/dist/CloudflareCluster.d.ts +196 -0
  92. package/dist/CloudflareCluster.d.ts.map +1 -0
  93. package/dist/CloudflareCluster.js +533 -0
  94. package/dist/CloudflareCluster.js.map +1 -0
  95. package/dist/CloudflareDurableObjectPrograms.d.ts +279 -0
  96. package/dist/CloudflareDurableObjectPrograms.d.ts.map +1 -0
  97. package/dist/CloudflareDurableObjectPrograms.js +295 -0
  98. package/dist/CloudflareDurableObjectPrograms.js.map +1 -0
  99. package/dist/CloudflareDurableObjects.d.ts +149 -0
  100. package/dist/CloudflareDurableObjects.d.ts.map +1 -0
  101. package/dist/CloudflareDurableObjects.js +337 -0
  102. package/dist/CloudflareDurableObjects.js.map +1 -0
  103. package/dist/CloudflarePersistedQueue.d.ts +36 -0
  104. package/dist/CloudflarePersistedQueue.d.ts.map +1 -0
  105. package/dist/CloudflarePersistedQueue.js +122 -0
  106. package/dist/CloudflarePersistedQueue.js.map +1 -0
  107. package/dist/CloudflareWorkflowEngine.d.ts +72 -0
  108. package/dist/CloudflareWorkflowEngine.d.ts.map +1 -0
  109. package/dist/CloudflareWorkflowEngine.js +187 -0
  110. package/dist/CloudflareWorkflowEngine.js.map +1 -0
  111. package/dist/index.d.ts +29 -0
  112. package/dist/index.d.ts.map +1 -0
  113. package/dist/index.js +33 -0
  114. package/dist/index.js.map +1 -0
  115. package/dist/internal/alchemyCluster.d.ts +2 -0
  116. package/dist/internal/alchemyCluster.d.ts.map +1 -0
  117. package/dist/internal/alchemyCluster.js +75 -0
  118. package/dist/internal/alchemyCluster.js.map +1 -0
  119. package/dist/internal/boundedMap.d.ts +3 -0
  120. package/dist/internal/boundedMap.d.ts.map +1 -0
  121. package/dist/internal/boundedMap.js +15 -0
  122. package/dist/internal/boundedMap.js.map +1 -0
  123. package/dist/internal/cluster.d.ts +57 -0
  124. package/dist/internal/cluster.d.ts.map +1 -0
  125. package/dist/internal/cluster.js +2 -0
  126. package/dist/internal/cluster.js.map +1 -0
  127. package/dist/internal/clusterName.d.ts +2 -0
  128. package/dist/internal/clusterName.d.ts.map +1 -0
  129. package/dist/internal/clusterName.js +16 -0
  130. package/dist/internal/clusterName.js.map +1 -0
  131. package/dist/internal/entityKeepAlive.d.ts +2 -0
  132. package/dist/internal/entityKeepAlive.d.ts.map +1 -0
  133. package/dist/internal/entityKeepAlive.js +73 -0
  134. package/dist/internal/entityKeepAlive.js.map +1 -0
  135. package/dist/internal/entityMailbox.d.ts +2 -0
  136. package/dist/internal/entityMailbox.d.ts.map +1 -0
  137. package/dist/internal/entityMailbox.js +160 -0
  138. package/dist/internal/entityMailbox.js.map +1 -0
  139. package/dist/internal/entityRegistry.d.ts +16 -0
  140. package/dist/internal/entityRegistry.d.ts.map +1 -0
  141. package/dist/internal/entityRegistry.js +22 -0
  142. package/dist/internal/entityRegistry.js.map +1 -0
  143. package/dist/internal/entityReply.d.ts +2 -0
  144. package/dist/internal/entityReply.d.ts.map +1 -0
  145. package/dist/internal/entityReply.js +35 -0
  146. package/dist/internal/entityReply.js.map +1 -0
  147. package/dist/internal/entityRuntime.d.ts +2 -0
  148. package/dist/internal/entityRuntime.d.ts.map +1 -0
  149. package/dist/internal/entityRuntime.js +823 -0
  150. package/dist/internal/entityRuntime.js.map +1 -0
  151. package/dist/internal/entitySql.d.ts +2 -0
  152. package/dist/internal/entitySql.d.ts.map +1 -0
  153. package/dist/internal/entitySql.js +29 -0
  154. package/dist/internal/entitySql.js.map +1 -0
  155. package/dist/internal/entityStorage.d.ts +2 -0
  156. package/dist/internal/entityStorage.d.ts.map +1 -0
  157. package/dist/internal/entityStorage.js +98 -0
  158. package/dist/internal/entityStorage.js.map +1 -0
  159. package/dist/internal/entityWire.d.ts +2 -0
  160. package/dist/internal/entityWire.d.ts.map +1 -0
  161. package/dist/internal/entityWire.js +146 -0
  162. package/dist/internal/entityWire.js.map +1 -0
  163. package/dist/internal/queueRuntime.d.ts +2 -0
  164. package/dist/internal/queueRuntime.d.ts.map +1 -0
  165. package/dist/internal/queueRuntime.js +88 -0
  166. package/dist/internal/queueRuntime.js.map +1 -0
  167. package/dist/internal/queueStorage.d.ts +2 -0
  168. package/dist/internal/queueStorage.d.ts.map +1 -0
  169. package/dist/internal/queueStorage.js +129 -0
  170. package/dist/internal/queueStorage.js.map +1 -0
  171. package/dist/internal/registry.d.ts +10 -0
  172. package/dist/internal/registry.d.ts.map +1 -0
  173. package/dist/internal/registry.js +24 -0
  174. package/dist/internal/registry.js.map +1 -0
  175. package/dist/internal/singletonRegistry.d.ts +2 -0
  176. package/dist/internal/singletonRegistry.d.ts.map +1 -0
  177. package/dist/internal/singletonRegistry.js +9 -0
  178. package/dist/internal/singletonRegistry.js.map +1 -0
  179. package/dist/internal/singletonRuntime.d.ts +2 -0
  180. package/dist/internal/singletonRuntime.d.ts.map +1 -0
  181. package/dist/internal/singletonRuntime.js +42 -0
  182. package/dist/internal/singletonRuntime.js.map +1 -0
  183. package/dist/internal/singletonStorage.d.ts +2 -0
  184. package/dist/internal/singletonStorage.d.ts.map +1 -0
  185. package/dist/internal/singletonStorage.js +34 -0
  186. package/dist/internal/singletonStorage.js.map +1 -0
  187. package/dist/internal/transport.d.ts +2 -0
  188. package/dist/internal/transport.d.ts.map +1 -0
  189. package/dist/internal/transport.js +49 -0
  190. package/dist/internal/transport.js.map +1 -0
  191. package/dist/internal/workflowRegistry.d.ts +2 -0
  192. package/dist/internal/workflowRegistry.d.ts.map +1 -0
  193. package/dist/internal/workflowRegistry.js +29 -0
  194. package/dist/internal/workflowRegistry.js.map +1 -0
  195. package/dist/internal/workflowRuntime.d.ts +2 -0
  196. package/dist/internal/workflowRuntime.d.ts.map +1 -0
  197. package/dist/internal/workflowRuntime.js +322 -0
  198. package/dist/internal/workflowRuntime.js.map +1 -0
  199. package/dist/internal/workflowStorage.d.ts +2 -0
  200. package/dist/internal/workflowStorage.d.ts.map +1 -0
  201. package/dist/internal/workflowStorage.js +113 -0
  202. package/dist/internal/workflowStorage.js.map +1 -0
  203. package/dist/internal/workflowWire.d.ts +2 -0
  204. package/dist/internal/workflowWire.d.ts.map +1 -0
  205. package/dist/internal/workflowWire.js +52 -0
  206. package/dist/internal/workflowWire.js.map +1 -0
  207. package/package.json +74 -3
  208. package/src/AlchemyCloudflareCluster.ts +200 -0
  209. package/src/CloudflareCluster.ts +704 -0
  210. package/src/CloudflareDurableObjectPrograms.ts +543 -0
  211. package/src/CloudflareDurableObjects.ts +430 -0
  212. package/src/CloudflarePersistedQueue.ts +189 -0
  213. package/src/CloudflareWorkflowEngine.ts +220 -0
  214. package/src/index.ts +38 -0
  215. package/src/internal/alchemyCluster.ts +120 -0
  216. package/src/internal/boundedMap.ts +15 -0
  217. package/src/internal/cluster.ts +57 -0
  218. package/src/internal/clusterName.ts +19 -0
  219. package/src/internal/entityKeepAlive.ts +121 -0
  220. package/src/internal/entityMailbox.ts +324 -0
  221. package/src/internal/entityRegistry.ts +46 -0
  222. package/src/internal/entityReply.ts +54 -0
  223. package/src/internal/entityRuntime.ts +1318 -0
  224. package/src/internal/entitySql.ts +60 -0
  225. package/src/internal/entityStorage.ts +168 -0
  226. package/src/internal/entityWire.ts +204 -0
  227. package/src/internal/queueRuntime.ts +174 -0
  228. package/src/internal/queueStorage.ts +255 -0
  229. package/src/internal/registry.ts +31 -0
  230. package/src/internal/singletonRegistry.ts +21 -0
  231. package/src/internal/singletonRuntime.ts +88 -0
  232. package/src/internal/singletonStorage.ts +64 -0
  233. package/src/internal/transport.ts +67 -0
  234. package/src/internal/workflowRegistry.ts +78 -0
  235. package/src/internal/workflowRuntime.ts +443 -0
  236. package/src/internal/workflowStorage.ts +234 -0
  237. 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,3 @@
1
+ ## Batching external requests
2
+
3
+ Learn how to batch multiple requests into fewer external calls.
@@ -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,3 @@
1
+ ## Working with Schedules
2
+
3
+ Schedules define recurring patterns for retries, repeats and polling.
@@ -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,5 @@
1
+ ## Working with DateTime
2
+
3
+ When working with dates and time, use the `DateTime` module instead of `Date` and `Date.now`.
4
+
5
+ Use it when your Effect programs need testable current time, safe parsing, stable ISO formatting, time-zone conversion, or calendar arithmetic.
@@ -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
+ })