@effect/sql-sqlite-wasm 4.0.0-beta.99 → 4.0.0-rc.108

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 (92) hide show
  1. package/AGENTS.md +381 -0
  2. package/CLAUDE.md +381 -0
  3. package/README.md +10 -3
  4. package/ai-docs/README.md +44 -0
  5. package/ai-docs/package.json +36 -0
  6. package/ai-docs/src/01_effect/01_basics/01_effect-gen.ts +30 -0
  7. package/ai-docs/src/01_effect/01_basics/02_effect-fn.ts +39 -0
  8. package/ai-docs/src/01_effect/01_basics/10_creating-effects.ts +74 -0
  9. package/ai-docs/src/01_effect/01_basics/index.md +5 -0
  10. package/ai-docs/src/01_effect/02_schema/10_schema-basics.ts +43 -0
  11. package/ai-docs/src/01_effect/02_schema/index.md +7 -0
  12. package/ai-docs/src/01_effect/03_services/01_service.ts +45 -0
  13. package/ai-docs/src/01_effect/03_services/10_reference.ts +10 -0
  14. package/ai-docs/src/01_effect/03_services/20_layer-composition.ts +70 -0
  15. package/ai-docs/src/01_effect/03_services/20_layer-unwrap.ts +66 -0
  16. package/ai-docs/src/01_effect/03_services/index.md +5 -0
  17. package/ai-docs/src/01_effect/04_errors/01_error-handling.ts +30 -0
  18. package/ai-docs/src/01_effect/04_errors/10_catch-tags.ts +24 -0
  19. package/ai-docs/src/01_effect/04_errors/20_reason-errors.ts +64 -0
  20. package/ai-docs/src/01_effect/04_errors/index.md +1 -0
  21. package/ai-docs/src/01_effect/05_resources/10_acquire-release.ts +105 -0
  22. package/ai-docs/src/01_effect/05_resources/20_layer-side-effects.ts +31 -0
  23. package/ai-docs/src/01_effect/05_resources/30_layer-map.ts +86 -0
  24. package/ai-docs/src/01_effect/05_resources/index.md +3 -0
  25. package/ai-docs/src/01_effect/06_running/10_run-main.ts +30 -0
  26. package/ai-docs/src/01_effect/06_running/20_layer-launch.ts +27 -0
  27. package/ai-docs/src/01_effect/06_running/index.md +1 -0
  28. package/ai-docs/src/01_effect/07_pubsub/10_pubsub.ts +56 -0
  29. package/ai-docs/src/01_effect/07_pubsub/index.md +3 -0
  30. package/ai-docs/src/03_stream/10_creating-streams.ts +103 -0
  31. package/ai-docs/src/03_stream/20_consuming-streams.ts +137 -0
  32. package/ai-docs/src/03_stream/30_encoding.ts +165 -0
  33. package/ai-docs/src/03_stream/index.md +4 -0
  34. package/ai-docs/src/04_integration/10_managed-runtime.ts +129 -0
  35. package/ai-docs/src/04_integration/index.md +5 -0
  36. package/ai-docs/src/05_batching/10_request-resolver.ts +89 -0
  37. package/ai-docs/src/05_batching/index.md +3 -0
  38. package/ai-docs/src/06_schedule/10_schedules.ts +110 -0
  39. package/ai-docs/src/06_schedule/index.md +3 -0
  40. package/ai-docs/src/07_datetime/10_creating-and-formatting.ts +30 -0
  41. package/ai-docs/src/07_datetime/20_time-zones.ts +44 -0
  42. package/ai-docs/src/07_datetime/index.md +5 -0
  43. package/ai-docs/src/08_observability/10_logging.ts +66 -0
  44. package/ai-docs/src/08_observability/20_otlp-tracing.ts +95 -0
  45. package/ai-docs/src/08_observability/index.md +7 -0
  46. package/ai-docs/src/09_testing/10_effect-tests.ts +55 -0
  47. package/ai-docs/src/09_testing/20_layer-tests.ts +138 -0
  48. package/ai-docs/src/09_testing/index.md +1 -0
  49. package/ai-docs/src/10_predicate/01_basics.ts +14 -0
  50. package/ai-docs/src/10_predicate/index.md +9 -0
  51. package/ai-docs/src/50_http-client/10_basics.ts +102 -0
  52. package/ai-docs/src/50_http-client/index.md +3 -0
  53. package/ai-docs/src/51_http-server/10_basics.ts +116 -0
  54. package/ai-docs/src/51_http-server/fixtures/api/Api.ts +14 -0
  55. package/ai-docs/src/51_http-server/fixtures/api/Authorization.ts +36 -0
  56. package/ai-docs/src/51_http-server/fixtures/api/System.ts +10 -0
  57. package/ai-docs/src/51_http-server/fixtures/api/Users.ts +91 -0
  58. package/ai-docs/src/51_http-server/fixtures/domain/User.ts +12 -0
  59. package/ai-docs/src/51_http-server/fixtures/domain/UserErrors.ts +22 -0
  60. package/ai-docs/src/51_http-server/fixtures/server/Authorization.ts +36 -0
  61. package/ai-docs/src/51_http-server/fixtures/server/Users/http.ts +71 -0
  62. package/ai-docs/src/51_http-server/fixtures/server/Users.ts +62 -0
  63. package/ai-docs/src/51_http-server/index.md +3 -0
  64. package/ai-docs/src/60_child-process/10_working-with-child-processes.ts +117 -0
  65. package/ai-docs/src/60_child-process/index.md +3 -0
  66. package/ai-docs/src/70_cli/10_basics.ts +136 -0
  67. package/ai-docs/src/70_cli/index.md +5 -0
  68. package/ai-docs/src/71_ai/10_language-model.ts +156 -0
  69. package/ai-docs/src/71_ai/20_tools.ts +226 -0
  70. package/ai-docs/src/71_ai/30_chat.ts +158 -0
  71. package/ai-docs/src/71_ai/fixtures/domain/LaunchPlan.ts +9 -0
  72. package/ai-docs/src/71_ai/index.md +5 -0
  73. package/ai-docs/src/80_cluster/10_entities.ts +97 -0
  74. package/ai-docs/src/80_cluster/index.md +4 -0
  75. package/ai-docs/src/index.md +10 -0
  76. package/ai-docs/tsconfig.json +24 -0
  77. package/dist/OpfsWorker.d.ts +1 -1
  78. package/dist/OpfsWorker.d.ts.map +1 -1
  79. package/dist/OpfsWorker.js +2 -2
  80. package/dist/OpfsWorker.js.map +1 -1
  81. package/dist/SqliteClient.d.ts +2 -2
  82. package/dist/SqliteClient.d.ts.map +1 -1
  83. package/dist/SqliteClient.js +15 -6
  84. package/dist/SqliteClient.js.map +1 -1
  85. package/dist/SqliteMigrator.d.ts +2 -2
  86. package/dist/SqliteMigrator.js +2 -2
  87. package/dist/SqliteMigrator.js.map +1 -1
  88. package/dist/index.js.map +1 -1
  89. package/package.json +10 -8
  90. package/src/OpfsWorker.ts +5 -2
  91. package/src/SqliteClient.ts +16 -8
  92. package/src/SqliteMigrator.ts +2 -2
