@effect/sql-sqlite-wasm 4.0.0-beta.98 → 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.
- package/AGENTS.md +381 -0
- package/CLAUDE.md +381 -0
- package/README.md +10 -3
- 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 +39 -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 +5 -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 +165 -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 +110 -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/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 +116 -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 +91 -0
- package/ai-docs/src/51_http-server/fixtures/domain/User.ts +12 -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 +36 -0
- package/ai-docs/src/51_http-server/fixtures/server/Users/http.ts +71 -0
- package/ai-docs/src/51_http-server/fixtures/server/Users.ts +62 -0
- package/ai-docs/src/51_http-server/index.md +3 -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 +136 -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 +24 -0
- package/dist/OpfsWorker.d.ts +1 -1
- package/dist/OpfsWorker.d.ts.map +1 -1
- package/dist/OpfsWorker.js +2 -2
- package/dist/OpfsWorker.js.map +1 -1
- package/dist/SqliteClient.d.ts +2 -2
- package/dist/SqliteClient.d.ts.map +1 -1
- package/dist/SqliteClient.js +15 -6
- package/dist/SqliteClient.js.map +1 -1
- package/dist/SqliteMigrator.d.ts +2 -2
- package/dist/SqliteMigrator.js +2 -2
- package/dist/SqliteMigrator.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +12 -10
- package/src/OpfsWorker.ts +5 -2
- package/src/SqliteClient.ts +16 -8
- 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,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,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.
|