@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.
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 +12 -10
  90. package/src/OpfsWorker.ts +5 -2
  91. package/src/SqliteClient.ts +16 -8
  92. package/src/SqliteMigrator.ts +2 -2
@@ -0,0 +1,89 @@
1
+ /**
2
+ * @title Batching requests with RequestResolver
3
+ *
4
+ * Define request types with `Request.Class`, resolve them in batches with `RequestResolver`.
5
+ */
6
+ import { Context, Effect, Exit, Layer, Request, RequestResolver, Schema, Tracer } from "effect"
7
+
8
+ export class User extends Schema.Class<User>("User")({
9
+ id: Schema.Int,
10
+ name: Schema.String,
11
+ email: Schema.String
12
+ }) {}
13
+
14
+ export class UserNotFound extends Schema.TaggedError<UserNotFound>()("UserNotFound", {
15
+ id: Schema.Int
16
+ }) {}
17
+
18
+ export class Users extends Context.Service<Users, {
19
+ getUserById(id: number): Effect.Effect<User, UserNotFound>
20
+ }>()("app/Users") {
21
+ static readonly layer = Layer.effect(
22
+ Users,
23
+ Effect.gen(function*() {
24
+ // Request classes model a single external lookup.
25
+ class GetUserById extends Request.Class<
26
+ { readonly id: number },
27
+ User, // The success type of the request
28
+ UserNotFound, // The error type of the request
29
+ never // The requirements type of the request, if any
30
+ > {}
31
+
32
+ // Simulate an external data source that supports batched lookup.
33
+ const usersTable = new Map<number, User>([
34
+ [1, new User({ id: 1, name: "Ada Lovelace", email: "ada@acme.dev" })],
35
+ [2, new User({ id: 2, name: "Alan Turing", email: "alan@acme.dev" })],
36
+ [3, new User({ id: 3, name: "Grace Hopper", email: "grace@acme.dev" })]
37
+ ])
38
+
39
+ const resolver = yield* RequestResolver.make<GetUserById>(Effect.fn(function*(entries) {
40
+ for (const entry of entries) {
41
+ const user = usersTable.get(entry.request.id)
42
+
43
+ // If the request had requirements, you can access them with
44
+ // `entry.context`
45
+ const requestSpan = Context.getOption(entry.context, Tracer.ParentSpan)
46
+ console.log("Request span", requestSpan)
47
+
48
+ if (user) {
49
+ // Complete requests with .completeUnsafe and pass in an Exit value
50
+ entry.completeUnsafe(Exit.succeed(user))
51
+ } else {
52
+ entry.completeUnsafe(Exit.fail(new UserNotFound({ id: entry.request.id })))
53
+ }
54
+ }
55
+ })).pipe(
56
+ // Control the delay before the resolver is executed. This allows more
57
+ // requests to be batched together, but also adds latency to the first
58
+ // request.
59
+ RequestResolver.setDelay("10 millis"),
60
+ // RequestResolver.withSpan adds a span around the resolver execution,
61
+ // and also sets up span links for each request
62
+ RequestResolver.withSpan("Users.getUserById.resolver"),
63
+ // RequestResolver.withCache adds a simple LRU cache to avoid repeated
64
+ // lookups for the same ID.
65
+ RequestResolver.withCache({ capacity: 1024 })
66
+ )
67
+
68
+ // Wrap the resolver in a service method. The resolver batches calls to
69
+ // `getUserById` that occur within the delay window.
70
+ const getUserById = (id: number) =>
71
+ Effect.request(new GetUserById({ id }), resolver).pipe(
72
+ Effect.withSpan("Users.getUserById", { attributes: { userId: id } })
73
+ )
74
+
75
+ return { getUserById } as const
76
+ })
77
+ )
78
+ }
79
+
80
+ // Run multiple lookups concurrently. The resolver receives one batch and
81
+ // internally deduplicates repeated IDs for the external call.
82
+ export const batchedLookupExample = Effect.gen(function*() {
83
+ const { getUserById } = yield* Users
84
+
85
+ // This will only trigger a single call to the resolver with the unique IDs [1, 2, 3].
86
+ yield* Effect.forEach([1, 2, 1, 3, 2], getUserById, {
87
+ concurrency: "unbounded"
88
+ })
89
+ })
@@ -0,0 +1,3 @@
1
+ ## Batching external requests
2
+
3
+ Learn how to batch multiple requests into fewer external calls.
@@ -0,0 +1,110 @@
1
+ /**
2
+ * @title Working with the Schedule module
3
+ *
4
+ * Build schedules, compose them, and use them with `Effect.retry` and `Effect.repeat`.
5
+ */
6
+ import { Duration, Effect, Random, Schedule, Schema } from "effect"
7
+
8
+ // Production pattern: capped exponential backoff with jitter and max attempts.
9
+ // Delays start at 250ms, grow exponentially with jitter, and are capped at 10s.
10
+ export const productionRetrySchedule = Schedule.min([
11
+ Schedule.exponential("250 millis"),
12
+ // Cap the delay at 10 seconds to avoid excessively long waits.
13
+ Schedule.spaced("10 seconds")
14
+ ]).pipe(
15
+ Schedule.jittered,
16
+ Schedule.setInputType<HttpError>(),
17
+ Schedule.while(({ input }) => input.retryable)
18
+ )
19
+
20
+ export const fetchUserProfile = Effect.fn("fetchUserProfile")(
21
+ function*(userId: string) {
22
+ const random = yield* Random.next
23
+ const status = random > 0.7
24
+ ? 200
25
+ : random > 0.3
26
+ ? 503
27
+ : 401
28
+
29
+ if (status !== 200) {
30
+ return yield* new HttpError({
31
+ message: `Request for ${userId} failed`,
32
+ status,
33
+ retryable: status >= 500
34
+ })
35
+ }
36
+
37
+ return {
38
+ id: userId,
39
+ name: "Ada Lovelace"
40
+ } as const
41
+ }
42
+ )
43
+
44
+ // Use the schedule with `Effect.retry` to retry failures.
45
+ export const loadUserWithRetry = fetchUserProfile("user-123").pipe(
46
+ Effect.retry(productionRetrySchedule),
47
+ // If the effect still fails after exhausting the schedule, turn the error
48
+ // into a fatal one.
49
+ Effect.orDie
50
+ )
51
+
52
+ export const loadUserWithInferredInput = fetchUserProfile("user-123").pipe(
53
+ // You can also pass a schedule builder function that assists with inferring
54
+ // the input type. This is especially useful when the schedule needs to
55
+ // inspect the error to determine retryability.
56
+ Effect.retry(($) =>
57
+ $(Schedule.spaced("1 seconds")).pipe(
58
+ Schedule.while(({ input }) => input.retryable)
59
+ )
60
+ ),
61
+ Effect.orDie
62
+ )
63
+
64
+ export class HttpError extends Schema.TaggedError<HttpError>()("HttpError", {
65
+ message: Schema.String,
66
+ status: Schema.Int,
67
+ retryable: Schema.Boolean
68
+ }) {}
69
+
70
+ // Start with a few schedule constructors.
71
+ export const maxRetries = Schedule.recurs(5)
72
+ export const spacedPolling = Schedule.spaced("30 seconds")
73
+ export const exponentialBackoff = Schedule.exponential("200 millis")
74
+
75
+ // `Schedule.max` continues only while all schedules continue and outputs
76
+ // the slowest delay. It is useful for combining a delay pattern with a hard
77
+ // attempt cap.
78
+ export const retryBackoffWithLimit = Schedule.max([
79
+ Schedule.exponential("250 millis"),
80
+ Schedule.recurs(6)
81
+ ])
82
+
83
+ // `Schedule.min` continues while any schedule continues and outputs
84
+ // the fastest delay. It is useful for fallback behavior.
85
+ //
86
+ // This schedule will start at 500ms, exponentially increase, and will be capped
87
+ // at 2 seconds.
88
+ export const keepTryingUpToTwoSeconds = Schedule.min([
89
+ Schedule.exponential("500 millis"),
90
+ Schedule.spaced("2 seconds")
91
+ ])
92
+
93
+ // Use `Schedule.while` to continue only for retryable failures.
94
+ // This lets non-retryable errors fail fast, even if attempts remain.
95
+ export const retryableOnly = Schedule.exponential("200 millis").pipe(
96
+ // You can use `setInputType` to specify the type of input the schedule will
97
+ // receive.
98
+ Schedule.setInputType<HttpError>(),
99
+ Schedule.while(({ input }) => input.retryable)
100
+ )
101
+
102
+ // `tap` is useful for performing side effects like logging or metrics.
103
+ export const instrumentedRetrySchedule = retryableOnly.pipe(
104
+ Schedule.setInputType<HttpError>(),
105
+ Schedule.tap((meta) =>
106
+ Effect.logDebug(
107
+ `Retrying after ${meta.input.status}: ${meta.input.message}. Next retry in ${Duration.toMillis(meta.duration)}ms`
108
+ )
109
+ )
110
+ )
@@ -0,0 +1,3 @@
1
+ ## Working with Schedules
2
+
3
+ Schedules define recurring patterns for retries, repeats and polling.
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @title Creating and formatting DateTime values
3
+ *
4
+ * Parse incoming date values safely, use Clock-powered current time, and format
5
+ * instants for API payloads or user-facing labels.
6
+ */
7
+ import { DateTime, Effect, Option } from "effect"
8
+
9
+ Effect.gen(function*() {
10
+ // Use DateTime.now to get the current time from Effect's Clock service.
11
+ // Using the Clock service ensures tests can use the `TestClock` module to
12
+ // control time.
13
+ const now = yield* DateTime.now
14
+
15
+ // Use DateTime.make to parse a date input, such as a user-entered string or a
16
+ // epoch timestamp. It returns an Option depending on whether the input was
17
+ // valid.
18
+ const parsedOption: Option.Option<DateTime.Utc> = DateTime.make("2024-06-15T14:30:00.000Z")
19
+
20
+ // you can then use the Option apis to unwrap the value
21
+ Option.getOrUndefined(parsedOption)
22
+
23
+ // Calendar/date-time math returns a new DateTime value; the original value is
24
+ // immutable.
25
+ const endsAt = now.pipe(DateTime.add({ hours: 2 }))
26
+
27
+ // The DateTime.format* functions can be used to convert a DateTime value to
28
+ // differen formats.
29
+ yield* Effect.log("ISO string:", DateTime.formatIso(endsAt))
30
+ })
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @title Working with time zones
3
+ *
4
+ * Attach IANA zones to instants, render zoned ISO strings, and provide a
5
+ * CurrentTimeZone service for code that should use the workspace/user zone.
6
+ */
7
+ import { NodeRuntime } from "@effect/platform-node"
8
+ import { DateTime, Effect, Option } from "effect"
9
+
10
+ Effect.gen(function*() {
11
+ // Use DateTime.now to get the current time from Effect's Clock service.
12
+ const now = yield* DateTime.now
13
+
14
+ // To attach a named IANA zone to a DateTime value
15
+ const nowInAuckland = now.pipe(
16
+ // Use DateTime.setZoneNamedUnsafe when you know the zone is valid.
17
+ DateTime.setZoneNamedUnsafe("Pacific/Auckland")
18
+ )
19
+ yield* Effect.log("Now in Auckland:", nowInAuckland)
20
+
21
+ // Use DateTime.setZoneNamed when you don't know the zone is valid.
22
+ const nowInSydneyOption: Option.Option<DateTime.Zoned> = now.pipe(
23
+ DateTime.setZoneNamed("Australia/Sydney")
24
+ )
25
+
26
+ yield* Effect.log("Now in Sydney:", Option.getOrUndefined(nowInSydneyOption))
27
+
28
+ // To generate a `DateTime.Zoned` in the `DateTime.CurrentTimeZone`
29
+ const nowInNewYork = yield* DateTime.nowInCurrentZone
30
+ yield* Effect.log("Now in New York:", nowInNewYork)
31
+
32
+ // If you have a date string that you know is in a particular IANA zone, you
33
+ // can convert it to a DateTime.Zoned to ensure the instant is correct
34
+ const dateInAuckland: DateTime.Zoned = DateTime.makeZonedUnsafe("2026-06-05", {
35
+ timeZone: "Pacific/Auckland",
36
+ // adjustForTimeZone will adjust the input to the given zone, otherwise it
37
+ // will be treated as UTC.
38
+ adjustForTimeZone: true
39
+ })
40
+ yield* Effect.log("Date in Auckland:", dateInAuckland)
41
+ }).pipe(
42
+ Effect.provide(DateTime.layerCurrentZoneNamed("America/New_York")),
43
+ NodeRuntime.runMain
44
+ )
@@ -0,0 +1,5 @@
1
+ ## Working with DateTime
2
+
3
+ When working with dates and time, use the `DateTime` module instead of `Date` and `Date.now`.
4
+
5
+ Use it when your Effect programs need testable current time, safe parsing, stable ISO formatting, time-zone conversion, or calendar arithmetic.
@@ -0,0 +1,66 @@
1
+ /**
2
+ * @title Customizing logging
3
+ *
4
+ * Configure loggers & log-level filtering for production applications.
5
+ */
6
+ import { NodeFileSystem } from "@effect/platform-node"
7
+ import { Config, Effect, Layer, Logger, References } from "effect"
8
+
9
+ // Build a logger layer that emits one JSON line per log entry.
10
+ export const JsonLoggerLayer = Logger.layer([Logger.consoleJson])
11
+
12
+ // Raise the minimum level to "Warn" to skip debug/info logs.
13
+ export const WarnAndAbove = Layer.succeed(References.MinimumLogLevel, "Warn")
14
+
15
+ // There is a built-in logger for writing to a file
16
+ export const FileLoggerLayer = Logger.layer([
17
+ Logger.toFile(Logger.formatSimple, "app.log")
18
+ ]).pipe(
19
+ Layer.provide(NodeFileSystem.layer)
20
+ )
21
+
22
+ // Define a custom logger for app-specific formatting and routing.
23
+ export const appLogger = Effect.gen(function*() {
24
+ // Here you could initialize a connection to an external logging service, set
25
+ // up log file rotation, etc.
26
+ yield* Effect.logDebug("initializing app logger")
27
+
28
+ return yield* Logger.batched(Logger.formatStructured, {
29
+ window: "1 second",
30
+ flush: Effect.fn(function*(batch) {
31
+ // In a real implementation, this is where you would send the batch of log entries to an external logging service or write them to a file.
32
+ console.log(`Flushing ${batch.length} log entries`)
33
+ })
34
+ })
35
+ })
36
+
37
+ export const AppLoggerLayer = Logger.layer([appLogger]).pipe(
38
+ Layer.provideMerge(WarnAndAbove) // Start with "Warn" level for the app logger.
39
+ )
40
+
41
+ // Create a logger layer that uses the default logger for development, and the
42
+ // custom logger for production
43
+ export const LoggerLayer = Layer.unwrap(Effect.gen(function*() {
44
+ const env = yield* Config.string("NODE_ENV").pipe(Config.withDefault("development"))
45
+ if (env === "production") {
46
+ return AppLoggerLayer
47
+ }
48
+ return Logger.layer([Logger.defaultLogger])
49
+ }))
50
+
51
+ // Example effect that logs at various levels during a checkout flow.
52
+ export const logCheckoutFlow = Effect.gen(function*() {
53
+ yield* Effect.logDebug("loading checkout state")
54
+
55
+ yield* Effect.logInfo("validating cart")
56
+ yield* Effect.logWarning("inventory is low for one line item")
57
+ yield* Effect.logError("payment provider timeout")
58
+ }).pipe(
59
+ // Attach structured metadata to all log lines emitted by this effect.
60
+ Effect.annotateLogs({
61
+ service: "checkout-api",
62
+ route: "POST /checkout"
63
+ }),
64
+ // Add a duration span so each log line includes checkout=<N>ms metadata.
65
+ Effect.withLogSpan("checkout")
66
+ )
@@ -0,0 +1,95 @@
1
+ /**
2
+ * @title Setting up tracing with Otlp modules
3
+ *
4
+ * Configure Otlp tracing + log export with a reusable observability layer.
5
+ */
6
+ import { NodeRuntime } from "@effect/platform-node"
7
+ import { Context, Effect, Layer } from "effect"
8
+ import { FetchHttpClient } from "effect/unstable/http"
9
+ import { OtlpLogger, OtlpSerialization, OtlpTracer } from "effect/unstable/observability"
10
+
11
+ // Configure OTLP span export.
12
+ export const OtlpTracingLayer = OtlpTracer.layer({
13
+ url: "http://localhost:4318/v1/traces",
14
+ resource: {
15
+ serviceName: "checkout-api",
16
+ serviceVersion: "1.0.0",
17
+ attributes: {
18
+ "deployment.environment": "staging"
19
+ }
20
+ }
21
+ })
22
+
23
+ // Configure OTLP log export.
24
+ export const OtlpLoggingLayer = OtlpLogger.layer({
25
+ url: "http://localhost:4318/v1/logs",
26
+ resource: {
27
+ serviceName: "checkout-api",
28
+ serviceVersion: "1.0.0"
29
+ }
30
+ })
31
+
32
+ // Reusable app-wide observability layer.
33
+ //
34
+ // - OtlpTracer/OtlpLogger require an OTLP serializer and an HttpClient.
35
+ // - FetchHttpClient.layer provides the HttpClient used by the exporter.
36
+ export const ObservabilityLayer = Layer.merge(OtlpTracingLayer, OtlpLoggingLayer).pipe(
37
+ Layer.provide(OtlpSerialization.layerJson),
38
+ Layer.provide(FetchHttpClient.layer)
39
+ )
40
+
41
+ export class Checkout extends Context.Service<Checkout, {
42
+ processCheckout(orderId: string): Effect.Effect<void>
43
+ }>()("acme/Checkout") {
44
+ static readonly layer = Layer.effect(
45
+ Checkout,
46
+ Effect.gen(function*() {
47
+ yield* Effect.logInfo("setting up checkout service")
48
+
49
+ return Checkout.of({
50
+ processCheckout: Effect.fn("Checkout.processCheckout")(function*(orderId: string) {
51
+ yield* Effect.logInfo("starting checkout", { orderId })
52
+
53
+ yield* Effect.sleep("50 millis").pipe(
54
+ Effect.withSpan("checkout.charge-card"),
55
+ Effect.annotateSpans({
56
+ "checkout.order_id": orderId,
57
+ "checkout.provider": "acme-pay"
58
+ })
59
+ )
60
+
61
+ yield* Effect.sleep("20 millis").pipe(
62
+ Effect.withSpan("checkout.persist-order")
63
+ )
64
+
65
+ yield* Effect.logInfo("checkout completed", { orderId })
66
+ })
67
+ })
68
+ })
69
+ )
70
+ }
71
+
72
+ // Example usage of the Checkout service.
73
+ const CheckoutTest = Layer.effectDiscard(
74
+ Effect.gen(function*() {
75
+ const checkout = yield* Checkout
76
+ yield* checkout.processCheckout("ord_123")
77
+ }).pipe(
78
+ Effect.withSpan("checkout-test-run")
79
+ )
80
+ ).pipe(
81
+ // You can also attach spans to Layers
82
+ Layer.withSpan("checkout-test"),
83
+ Layer.provide(Checkout.layer)
84
+ )
85
+
86
+ const Main = CheckoutTest.pipe(
87
+ // Provide the observability layer at the very end, so that all spans created
88
+ // by the app are exported.
89
+ Layer.provide(ObservabilityLayer)
90
+ )
91
+
92
+ // Launch the app
93
+ Layer.launch(Main).pipe(
94
+ NodeRuntime.runMain
95
+ )
@@ -0,0 +1,7 @@
1
+ ## Observability
2
+
3
+ Effect has built-in support for structured logging, distributed tracing, and
4
+ metrics. For exporting telemetry, use the lightweight Otlp modules from
5
+ `effect/unstable/observability` in new projects, or use
6
+ `@effect/opentelemetry` NodeSdk when integrating with an existing OpenTelemetry
7
+ setup.
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @title Writing Effect tests with @effect/vitest
3
+ *
4
+ * Using `it.effect` for Effect-based tests.
5
+ */
6
+ import { assert, describe, it } from "@effect/vitest"
7
+ import { Effect, Fiber, Schema } from "effect"
8
+ import { TestClock } from "effect/testing"
9
+
10
+ describe("@effect/vitest basics", () => {
11
+ it.effect("runs Effect code with assert helpers", () =>
12
+ Effect.gen(function*() {
13
+ const upper = ["ada", "lin"].map((name) => name.toUpperCase())
14
+ assert.deepStrictEqual(upper, ["ADA", "LIN"])
15
+ assert.strictEqual(upper.length, 2)
16
+ assert.isTrue(upper.includes("ADA"))
17
+ }))
18
+
19
+ it.effect.each([
20
+ { input: " Ada ", expected: "ada" },
21
+ { input: " Lin ", expected: "lin" },
22
+ { input: " Nia ", expected: "nia" }
23
+ ])("parameterized normalization %#", ({ input, expected }) =>
24
+ Effect.gen(function*() {
25
+ assert.strictEqual(input.trim().toLowerCase(), expected)
26
+ }))
27
+
28
+ it.effect("controls time with TestClock", () =>
29
+ Effect.gen(function*() {
30
+ const fiber = yield* Effect.forkChild(
31
+ Effect.sleep(60_000).pipe(Effect.as("done" as const))
32
+ )
33
+
34
+ // Move virtual time forward to complete sleeping fibers immediately.
35
+ yield* TestClock.adjust(60_000)
36
+
37
+ const value = yield* Fiber.join(fiber)
38
+ assert.strictEqual(value, "done")
39
+ }))
40
+
41
+ it.live("uses real runtime services", () =>
42
+ Effect.gen(function*() {
43
+ const startedAt = Date.now()
44
+ yield* Effect.sleep(1)
45
+ assert.isTrue(Date.now() >= startedAt)
46
+ }))
47
+
48
+ // For property-based testing, use `it.effect.prop` with Schema-based
49
+ // arbitraries
50
+ it.effect.prop("reversing twice is identity", [Schema.String], ([value]) =>
51
+ Effect.gen(function*() {
52
+ const reversedTwice = value.split("").reverse().reverse().join("")
53
+ assert.strictEqual(reversedTwice, value)
54
+ }))
55
+ })
@@ -0,0 +1,138 @@
1
+ /**
2
+ * @title Testing services with shared layers
3
+ *
4
+ * How to test Effect services that depend on other services.
5
+ */
6
+ import { assert, describe, it, layer } from "@effect/vitest"
7
+ import { Array, Context, Effect, Layer, Ref } from "effect"
8
+
9
+ export interface Todo {
10
+ readonly id: number
11
+ readonly title: string
12
+ }
13
+
14
+ // Create a test ref service that can be used to store and manipulate test data
15
+ // in layers.
16
+ export class TodoRepoTestRef extends Context.Service<TodoRepoTestRef, Ref.Ref<Array<Todo>>>()("app/TodoRepoTestRef") {
17
+ static readonly layer = Layer.effect(TodoRepoTestRef, Ref.make(Array.empty()))
18
+ }
19
+
20
+ class TodoRepo extends Context.Service<TodoRepo, {
21
+ create(title: string): Effect.Effect<Todo>
22
+ readonly list: Effect.Effect<ReadonlyArray<Todo>>
23
+ }>()("app/TodoRepo") {
24
+ static readonly layerTest = Layer.effect(
25
+ TodoRepo,
26
+ Effect.gen(function*() {
27
+ const store = yield* TodoRepoTestRef
28
+
29
+ const create = Effect.fn("TodoRepo.create")(function*(title: string) {
30
+ const todos = yield* Ref.get(store)
31
+ const todo = { id: todos.length + 1, title }
32
+ yield* Ref.set(store, [...todos, todo])
33
+ return todo
34
+ })
35
+
36
+ const list = Ref.get(store)
37
+
38
+ return TodoRepo.of({
39
+ create,
40
+ list
41
+ })
42
+ })
43
+ ).pipe(
44
+ // Provide the test ref layer as a dependency for the test repo layer.
45
+ // Use Layer.provideMerge so the tests can also access the test ref directly
46
+ // if needed.
47
+ Layer.provideMerge(TodoRepoTestRef.layer)
48
+ )
49
+ }
50
+
51
+ class TodoService extends Context.Service<TodoService, {
52
+ addAndCount(title: string): Effect.Effect<number>
53
+ readonly titles: Effect.Effect<ReadonlyArray<string>>
54
+ }>()("app/TodoService") {
55
+ static readonly layerNoDeps = Layer.effect(
56
+ TodoService,
57
+ Effect.gen(function*() {
58
+ const repo = yield* TodoRepo
59
+
60
+ const addAndCount = Effect.fn("TodoService.addAndCount")(function*(title: string) {
61
+ yield* repo.create(title)
62
+ const todos = yield* repo.list
63
+ return todos.length
64
+ })
65
+
66
+ const titles = repo.list.pipe(
67
+ Effect.map((todos) => todos.map((todo) => todo.title))
68
+ )
69
+
70
+ return TodoService.of({
71
+ addAndCount,
72
+ titles
73
+ })
74
+ })
75
+ )
76
+
77
+ // You would also add a live layer here that provides real dependencies for
78
+ // production code.
79
+ //
80
+ // static readonly layer = Layer.effect(TodoService, ...).pipe(
81
+ // Layer.provide(TodoRepo.layer)
82
+ // )
83
+
84
+ static readonly layerTest = this.layerNoDeps.pipe(
85
+ // Provide the test repo layer as a dependency for the test service layer.
86
+ // Use `Layer.provideMerge` so the tests can also access the test repo
87
+ // directly if needed, as well as the test ref through the repo layer.
88
+ Layer.provideMerge(TodoRepo.layerTest)
89
+ )
90
+ }
91
+
92
+ // `layer(...)` creates one shared layer for the block and tears it down in
93
+ // `afterAll`, so all tests inside can access the same service context.
94
+ layer(TodoRepo.layerTest)("TodoRepo", (it) => {
95
+ it.effect("tests repository behavior", () =>
96
+ Effect.gen(function*() {
97
+ const repo = yield* TodoRepo
98
+ const before = (yield* repo.list).length
99
+ assert.strictEqual(before, 0)
100
+
101
+ yield* repo.create("Write docs")
102
+
103
+ const after = (yield* repo.list).length
104
+ assert.strictEqual(after, 1)
105
+ }))
106
+
107
+ it.effect("layer is shared", () =>
108
+ Effect.gen(function*() {
109
+ const repo = yield* TodoRepo
110
+ const before = (yield* repo.list).length
111
+ assert.strictEqual(before, 1)
112
+
113
+ yield* repo.create("Write docs again")
114
+
115
+ // because the layer is shared between tests, the todo created in the
116
+ // previous test is still present, so the count should be 2, not 1
117
+ const after = (yield* repo.list).length
118
+ assert.strictEqual(after, 2)
119
+ }))
120
+ })
121
+
122
+ describe("TodoService", () => {
123
+ it.effect("tests higher-level service logic", () =>
124
+ Effect.gen(function*() {
125
+ const ref = yield* TodoRepoTestRef
126
+ const service = yield* TodoService
127
+ const count = yield* service.addAndCount("Review docs")
128
+ const titles = yield* service.titles
129
+
130
+ assert.isTrue(count >= 1)
131
+ assert.isTrue(titles.some((title) => title.includes("Review docs")))
132
+
133
+ // You can also access the test ref directly to make assertions about the
134
+ // underlying data.
135
+ const todos = yield* Ref.get(ref)
136
+ assert.isTrue(todos.length >= 1)
137
+ }).pipe(Effect.provide(TodoService.layerTest)))
138
+ })
@@ -0,0 +1 @@
1
+ ## Testing Effect programs
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @title Using the Predicate module
3
+ */
4
+ import { Predicate } from "effect"
5
+
6
+ const thing: unknown = {
7
+ a: 1
8
+ }
9
+
10
+ if (Predicate.isObject(thing)) {
11
+ if (Predicate.isNumber(thing.a)) {
12
+ console.log("number", thing.a)
13
+ }
14
+ }
@@ -0,0 +1,9 @@
1
+ ## Runtime type guards
2
+
3
+ The `Predicate` module contains small, reusable runtime checks.
4
+
5
+ **NEVER** write your own helper functions like `isRecord` or `isString`, instead
6
+ use the helpers from the `Predicate` module.
7
+
8
+ Predicates can be composed with apis such as `Predicate.and`,
9
+ `Predicate.or`, `Predicate.not`, and `Predicate.compose`.