@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.
- 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 +10 -8
- package/src/OpfsWorker.ts +5 -2
- package/src/SqliteClient.ts +16 -8
- 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,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,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,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,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`.
|