@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.
- package/AGENTS.md +434 -0
- package/CLAUDE.md +434 -0
- package/LICENSE +21 -0
- package/README.md +505 -2
- 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 +74 -3
- 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,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,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,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
|
+
)
|