@@ -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,165 @@
1
+ /**
2
+ * @title Decoding and encoding streams
3
+ *
4
+ * Use `Stream.pipeThroughChannel` with the `Ndjson` & `Msgpack` modules to
5
+ * decode and encode streams of structured data.
6
+ */
7
+ import { DateTime, Schema, Stream } from "effect"
8
+ import { Msgpack, Ndjson } from "effect/unstable/encoding"
9
+
10
+ // All of the examples below can also be done with Msgpack by replacing `Ndjson`
11
+ // with `Msgpack` and using the appropriate channels (`Msgpack.decode()`,
12
+ // `Msgpack.encode()`, etc.).
13
+ export const msgpackDecoder = Msgpack.decodeSchema(Schema.Struct({
14
+ id: Schema.Int,
15
+ name: Schema.String
16
+ }))
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Domain
20
+ // ---------------------------------------------------------------------------
21
+
22
+ // A log entry schema representing structured log events. In practice these
23
+ // would come from a file, HTTP body, or socket connection.
24
+ // `DateTimeUtcFromString` decodes an ISO-8601 string into a `DateTime.Utc`.
25
+ class LogEntry extends Schema.Class<LogEntry>("LogEntry")({
26
+ timestamp: Schema.DateTimeUtcFromString,
27
+ level: Schema.Literals(["info", "warn", "error"]),
28
+ message: Schema.String
29
+ }) {}
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Decoding NDJSON strings → objects
33
+ // ---------------------------------------------------------------------------
34
+
35
+ // Suppose we receive raw NDJSON text from a file or network socket.
36
+ // `Ndjson.decodeString()` is a Channel that splits incoming strings on
37
+ // newlines and `JSON.parse`s each line.
38
+ // Pipe the stream through the channel with `Stream.pipeThroughChannel`.
39
+ export const decodeUntyped = Stream.make(
40
+ "{\"timestamp\":\"2025-06-01T00:00:00Z\",\"level\":\"info\",\"message\":\"start\"}\n" +
41
+ "{\"timestamp\":\"2025-06-01T00:00:01Z\",\"level\":\"error\",\"message\":\"oops\"}\n"
42
+ ).pipe(
43
+ Stream.pipeThroughChannel(Ndjson.decodeString()),
44
+ Stream.runCollect
45
+ )
46
+
47
+ // When you need schema validation on top of the raw JSON parse, use
48
+ // `Ndjson.decodeSchemaString(Schema)()`. This decodes each line, parses the
49
+ // JSON, and then validates each value against the schema — all in one channel.
50
+ export const decodeTyped = Stream.make(
51
+ "{\"timestamp\":\"2025-06-01T00:00:00Z\",\"level\":\"info\",\"message\":\"start\"}\n" +
52
+ "{\"timestamp\":\"2025-06-01T00:00:01Z\",\"level\":\"error\",\"message\":\"oops\"}\n"
53
+ ).pipe(
54
+ Stream.pipeThroughChannel(Ndjson.decodeSchemaString(LogEntry)()),
55
+ Stream.runCollect
56
+ )
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Encoding objects → NDJSON strings
60
+ // ---------------------------------------------------------------------------
61
+
62
+ // `Ndjson.encodeString()` serialises each value to a JSON line.
63
+ // The resulting stream emits ready-to-write NDJSON strings.
64
+ export const encodeUntyped = Stream.make(
65
+ { timestamp: "2025-06-01T00:00:00Z", level: "info", message: "start" },
66
+ { timestamp: "2025-06-01T00:00:01Z", level: "error", message: "oops" }
67
+ ).pipe(
68
+ Stream.pipeThroughChannel(Ndjson.encodeString()),
69
+ Stream.runCollect
70
+ )
71
+
72
+ // `Ndjson.encodeSchemaString(Schema)()` encodes each value through the schema
73
+ // first (applying any transformations such as date formatting), then
74
+ // serialises it to an NDJSON line.
75
+ export const encodeTyped = Stream.make(
76
+ new LogEntry({
77
+ timestamp: DateTime.makeUnsafe("2025-06-01T00:00:00Z"),
78
+ level: "info",
79
+ message: "start"
80
+ }),
81
+ new LogEntry({
82
+ timestamp: DateTime.makeUnsafe("2025-06-01T00:00:01Z"),
83
+ level: "error",
84
+ message: "oops"
85
+ })
86
+ ).pipe(
87
+ Stream.pipeThroughChannel(Ndjson.encodeSchemaString(LogEntry)()),
88
+ Stream.runCollect
89
+ )
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // Binary (Uint8Array) variants
93
+ // ---------------------------------------------------------------------------
94
+
95
+ // When working with binary I/O (e.g. TCP sockets, file descriptors) use the
96
+ // non-string variants. `Ndjson.decode()` expects `Uint8Array` chunks and
97
+ // handles text decoding internally. `Ndjson.encode()` produces `Uint8Array`
98
+ // output.
99
+ const enc = new TextEncoder()
100
+
101
+ export const decodeBinary = Stream.make(
102
+ enc.encode("{\"level\":\"info\",\"message\":\"binary\"}\n")
103
+ ).pipe(
104
+ Stream.pipeThroughChannel(Ndjson.decode()),
105
+ Stream.runCollect
106
+ )
107
+
108
+ export const encodeBinary = Stream.make(
109
+ { level: "info", message: "binary" }
110
+ ).pipe(
111
+ Stream.pipeThroughChannel(Ndjson.encode()),
112
+ Stream.runCollect
113
+ )
114
+
115
+ // ---------------------------------------------------------------------------
116
+ // Handling empty lines
117
+ // ---------------------------------------------------------------------------
118
+
119
+ // NDJSON files sometimes contain blank lines (e.g. trailing newlines or
120
+ // pretty-printed output). Pass `{ ignoreEmptyLines: true }` to skip them
121
+ // instead of raising an `NdjsonError`.
122
+ export const decodeIgnoringBlanks = Stream.make(
123
+ "{\"ok\":true}\n\n{\"ok\":false}\n"
124
+ ).pipe(
125
+ Stream.pipeThroughChannel(Ndjson.decodeString({ ignoreEmptyLines: true })),
126
+ Stream.runCollect
127
+ )
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // Error handling
131
+ // ---------------------------------------------------------------------------
132
+
133
+ // `Ndjson.NdjsonError` is raised when encoding (`kind: "Pack"`) or decoding
134
+ // (`kind: "Unpack"`) fails. You can catch it with `Stream.catchTag` or
135
+ // `Effect.catchTag`.
136
+ export const handleDecodeErrors = Stream.make("not-valid-json\n").pipe(
137
+ Stream.pipeThroughChannel(Ndjson.decodeString()),
138
+ Stream.catchTag("NdjsonError", (err) =>
139
+ // The `kind` field indicates whether the error occurred during
140
+ // encoding ("Pack") or decoding ("Unpack"), and `cause` contains
141
+ // the underlying exception.
142
+ Stream.succeed({ recovered: true, kind: err.kind })),
143
+ Stream.runCollect
144
+ )
145
+
146
+ // ---------------------------------------------------------------------------
147
+ // Realistic pipeline: decode → transform → re-encode
148
+ // ---------------------------------------------------------------------------
149
+
150
+ // A common pattern is to read NDJSON, transform each record, and write it
151
+ // back as NDJSON. This example filters error-level log entries and re-encodes
152
+ // them.
153
+ const ndjsonInput = "{\"timestamp\":\"2025-06-01T00:00:00Z\",\"level\":\"info\",\"message\":\"ok\"}\n" +
154
+ "{\"timestamp\":\"2025-06-01T00:00:01Z\",\"level\":\"error\",\"message\":\"fail\"}\n" +
155
+ "{\"timestamp\":\"2025-06-01T00:00:02Z\",\"level\":\"warn\",\"message\":\"slow\"}\n"
156
+
157
+ export const filterAndReencode = Stream.make(ndjsonInput).pipe(
158
+ // Decode each line into a validated LogEntry
159
+ Stream.pipeThroughChannel(Ndjson.decodeSchemaString(LogEntry)()),
160
+ // Keep only error-level entries
161
+ Stream.filter((entry) => entry.level === "error"),
162
+ // Re-encode the filtered entries back to NDJSON strings
163
+ Stream.pipeThroughChannel(Ndjson.encodeSchemaString(LogEntry)()),
164
+ Stream.runCollect
165
+ )
@@ -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.
@@ -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.