@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.
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 +506 -0
  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 +77 -1
  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,86 @@
1
+ /**
2
+ * @title Dynamic resources with LayerMap
3
+ *
4
+ * Use `LayerMap.Service` to dynamically build and manage resources that are
5
+ * keyed by some identifier, such as a tenant ID.
6
+ */
7
+ import { Context, Effect, Layer, LayerMap, Schema } from "effect"
8
+
9
+ class DatabaseQueryError extends Schema.TaggedError<DatabaseQueryError>()("DatabaseQueryError", {
10
+ tenantId: Schema.String,
11
+ cause: Schema.Defect()
12
+ }) {}
13
+
14
+ type UserRecord = {
15
+ readonly id: number
16
+ readonly email: string
17
+ }
18
+
19
+ let nextConnectionId = 0
20
+
21
+ export class DatabasePool extends Context.Service<DatabasePool, {
22
+ readonly tenantId: string
23
+ readonly connectionId: number
24
+ readonly query: (sql: string) => Effect.Effect<ReadonlyArray<UserRecord>, DatabaseQueryError>
25
+ }>()("app/DatabasePool") {
26
+ // A layer factory that builds one pool per tenant.
27
+ static readonly layer = (tenantId: string) =>
28
+ Layer.effect(
29
+ DatabasePool,
30
+ Effect.acquireRelease(
31
+ Effect.sync(() => {
32
+ const connectionId = ++nextConnectionId
33
+
34
+ return DatabasePool.of({
35
+ tenantId,
36
+ connectionId,
37
+ query: Effect.fn("DatabasePool.query")((_sql: string) =>
38
+ Effect.succeed([
39
+ { id: 1, email: `admin@${tenantId}.example.com` },
40
+ { id: 2, email: `ops@${tenantId}.example.com` }
41
+ ])
42
+ )
43
+ })
44
+ }),
45
+ (pool) => Effect.logInfo(`Closing tenant pool ${pool.tenantId}#${pool.connectionId}`)
46
+ )
47
+ )
48
+ }
49
+
50
+ // extend `LayerMap.Service` to create a `LayerMap` service
51
+ export class PoolMap extends LayerMap.Service<PoolMap>()("app/PoolMap", {
52
+ // `lookup` tells LayerMap how to build a layer for each tenant key.
53
+ lookup: (tenantId: string) => DatabasePool.layer(tenantId),
54
+
55
+ // You can also use the layers option for a static set of layers
56
+ // layers: {
57
+ // acme: DatabasePool.layer("acme"),
58
+ // globex: DatabasePool.layer("globex")
59
+ // },
60
+
61
+ // If a pool is not used for this duration, it is released automatically.
62
+ idleTimeToLive: "1 minute"
63
+ }) {}
64
+
65
+ const queryUsersForCurrentTenant = Effect.gen(function*() {
66
+ // Run a query agnostic of the tenant. The correct pool will be provided by
67
+ // the LayerMap.
68
+ const pool = yield* DatabasePool
69
+ return yield* pool.query("SELECT id, email FROM users ORDER BY id")
70
+ })
71
+
72
+ export const program = Effect.gen(function*() {
73
+ yield* queryUsersForCurrentTenant.pipe(
74
+ // Use `PoolMap.get` to access the pool for a specific tenant. The first
75
+ // time this is called for a tenant, the pool will be built using the
76
+ // `lookup` function defined in `PoolMap`. Subsequent calls will reuse the
77
+ // cached pool until it is idle for too long or invalidated.
78
+ Effect.provide(PoolMap.get("acme"))
79
+ )
80
+
81
+ // `PoolMap.invalidate` forces a key to rebuild on the next access.
82
+ yield* PoolMap.invalidate("acme")
83
+ }).pipe(
84
+ // Provide the `PoolMap` layer to the entire program.
85
+ Effect.provide(PoolMap.layer)
86
+ )
@@ -0,0 +1,3 @@
1
+ ## Managing resources and `Scope`s
2
+
3
+ Learn how to safely manage resources in Effect using `Scope`s and finalizers.
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @title Running effects with NodeRuntime and BunRuntime
3
+ *
4
+ * Use `NodeRuntime.runMain` to run an Effect program as your process entrypoint.
5
+ */
6
+ import { BunRuntime } from "@effect/platform-bun"
7
+ import { NodeRuntime } from "@effect/platform-node"
8
+ import { Effect, Layer } from "effect"
9
+
10
+ const Worker = Layer.effectDiscard(Effect.gen(function*() {
11
+ yield* Effect.logInfo("Starting worker...")
12
+ yield* Effect.forkScoped(Effect.gen(function*() {
13
+ while (true) {
14
+ yield* Effect.logInfo("Working...")
15
+ yield* Effect.sleep("1 second")
16
+ }
17
+ }))
18
+ }))
19
+
20
+ const program = Layer.launch(Worker)
21
+
22
+ // `runMain` installs SIGINT / SIGTERM handlers and interrupts running fibers
23
+ // for graceful shutdown.
24
+ NodeRuntime.runMain(program, {
25
+ // Disable automatic error reporting if your app already centralizes it.
26
+ disableErrorReporting: true
27
+ })
28
+
29
+ // Bun has the same API shape:
30
+ BunRuntime.runMain(program, { disableErrorReporting: true })
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @title Using Layer.launch as the application entry point
3
+ *
4
+ * Use `Layer.launch` to run a long-running Effect program as your process entrypoint.
5
+ */
6
+ import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
7
+ import { Effect, Layer } from "effect"
8
+ import { HttpRouter, HttpServerResponse } from "effect/http"
9
+ import { createServer } from "node:http"
10
+
11
+ // Build a tiny HTTP app with a health-check endpoint.
12
+ export const HealthRoutes = HttpRouter.use(Effect.fn(function*(router) {
13
+ yield* router.add("GET", "/health", Effect.succeed(HttpServerResponse.text("ok")))
14
+ yield* router.add("GET", "/healthz", Effect.succeed(HttpServerResponse.text("ok")))
15
+ }))
16
+
17
+ // Turn the routes into a server layer and provide the Node HTTP server backend.
18
+ export const HttpServerLive = HttpRouter.serve(HealthRoutes).pipe(
19
+ Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
20
+ )
21
+
22
+ // `Layer.launch` converts the layer into a long-running Effect<never>.
23
+ export const main = Layer.launch(HttpServerLive)
24
+
25
+ // This entrypoint pattern works well when the whole app is represented as
26
+ // layers (for example: HTTP server + background workers).
27
+ NodeRuntime.runMain(main)
@@ -0,0 +1 @@
1
+ ## Running Effect programs
@@ -0,0 +1,56 @@
1
+ /**
2
+ * @title Broadcasting domain events with PubSub
3
+ *
4
+ * Build an in-process event bus with `PubSub` and expose it as a service.
5
+ */
6
+ import { Context, Effect, Layer, PubSub, Stream } from "effect"
7
+
8
+ export type OrderEvent =
9
+ | { readonly _tag: "OrderPlaced"; readonly orderId: string }
10
+ | { readonly _tag: "PaymentCaptured"; readonly orderId: string }
11
+ | { readonly _tag: "OrderShipped"; readonly orderId: string }
12
+
13
+ export class OrderEvents extends Context.Service<OrderEvents, {
14
+ publish(event: OrderEvent): Effect.Effect<void>
15
+ publishAll(events: ReadonlyArray<OrderEvent>): Effect.Effect<void>
16
+ readonly subscribe: Stream.Stream<OrderEvent>
17
+ }>()("acme/OrderEvents") {
18
+ static readonly layer = Layer.effect(
19
+ OrderEvents,
20
+ Effect.gen(function*() {
21
+ // Use PubSub.bounded to create a PubSub with backpressure support.
22
+ // You can also use PubSub.unbounded if you don't need backpressure.
23
+ const pubsub = yield* PubSub.bounded<OrderEvent>({
24
+ capacity: 256,
25
+ // Optionally add a replay buffer to let late subscribers catch up on
26
+ // recent events after restarts.
27
+ replay: 50
28
+ })
29
+
30
+ // Ensure the PubSub is properly shut down when the service is no longer
31
+ // needed.
32
+ yield* Effect.addFinalizer(() => PubSub.shutdown(pubsub))
33
+
34
+ const publish = Effect.fn("OrderEvents.publish")(function*(event: OrderEvent) {
35
+ yield* PubSub.publish(pubsub, event)
36
+ })
37
+
38
+ const publishAll = Effect.fn("OrderEvents.publishAll")(function*(events: ReadonlyArray<OrderEvent>) {
39
+ yield* PubSub.publishAll(pubsub, events)
40
+ })
41
+
42
+ // Create a Stream that emits events published to the PubSub.
43
+ //
44
+ // Each subscriber will receive all events published after they subscribe,
45
+ // and if a replay buffer is configured, they will also receive the most
46
+ // recent events that were published before they subscribed.
47
+ const subscribe = Stream.fromPubSub(pubsub)
48
+
49
+ return OrderEvents.of({
50
+ publish,
51
+ publishAll,
52
+ subscribe
53
+ })
54
+ })
55
+ )
56
+ }
@@ -0,0 +1,3 @@
1
+ ## Broadcasting messages with PubSub
2
+
3
+ Use `PubSub` when you need one producer to fan out messages to many consumers.
@@ -0,0 +1,103 @@
1
+ /**
2
+ * @title Creating streams from common data sources
3
+ *
4
+ * Learn how to create streams from various data sources. Includes:
5
+ *
6
+ * - `Stream.fromIterable` for arrays and other iterables
7
+ * - `Stream.fromEffectSchedule` for polling effects
8
+ * - `Stream.paginate` for paginated APIs
9
+ * - `Stream.fromAsyncIterable` for async iterables
10
+ * - `Stream.fromEventListener` for DOM events
11
+ * - `Stream.callback` for any callback-based API
12
+ * - `NodeStream.fromReadable` for Node.js readable streams
13
+ */
14
+ import { NodeStream } from "@effect/platform-node"
15
+ import { Array, Effect, Queue, Schedule, Schema, Stream } from "effect"
16
+ import * as Option from "effect/Option"
17
+ import { Readable } from "node:stream"
18
+
19
+ // `Stream.fromIterable` turns any iterable into a stream.
20
+ export const numbers = Stream.fromIterable<number>([1, 2, 3, 4, 5])
21
+
22
+ // `Stream.fromEffectSchedule` turns a single effect into a polling stream.
23
+ // This is useful for metrics, health checks, and cache refresh loops.
24
+ export const samples = Stream.fromEffectSchedule(
25
+ Effect.succeed(3),
26
+ Schedule.spaced("30 seconds")
27
+ ).pipe(
28
+ // Stream.take limits the number of elements emitted by the stream.
29
+ Stream.take(3)
30
+ )
31
+
32
+ // Use `Stream.paginate` when reading APIs that return one page at a time.
33
+ // The function returns the current page of values and optionally the next
34
+ // cursor.
35
+ export const fetchJobsPage = Stream.paginate(
36
+ 0, // start with page 0 (the cursor)
37
+ Effect.fn(function*(page) {
38
+ // Simulate network latency
39
+ yield* Effect.sleep("50 millis")
40
+
41
+ const results = Array.range(0, 100).map((i) => `Job ${i + 1 + page * 100}`)
42
+
43
+ // only return 10 pages of results
44
+ const nextPage = page <= 10
45
+ ? Option.some(page + 1)
46
+ : Option.none()
47
+
48
+ return [results, nextPage] as const
49
+ })
50
+ )
51
+
52
+ class LetterError extends Schema.TaggedError<LetterError>()("LetterError", {
53
+ cause: Schema.Defect()
54
+ }) {}
55
+
56
+ async function* asyncIterable() {
57
+ yield "a"
58
+ yield "b"
59
+ yield "c"
60
+ }
61
+
62
+ // Create a stream from an async iterable.
63
+ // The second argument is a function that converts any errors thrown by the
64
+ // async iterable into a typed error.
65
+ export const letters = Stream.fromAsyncIterable(
66
+ asyncIterable(),
67
+ (cause) => new LetterError({ cause })
68
+ )
69
+
70
+ const button = document.getElementById("my-button")!
71
+
72
+ // `Stream.fromEventListener` creates a stream from an event listener.
73
+ export const events = Stream.fromEventListener<PointerEvent>(button, "click")
74
+
75
+ // You can also use `Stream.callback` to create a stream from any callback-based
76
+ // API.
77
+ export const callbackStream = Stream.callback<PointerEvent>(Effect.fn(function*(queue) {
78
+ // You can use the `Queue` apis to emit values into the stream from the
79
+ // callback.
80
+ function onEvent(event: PointerEvent) {
81
+ Queue.offerUnsafe(queue, event)
82
+ }
83
+ // register the event listener and add a finalizer to unregister it when the
84
+ // stream is finished.
85
+ yield* Effect.acquireRelease(
86
+ Effect.sync(() => button.addEventListener("click", onEvent)),
87
+ () => Effect.sync(() => button.removeEventListener("click", onEvent))
88
+ )
89
+ }))
90
+
91
+ export class NodeStreamError extends Schema.TaggedError<NodeStreamError>()("NodeStreamError", {
92
+ cause: Schema.Defect()
93
+ }) {}
94
+
95
+ // Create a stream from a Node.js readable stream.
96
+ //
97
+ // It takes options to convert any errors emitted by the stream into a typed
98
+ // error, and to evaluate the stream lazily.
99
+ export const nodeStream = NodeStream.fromReadable({
100
+ evaluate: () => Readable.from(["Hello", " ", "world", "!"]),
101
+ onError: (cause) => new NodeStreamError({ cause }),
102
+ closeOnDone: true // true by default
103
+ })
@@ -0,0 +1,137 @@
1
+ /**
2
+ * @title Consuming and transforming streams
3
+ *
4
+ * How to transform and consume streams using operators like `map`, `flatMap`, `filter`, `mapEffect`, and various `run*` methods.
5
+ */
6
+ import { Effect, Sink, Stream } from "effect"
7
+
8
+ interface Order {
9
+ readonly id: string
10
+ readonly customerId: string
11
+ readonly status: "paid" | "refunded"
12
+ readonly subtotalCents: number
13
+ readonly shippingCents: number
14
+ readonly country: "US" | "CA" | "NZ"
15
+ }
16
+
17
+ interface NormalizedOrder extends Order {
18
+ readonly totalCents: number
19
+ }
20
+
21
+ interface EnrichedOrder extends NormalizedOrder {
22
+ readonly taxCents: number
23
+ readonly grandTotalCents: number
24
+ readonly priority: "normal" | "high"
25
+ }
26
+
27
+ // Start with structured order events from an in-memory source.
28
+ export const orderEvents = Stream.succeed<Order>({
29
+ id: "ord_1001",
30
+ customerId: "cus_1",
31
+ status: "paid",
32
+ subtotalCents: 4_500,
33
+ shippingCents: 500,
34
+ country: "US"
35
+ })
36
+
37
+ // Use `Stream.map` for pure per-element transforms.
38
+ export const normalizedOrders = orderEvents.pipe(
39
+ Stream.map((order): NormalizedOrder => ({
40
+ ...order,
41
+ totalCents: order.subtotalCents + order.shippingCents
42
+ }))
43
+ )
44
+
45
+ // `Stream.filter` lets you exclude elements that don't match a predicate.
46
+ export const paidOrders = normalizedOrders.pipe(
47
+ Stream.filter((order) => order.status === "paid")
48
+ )
49
+
50
+ // Use `Stream.flatMap` to transform each element into a stream, and flatten the
51
+ // results.
52
+ export const allOrders = Stream.make("US", "CA", "NZ").pipe(
53
+ Stream.flatMap(
54
+ (country) =>
55
+ Stream.range(1, 50).pipe(
56
+ Stream.map((i): Order => ({
57
+ id: `ord_${country}_${i}`,
58
+ customerId: `cus_${i}`,
59
+ status: i % 10 === 0 ? "refunded" : "paid",
60
+ subtotalCents: Math.round(Math.random() * 100_000),
61
+ shippingCents: Math.round(Math.random() * 10_000),
62
+ country
63
+ }))
64
+ ),
65
+ // Optionally control the concurrency of the flatMap with the second argument.
66
+ { concurrency: 2 }
67
+ )
68
+ )
69
+
70
+ const enrichOrder = Effect.fn(function*(order: NormalizedOrder): Effect.fn.Return<EnrichedOrder> {
71
+ // Simulate effectful enrichment (for example, tax/risk lookup).
72
+ yield* Effect.sleep("5 millis")
73
+
74
+ const taxRate = order.country === "US" ? 0.08 : 0.13
75
+ const taxCents = Math.round(order.totalCents * taxRate)
76
+
77
+ return {
78
+ ...order,
79
+ taxCents,
80
+ grandTotalCents: order.totalCents + taxCents,
81
+ priority: order.totalCents >= 20_000 ? "high" : "normal"
82
+ }
83
+ })
84
+
85
+ // `Stream.mapEffect` performs effectful per-element transforms with concurrency control.
86
+ export const enrichedPaidOrders = paidOrders.pipe(
87
+ Stream.mapEffect(enrichOrder, { concurrency: 4 })
88
+ )
89
+
90
+ // `runCollect` gathers all stream outputs into an immutable array.
91
+ export const collectedOrders = Stream.runCollect(enrichedPaidOrders)
92
+
93
+ // `runDrain` runs the stream for its effects, ignoring all outputs.
94
+ export const drained = Stream.runDrain(enrichedPaidOrders)
95
+
96
+ // `runForEach` executes an effectful consumer for every element.
97
+ export const logOrders = enrichedPaidOrders.pipe(
98
+ Stream.runForEach((order) => Effect.logInfo(`Order ${order.id} total=$${(order.grandTotalCents / 100).toFixed(2)}`))
99
+ )
100
+
101
+ // `runFold` reduces the stream to one accumulated value.
102
+ export const totalRevenueCents = enrichedPaidOrders.pipe(
103
+ Stream.runFold(() => 0, (acc: number, order) => acc + order.grandTotalCents)
104
+ )
105
+
106
+ // `run` lets you consume a stream through any Sink.
107
+ export const totalRevenueViaSink = enrichedPaidOrders.pipe(
108
+ Stream.map((order) => order.grandTotalCents),
109
+ Stream.run(Sink.sum)
110
+ )
111
+
112
+ // `runHead` and `runLast` capture edge elements as Option values.
113
+ export const firstLargeOrder = enrichedPaidOrders.pipe(
114
+ Stream.filter((order) => order.priority === "high"),
115
+ Stream.runHead
116
+ )
117
+
118
+ export const lastLargeOrder = enrichedPaidOrders.pipe(
119
+ Stream.filter((order) => order.priority === "high"),
120
+ Stream.runLast
121
+ )
122
+
123
+ // Windowing-style operators help shape what downstream consumers see.
124
+ export const firstTwoOrders = enrichedPaidOrders.pipe(
125
+ Stream.take(2),
126
+ Stream.runCollect
127
+ )
128
+
129
+ export const afterWarmupOrder = enrichedPaidOrders.pipe(
130
+ Stream.drop(1),
131
+ Stream.runCollect
132
+ )
133
+
134
+ export const untilLargeOrder = enrichedPaidOrders.pipe(
135
+ Stream.takeWhile((order) => order.priority === "normal"),
136
+ Stream.runCollect
137
+ )
@@ -0,0 +1,163 @@
1
+ /**
2
+ * @title Decoding and encoding streams
3
+ *
4
+ * Use `Stream.pipeThroughChannel` with the `Ndjson` and `SchemaBinary` modules to
5
+ * decode and encode streams of structured data.
6
+ */
7
+ import { DateTime, Schema, Stream } from "effect"
8
+ import { Ndjson, SchemaBinary } from "effect/encoding"
9
+
10
+ // SchemaBinary derives a framed binary decoder directly from a schema.
11
+ export const schemaBinaryDecoder = SchemaBinary.decode(Schema.Struct({
12
+ id: Schema.Int,
13
+ name: Schema.String
14
+ }))()
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Domain
18
+ // ---------------------------------------------------------------------------
19
+
20
+ // A log entry schema representing structured log events. In practice these
21
+ // would come from a file, HTTP body, or socket connection.
22
+ // `DateTimeUtcFromString` decodes an ISO-8601 string into a `DateTime.Utc`.
23
+ class LogEntry extends Schema.Class<LogEntry>("LogEntry")({
24
+ timestamp: Schema.DateTimeUtcFromString,
25
+ level: Schema.Literals(["info", "warn", "error"]),
26
+ message: Schema.String
27
+ }) {}
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Decoding NDJSON strings → objects
31
+ // ---------------------------------------------------------------------------
32
+
33
+ // Suppose we receive raw NDJSON text from a file or network socket.
34
+ // `Ndjson.decodeString()` is a Channel that splits incoming strings on
35
+ // newlines and `JSON.parse`s each line.
36
+ // Pipe the stream through the channel with `Stream.pipeThroughChannel`.
37
+ export const decodeUntyped = Stream.make(
38
+ "{\"timestamp\":\"2025-06-01T00:00:00Z\",\"level\":\"info\",\"message\":\"start\"}\n" +
39
+ "{\"timestamp\":\"2025-06-01T00:00:01Z\",\"level\":\"error\",\"message\":\"oops\"}\n"
40
+ ).pipe(
41
+ Stream.pipeThroughChannel(Ndjson.decodeString()),
42
+ Stream.runCollect
43
+ )
44
+
45
+ // When you need schema validation on top of the raw JSON parse, use
46
+ // `Ndjson.decodeSchemaString(Schema)()`. This decodes each line, parses the
47
+ // JSON, and then validates each value against the schema — all in one channel.
48
+ export const decodeTyped = Stream.make(
49
+ "{\"timestamp\":\"2025-06-01T00:00:00Z\",\"level\":\"info\",\"message\":\"start\"}\n" +
50
+ "{\"timestamp\":\"2025-06-01T00:00:01Z\",\"level\":\"error\",\"message\":\"oops\"}\n"
51
+ ).pipe(
52
+ Stream.pipeThroughChannel(Ndjson.decodeSchemaString(LogEntry)()),
53
+ Stream.runCollect
54
+ )
55
+
56
+ // ---------------------------------------------------------------------------
57
+ // Encoding objects → NDJSON strings
58
+ // ---------------------------------------------------------------------------
59
+
60
+ // `Ndjson.encodeString()` serialises each value to a JSON line.
61
+ // The resulting stream emits ready-to-write NDJSON strings.
62
+ export const encodeUntyped = Stream.make(
63
+ { timestamp: "2025-06-01T00:00:00Z", level: "info", message: "start" },
64
+ { timestamp: "2025-06-01T00:00:01Z", level: "error", message: "oops" }
65
+ ).pipe(
66
+ Stream.pipeThroughChannel(Ndjson.encodeString()),
67
+ Stream.runCollect
68
+ )
69
+
70
+ // `Ndjson.encodeSchemaString(Schema)()` encodes each value through the schema
71
+ // first (applying any transformations such as date formatting), then
72
+ // serialises it to an NDJSON line.
73
+ export const encodeTyped = Stream.make(
74
+ new LogEntry({
75
+ timestamp: DateTime.makeUnsafe("2025-06-01T00:00:00Z"),
76
+ level: "info",
77
+ message: "start"
78
+ }),
79
+ new LogEntry({
80
+ timestamp: DateTime.makeUnsafe("2025-06-01T00:00:01Z"),
81
+ level: "error",
82
+ message: "oops"
83
+ })
84
+ ).pipe(
85
+ Stream.pipeThroughChannel(Ndjson.encodeSchemaString(LogEntry)()),
86
+ Stream.runCollect
87
+ )
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // Binary (Uint8Array) variants
91
+ // ---------------------------------------------------------------------------
92
+
93
+ // When working with binary I/O (e.g. TCP sockets, file descriptors) use the
94
+ // non-string variants. `Ndjson.decode()` expects `Uint8Array` chunks and
95
+ // handles text decoding internally. `Ndjson.encode()` produces `Uint8Array`
96
+ // output.
97
+ const enc = new TextEncoder()
98
+
99
+ export const decodeBinary = Stream.make(
100
+ enc.encode("{\"level\":\"info\",\"message\":\"binary\"}\n")
101
+ ).pipe(
102
+ Stream.pipeThroughChannel(Ndjson.decode()),
103
+ Stream.runCollect
104
+ )
105
+
106
+ export const encodeBinary = Stream.make(
107
+ { level: "info", message: "binary" }
108
+ ).pipe(
109
+ Stream.pipeThroughChannel(Ndjson.encode()),
110
+ Stream.runCollect
111
+ )
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // Handling empty lines
115
+ // ---------------------------------------------------------------------------
116
+
117
+ // NDJSON files sometimes contain blank lines (e.g. trailing newlines or
118
+ // pretty-printed output). Pass `{ ignoreEmptyLines: true }` to skip them
119
+ // instead of raising an `NdjsonError`.
120
+ export const decodeIgnoringBlanks = Stream.make(
121
+ "{\"ok\":true}\n\n{\"ok\":false}\n"
122
+ ).pipe(
123
+ Stream.pipeThroughChannel(Ndjson.decodeString({ ignoreEmptyLines: true })),
124
+ Stream.runCollect
125
+ )
126
+
127
+ // ---------------------------------------------------------------------------
128
+ // Error handling
129
+ // ---------------------------------------------------------------------------
130
+
131
+ // `Ndjson.NdjsonError` is raised when encoding (`kind: "Pack"`) or decoding
132
+ // (`kind: "Unpack"`) fails. You can catch it with `Stream.catchTag` or
133
+ // `Effect.catchTag`.
134
+ export const handleDecodeErrors = Stream.make("not-valid-json\n").pipe(
135
+ Stream.pipeThroughChannel(Ndjson.decodeString()),
136
+ Stream.catchTag("NdjsonError", (err) =>
137
+ // The `kind` field indicates whether the error occurred during
138
+ // encoding ("Pack") or decoding ("Unpack"), and `cause` contains
139
+ // the underlying exception.
140
+ Stream.succeed({ recovered: true, kind: err.kind })),
141
+ Stream.runCollect
142
+ )
143
+
144
+ // ---------------------------------------------------------------------------
145
+ // Realistic pipeline: decode → transform → re-encode
146
+ // ---------------------------------------------------------------------------
147
+
148
+ // A common pattern is to read NDJSON, transform each record, and write it
149
+ // back as NDJSON. This example filters error-level log entries and re-encodes
150
+ // them.
151
+ const ndjsonInput = "{\"timestamp\":\"2025-06-01T00:00:00Z\",\"level\":\"info\",\"message\":\"ok\"}\n" +
152
+ "{\"timestamp\":\"2025-06-01T00:00:01Z\",\"level\":\"error\",\"message\":\"fail\"}\n" +
153
+ "{\"timestamp\":\"2025-06-01T00:00:02Z\",\"level\":\"warn\",\"message\":\"slow\"}\n"
154
+
155
+ export const filterAndReencode = Stream.make(ndjsonInput).pipe(
156
+ // Decode each line into a validated LogEntry
157
+ Stream.pipeThroughChannel(Ndjson.decodeSchemaString(LogEntry)()),
158
+ // Keep only error-level entries
159
+ Stream.filter((entry) => entry.level === "error"),
160
+ // Re-encode the filtered entries back to NDJSON strings
161
+ Stream.pipeThroughChannel(Ndjson.encodeSchemaString(LogEntry)()),
162
+ Stream.runCollect
163
+ )
@@ -0,0 +1,4 @@
1
+ ## Working with Streams
2
+
3
+ Effect Streams represent effectful, pull-based sequences of values over time.
4
+ They let you model finite or infinite data sources.