@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,7 @@
|
|
|
1
|
+
## Defining schemas and domain models
|
|
2
|
+
|
|
3
|
+
All validation and domain modeling in Effect is done with `Schema`.
|
|
4
|
+
|
|
5
|
+
**AVOID using predicates or manual parsing**, instead use `Schema` to parse untrusted data and validate it.
|
|
6
|
+
|
|
7
|
+
For a comprehensive guide, see [SCHEMA.md](https://github.com/Effect-TS/effect/blob/main/packages/effect/SCHEMA.md). Make sure to read the guide in chunks, as it is a large document.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Context.Service
|
|
3
|
+
*
|
|
4
|
+
* The default way to define a service is to extend `Context.Service`,
|
|
5
|
+
* passing in the service interface as a type parameter.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// file: src/db/Database.ts
|
|
9
|
+
import { Context, Effect, Layer, Schema } from "effect"
|
|
10
|
+
|
|
11
|
+
// Pass in the service class name as the first type parameter, and the service
|
|
12
|
+
// interface as the second type parameter.
|
|
13
|
+
export class Database extends Context.Service<Database, {
|
|
14
|
+
query(sql: string): Effect.Effect<Array<unknown>, DatabaseError>
|
|
15
|
+
}>()(
|
|
16
|
+
// The string identifier for the service, which should include the package
|
|
17
|
+
// name and the subdirectory path to the service file.
|
|
18
|
+
"myapp/db/Database"
|
|
19
|
+
) {
|
|
20
|
+
// Attach a static layer to the service, which will be used to provide an
|
|
21
|
+
// implementation of the service.
|
|
22
|
+
static readonly layer = Layer.effect(
|
|
23
|
+
Database,
|
|
24
|
+
Effect.gen(function*() {
|
|
25
|
+
// Define the service methods using Effect.fn
|
|
26
|
+
const query = Effect.fn("Database.query")(function*(sql: string) {
|
|
27
|
+
yield* Effect.log("Executing SQL query:", sql)
|
|
28
|
+
return [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }]
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
// Return an instance of the service using Database.of, passing in an
|
|
32
|
+
// object that implements the service interface.
|
|
33
|
+
return Database.of({
|
|
34
|
+
query
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class DatabaseError extends Schema.TaggedError<DatabaseError>()("DatabaseError", {
|
|
41
|
+
cause: Schema.Defect()
|
|
42
|
+
}) {}
|
|
43
|
+
|
|
44
|
+
// If you ever need to access the service type, use `Database["Service"]`
|
|
45
|
+
export type DatabaseService = Database["Service"]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Context.Reference
|
|
3
|
+
*
|
|
4
|
+
* For defining configuration values, feature flags, or any other service that has a default value.
|
|
5
|
+
*/
|
|
6
|
+
import { Context } from "effect"
|
|
7
|
+
|
|
8
|
+
export const FeatureFlag = Context.Reference<boolean>("myapp/FeatureFlag", {
|
|
9
|
+
defaultValue: () => false
|
|
10
|
+
})
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Composing services with the Layer module
|
|
3
|
+
*
|
|
4
|
+
* Build focused service layers, then compose them with `Layer.provide` and
|
|
5
|
+
* `Layer.provideMerge` based on what services you want to expose.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { PgClient } from "@effect/sql-pg"
|
|
9
|
+
import { Array, Config, Context, Effect, Layer, type Option, Schema } from "effect"
|
|
10
|
+
import { SqlClient, SqlError } from "effect/unstable/sql"
|
|
11
|
+
|
|
12
|
+
// Define a layer for the SqlClient service
|
|
13
|
+
export const SqlClientLayer: Layer.Layer<
|
|
14
|
+
PgClient.PgClient | SqlClient.SqlClient,
|
|
15
|
+
Config.ConfigError | SqlError.SqlError
|
|
16
|
+
> = PgClient.layerConfig({
|
|
17
|
+
url: Config.redacted("DATABASE_URL")
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
export class UserRespositoryError extends Schema.TaggedError<UserRespositoryError>()("UserRespositoryError", {
|
|
21
|
+
reason: SqlError.SqlError
|
|
22
|
+
}) {}
|
|
23
|
+
|
|
24
|
+
export class UserRepository extends Context.Service<UserRepository, {
|
|
25
|
+
findById(id: string): Effect.Effect<
|
|
26
|
+
Option.Option<{ readonly id: string; readonly name: string }>,
|
|
27
|
+
UserRespositoryError
|
|
28
|
+
>
|
|
29
|
+
}>()("myapp/UserRepository") {
|
|
30
|
+
// Implement the layer for the UserRepository service, which depends on the
|
|
31
|
+
// SqlClient service
|
|
32
|
+
static readonly layerNoDeps: Layer.Layer<
|
|
33
|
+
UserRepository,
|
|
34
|
+
never,
|
|
35
|
+
SqlClient.SqlClient
|
|
36
|
+
> = Layer.effect(
|
|
37
|
+
UserRepository,
|
|
38
|
+
Effect.gen(function*() {
|
|
39
|
+
const sql = yield* SqlClient.SqlClient
|
|
40
|
+
|
|
41
|
+
const findById = Effect.fn("UserRepository.findById")(function*(id: string) {
|
|
42
|
+
const results = yield* sql<{
|
|
43
|
+
readonly id: string
|
|
44
|
+
readonly name: string
|
|
45
|
+
}>`SELECT * FROM users WHERE id = '${id}'`
|
|
46
|
+
return Array.head(results)
|
|
47
|
+
}, Effect.mapError((reason) => new UserRespositoryError({ reason })))
|
|
48
|
+
|
|
49
|
+
return UserRepository.of({ findById })
|
|
50
|
+
})
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
// Use Layer.provide to compose the UserRepository layer with the SqlClient
|
|
54
|
+
// layer, exposing only the UserRepository service
|
|
55
|
+
static readonly layer: Layer.Layer<
|
|
56
|
+
UserRepository,
|
|
57
|
+
Config.ConfigError | SqlError.SqlError
|
|
58
|
+
> = this.layerNoDeps.pipe(
|
|
59
|
+
Layer.provide(SqlClientLayer)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
// Use Layer.provideMerge to compose the UserRepository layer with the SqlClient
|
|
63
|
+
// layer, exposing both the UserRepository and SqlClient services
|
|
64
|
+
static readonly layerWithSqlClient: Layer.Layer<
|
|
65
|
+
UserRepository | SqlClient.SqlClient,
|
|
66
|
+
Config.ConfigError | SqlError.SqlError
|
|
67
|
+
> = this.layerNoDeps.pipe(
|
|
68
|
+
Layer.provideMerge(SqlClientLayer)
|
|
69
|
+
)
|
|
70
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Creating Layers from configuration and/or Effects
|
|
3
|
+
*
|
|
4
|
+
* Build a layer dynamically from an Effect / Config with `Layer.unwrap`.
|
|
5
|
+
*/
|
|
6
|
+
import { Config, Context, Effect, Layer, Schema } from "effect"
|
|
7
|
+
|
|
8
|
+
export class MessageStoreError extends Schema.TaggedError<MessageStoreError>()("MessageStoreError", {
|
|
9
|
+
cause: Schema.Defect()
|
|
10
|
+
}) {}
|
|
11
|
+
|
|
12
|
+
export class MessageStore extends Context.Service<MessageStore, {
|
|
13
|
+
append(message: string): Effect.Effect<void>
|
|
14
|
+
readonly all: Effect.Effect<ReadonlyArray<string>>
|
|
15
|
+
}>()("myapp/MessageStore") {
|
|
16
|
+
static readonly layerInMemory = Layer.effect(
|
|
17
|
+
MessageStore,
|
|
18
|
+
Effect.sync(() => {
|
|
19
|
+
const messages: Array<string> = []
|
|
20
|
+
|
|
21
|
+
return MessageStore.of({
|
|
22
|
+
append: (message) =>
|
|
23
|
+
Effect.sync(() => {
|
|
24
|
+
messages.push(message)
|
|
25
|
+
}),
|
|
26
|
+
all: Effect.sync(() => [...messages])
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
static readonly layerRemote = (url: URL) =>
|
|
32
|
+
Layer.effect(
|
|
33
|
+
MessageStore,
|
|
34
|
+
Effect.try({
|
|
35
|
+
try: () => {
|
|
36
|
+
// In a real app this is where you would open a network connection.
|
|
37
|
+
const messages: Array<string> = []
|
|
38
|
+
|
|
39
|
+
return MessageStore.of({
|
|
40
|
+
append: (message) =>
|
|
41
|
+
Effect.sync(() => {
|
|
42
|
+
messages.push(`[${url.host}] ${message}`)
|
|
43
|
+
}),
|
|
44
|
+
all: Effect.sync(() => [...messages])
|
|
45
|
+
})
|
|
46
|
+
},
|
|
47
|
+
catch: (cause) => new MessageStoreError({ cause })
|
|
48
|
+
})
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
static readonly layer = Layer.unwrap(
|
|
52
|
+
Effect.gen(function*() {
|
|
53
|
+
// Read config inside an Effect, then choose which concrete layer to use.
|
|
54
|
+
const useInMemory = yield* Config.boolean("MESSAGE_STORE_IN_MEMORY").pipe(
|
|
55
|
+
Config.withDefault(false)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if (useInMemory) {
|
|
59
|
+
return MessageStore.layerInMemory
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const remoteUrl = yield* Config.url("MESSAGE_STORE_URL")
|
|
63
|
+
return MessageStore.layerRemote(remoteUrl)
|
|
64
|
+
})
|
|
65
|
+
)
|
|
66
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Error handling basics
|
|
3
|
+
*
|
|
4
|
+
* Defining custom errors and handling them with Effect.catch and Effect.catchTag.
|
|
5
|
+
*/
|
|
6
|
+
import { Effect, Schema } from "effect"
|
|
7
|
+
|
|
8
|
+
// Define custom errors using Schema.TaggedError
|
|
9
|
+
export class ParseError extends Schema.TaggedError<ParseError>()("ParseError", {
|
|
10
|
+
input: Schema.String,
|
|
11
|
+
message: Schema.String
|
|
12
|
+
}) {}
|
|
13
|
+
|
|
14
|
+
export class ReservedPortError extends Schema.TaggedError<ReservedPortError>()("ReservedPortError", {
|
|
15
|
+
port: Schema.Int
|
|
16
|
+
}) {}
|
|
17
|
+
|
|
18
|
+
declare const loadPort: (input: string) => Effect.Effect<number, ParseError | ReservedPortError>
|
|
19
|
+
|
|
20
|
+
export const recovered = loadPort("80").pipe(
|
|
21
|
+
// Catch multiple errors with Effect.catchTag, and return a default port number.
|
|
22
|
+
Effect.catchTag(["ParseError", "ReservedPortError"], (_) => Effect.succeed(3000))
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
export const withFinalFallback = loadPort("invalid").pipe(
|
|
26
|
+
// Catch a specific error with Effect.catchTag
|
|
27
|
+
Effect.catchTag("ReservedPortError", (_) => Effect.succeed(3000)),
|
|
28
|
+
// Catch all errors with Effect.catch
|
|
29
|
+
Effect.catch((_) => Effect.succeed(3000))
|
|
30
|
+
)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Catch multiple errors with Effect.catchTags
|
|
3
|
+
*
|
|
4
|
+
* Use `Effect.catchTags` to handle several tagged errors in one place.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Effect, Schema } from "effect"
|
|
8
|
+
|
|
9
|
+
export class ValidationError extends Schema.TaggedError<ValidationError>()("ValidationError", {
|
|
10
|
+
message: Schema.String
|
|
11
|
+
}) {}
|
|
12
|
+
|
|
13
|
+
export class NetworkError extends Schema.TaggedError<NetworkError>()("NetworkError", {
|
|
14
|
+
statusCode: Schema.Int
|
|
15
|
+
}) {}
|
|
16
|
+
|
|
17
|
+
declare const fetchUser: (id: string) => Effect.Effect<string, ValidationError | NetworkError>
|
|
18
|
+
|
|
19
|
+
export const userOrFallback = fetchUser("123").pipe(
|
|
20
|
+
Effect.catchTags({
|
|
21
|
+
ValidationError: (error) => Effect.succeed(`Validation failed: ${error.message}`),
|
|
22
|
+
NetworkError: (error) => Effect.succeed(`Network request failed with status ${error.statusCode}`)
|
|
23
|
+
})
|
|
24
|
+
)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Creating and handling errors with reasons
|
|
3
|
+
*
|
|
4
|
+
* Define a tagged error with a tagged `reason` field, then recover with
|
|
5
|
+
* `Effect.catchReason`, `Effect.catchReasons`, or by unwrapping the reason into
|
|
6
|
+
* the error channel with `Effect.unwrapReason`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Effect, Schema } from "effect"
|
|
10
|
+
|
|
11
|
+
export class RateLimitError extends Schema.TaggedError<RateLimitError>()("RateLimitError", {
|
|
12
|
+
retryAfter: Schema.Finite
|
|
13
|
+
}) {}
|
|
14
|
+
|
|
15
|
+
export class QuotaExceededError extends Schema.TaggedError<QuotaExceededError>()("QuotaExceededError", {
|
|
16
|
+
limit: Schema.Int
|
|
17
|
+
}) {}
|
|
18
|
+
|
|
19
|
+
export class SafetyBlockedError extends Schema.TaggedError<SafetyBlockedError>()("SafetyBlockedError", {
|
|
20
|
+
category: Schema.String
|
|
21
|
+
}) {}
|
|
22
|
+
|
|
23
|
+
export class AiError extends Schema.TaggedError<AiError>()("AiError", {
|
|
24
|
+
reason: Schema.Union([RateLimitError, QuotaExceededError, SafetyBlockedError])
|
|
25
|
+
}) {}
|
|
26
|
+
|
|
27
|
+
declare const callModel: Effect.Effect<string, AiError>
|
|
28
|
+
|
|
29
|
+
export const handleOneReason = callModel.pipe(
|
|
30
|
+
// Use `Effect.catchReason` to handle a specific reason type
|
|
31
|
+
Effect.catchReason(
|
|
32
|
+
"AiError", // The parent error _tag to catch
|
|
33
|
+
"RateLimitError", // The reason _tag to catch
|
|
34
|
+
// The handler for the caught reason
|
|
35
|
+
(reason) => Effect.succeed(`Retry after ${reason.retryAfter} seconds`),
|
|
36
|
+
// Optionally handle all the other reasons with a catch-all handler
|
|
37
|
+
(reason) => Effect.succeed(`Model call failed for reason: ${reason._tag}`)
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
export const handleMultipleReasons = callModel.pipe(
|
|
42
|
+
// Use `Effect.catchReasons` to handle multiple reason types for a given error
|
|
43
|
+
// in one go
|
|
44
|
+
Effect.catchReasons(
|
|
45
|
+
"AiError",
|
|
46
|
+
{
|
|
47
|
+
RateLimitError: (reason) => Effect.succeed(`Retry after ${reason.retryAfter} seconds`),
|
|
48
|
+
QuotaExceededError: (reason) => Effect.succeed(`Quota exceeded at ${reason.limit} tokens`)
|
|
49
|
+
}
|
|
50
|
+
// Optionally handle all the other reasons with a catch-all handler
|
|
51
|
+
// (reason) => Effect.succeed(`Unhandled reason: ${reason._tag}`)
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
export const unwrapAndHandle = callModel.pipe(
|
|
56
|
+
// Use `Effect.unwrapReason` to move the reasons into the error channel, then
|
|
57
|
+
// handle them all with `Effect.catchTags` or other error handling combinators
|
|
58
|
+
Effect.unwrapReason("AiError"),
|
|
59
|
+
Effect.catchTags({
|
|
60
|
+
RateLimitError: (reason) => Effect.succeed(`Back off for ${reason.retryAfter} seconds`),
|
|
61
|
+
QuotaExceededError: (reason) => Effect.succeed(`Increase quota beyond ${reason.limit}`),
|
|
62
|
+
SafetyBlockedError: (reason) => Effect.succeed(`Blocked by safety category: ${reason.category}`)
|
|
63
|
+
})
|
|
64
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
## Error handling
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Acquiring resources with Effect.acquireRelease
|
|
3
|
+
*
|
|
4
|
+
* Define a service that uses `Effect.acquireRelease` to manage the lifecycle of
|
|
5
|
+
* a resource, ensuring that it is properly cleaned up when the service is no
|
|
6
|
+
* longer needed.
|
|
7
|
+
*/
|
|
8
|
+
import { Config, Context, Effect, Layer, Redacted, Schema } from "effect"
|
|
9
|
+
import * as NodeMailer from "nodemailer"
|
|
10
|
+
|
|
11
|
+
export class SmtpError extends Schema.Error<SmtpError>("SmtpError")({
|
|
12
|
+
cause: Schema.Defect()
|
|
13
|
+
}) {}
|
|
14
|
+
|
|
15
|
+
export class Smtp extends Context.Service<Smtp, {
|
|
16
|
+
send(message: {
|
|
17
|
+
readonly to: string
|
|
18
|
+
readonly subject: string
|
|
19
|
+
readonly body: string
|
|
20
|
+
}): Effect.Effect<void, SmtpError>
|
|
21
|
+
}>()("app/Smtp") {
|
|
22
|
+
static readonly layer = Layer.effect(
|
|
23
|
+
Smtp,
|
|
24
|
+
Effect.gen(function*() {
|
|
25
|
+
const user = yield* Config.string("SMTP_USER")
|
|
26
|
+
const pass = yield* Config.redacted("SMTP_PASS")
|
|
27
|
+
|
|
28
|
+
// Use `Effect.acquireRelease` to manage the lifecycle of the SMTP
|
|
29
|
+
// transporter.
|
|
30
|
+
//
|
|
31
|
+
// When the Layer is built, the transporter will be created. When the
|
|
32
|
+
// Layer is torn down, the transporter will be closed, ensuring that
|
|
33
|
+
// resources are always cleaned up properly.
|
|
34
|
+
const transporter = yield* Effect.acquireRelease(
|
|
35
|
+
Effect.sync(() =>
|
|
36
|
+
NodeMailer.createTransport({
|
|
37
|
+
host: "smtp.example.com",
|
|
38
|
+
port: 587,
|
|
39
|
+
secure: false,
|
|
40
|
+
auth: { user, pass: Redacted.value(pass) }
|
|
41
|
+
})
|
|
42
|
+
),
|
|
43
|
+
(transporter) => Effect.sync(() => transporter.close())
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
const send = Effect.fn("Smtp.send")((message: {
|
|
47
|
+
readonly to: string
|
|
48
|
+
readonly subject: string
|
|
49
|
+
readonly body: string
|
|
50
|
+
}) =>
|
|
51
|
+
Effect.tryPromise({
|
|
52
|
+
try: () =>
|
|
53
|
+
transporter.sendMail({
|
|
54
|
+
from: "Acme Cloud <cloud@acme.com>",
|
|
55
|
+
to: message.to,
|
|
56
|
+
subject: message.subject,
|
|
57
|
+
text: message.body
|
|
58
|
+
}),
|
|
59
|
+
catch: (cause) => new SmtpError({ cause })
|
|
60
|
+
}).pipe(
|
|
61
|
+
Effect.asVoid
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return Smtp.of({ send })
|
|
66
|
+
})
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// We can then use the `Smtp` service in another service, and the transporter
|
|
71
|
+
// will be properly managed by the Layer system.
|
|
72
|
+
|
|
73
|
+
export class MailerError extends Schema.TaggedError<MailerError>()("MailerError", {
|
|
74
|
+
reason: SmtpError
|
|
75
|
+
}) {}
|
|
76
|
+
|
|
77
|
+
export class Mailer extends Context.Service<Mailer, {
|
|
78
|
+
sendWelcomeEmail(to: string): Effect.Effect<void, MailerError>
|
|
79
|
+
}>()("app/Mailer") {
|
|
80
|
+
static readonly layerNoDeps = Layer.effect(
|
|
81
|
+
Mailer,
|
|
82
|
+
Effect.gen(function*() {
|
|
83
|
+
const smtp = yield* Smtp
|
|
84
|
+
|
|
85
|
+
const sendWelcomeEmail = Effect.fn("Mailer.sendWelcomeEmail")(function*(to: string) {
|
|
86
|
+
yield* smtp.send({
|
|
87
|
+
to,
|
|
88
|
+
subject: "Welcome to Acme Cloud!",
|
|
89
|
+
body: "Thanks for signing up for Acme Cloud. We're glad to have you!"
|
|
90
|
+
}).pipe(
|
|
91
|
+
Effect.mapError((reason) => new MailerError({ reason }))
|
|
92
|
+
)
|
|
93
|
+
yield* Effect.logInfo(`Sent welcome email to ${to}`)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
return Mailer.of({ sendWelcomeEmail })
|
|
97
|
+
})
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
// Locally provide the Smtp layer to the Mailer layer, to eliminate all the
|
|
101
|
+
// requirements
|
|
102
|
+
static readonly layer = this.layerNoDeps.pipe(
|
|
103
|
+
Layer.provide(Smtp.layer)
|
|
104
|
+
)
|
|
105
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Creating Layers that run background tasks
|
|
3
|
+
*
|
|
4
|
+
* Use Layer.effectDiscard to encapsulate background tasks without a service interface.
|
|
5
|
+
*/
|
|
6
|
+
import { NodeRuntime } from "@effect/platform-node"
|
|
7
|
+
import { Effect, Layer } from "effect"
|
|
8
|
+
|
|
9
|
+
// Use Layer.effectDiscard when you want to create a layer that runs an effect
|
|
10
|
+
// but does not provide any services.
|
|
11
|
+
const BackgroundTask = Layer.effectDiscard(Effect.gen(function*() {
|
|
12
|
+
yield* Effect.logInfo("Starting background task...")
|
|
13
|
+
|
|
14
|
+
yield* Effect.gen(function*() {
|
|
15
|
+
while (true) {
|
|
16
|
+
yield* Effect.sleep("5 seconds")
|
|
17
|
+
yield* Effect.logInfo("Background task running...")
|
|
18
|
+
}
|
|
19
|
+
}).pipe(
|
|
20
|
+
Effect.onInterrupt(() => Effect.logInfo("Background task interrupted: layer scope closed")),
|
|
21
|
+
Effect.forkScoped
|
|
22
|
+
)
|
|
23
|
+
}))
|
|
24
|
+
|
|
25
|
+
// Run the background task layer. It will start when the layer is launched and
|
|
26
|
+
// will be automatically interrupted when the layer scope is closed (e.g. when
|
|
27
|
+
// the program exits).
|
|
28
|
+
BackgroundTask.pipe(
|
|
29
|
+
Layer.launch,
|
|
30
|
+
NodeRuntime.runMain
|
|
31
|
+
)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Dynamic resources with LayerMap
|
|
3
|
+
*
|
|
4
|
+
* Use `LayerMap.Service` to dynamically build and manage resources that are
|
|
5
|
+
* keyed by some identifier, such as a tenant ID.
|
|
6
|
+
*/
|
|
7
|
+
import { Context, Effect, Layer, LayerMap, Schema } from "effect"
|
|
8
|
+
|
|
9
|
+
class DatabaseQueryError extends Schema.TaggedError<DatabaseQueryError>()("DatabaseQueryError", {
|
|
10
|
+
tenantId: Schema.String,
|
|
11
|
+
cause: Schema.Defect()
|
|
12
|
+
}) {}
|
|
13
|
+
|
|
14
|
+
type UserRecord = {
|
|
15
|
+
readonly id: number
|
|
16
|
+
readonly email: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let nextConnectionId = 0
|
|
20
|
+
|
|
21
|
+
export class DatabasePool extends Context.Service<DatabasePool, {
|
|
22
|
+
readonly tenantId: string
|
|
23
|
+
readonly connectionId: number
|
|
24
|
+
readonly query: (sql: string) => Effect.Effect<ReadonlyArray<UserRecord>, DatabaseQueryError>
|
|
25
|
+
}>()("app/DatabasePool") {
|
|
26
|
+
// A layer factory that builds one pool per tenant.
|
|
27
|
+
static readonly layer = (tenantId: string) =>
|
|
28
|
+
Layer.effect(
|
|
29
|
+
DatabasePool,
|
|
30
|
+
Effect.acquireRelease(
|
|
31
|
+
Effect.sync(() => {
|
|
32
|
+
const connectionId = ++nextConnectionId
|
|
33
|
+
|
|
34
|
+
return DatabasePool.of({
|
|
35
|
+
tenantId,
|
|
36
|
+
connectionId,
|
|
37
|
+
query: Effect.fn("DatabasePool.query")((_sql: string) =>
|
|
38
|
+
Effect.succeed([
|
|
39
|
+
{ id: 1, email: `admin@${tenantId}.example.com` },
|
|
40
|
+
{ id: 2, email: `ops@${tenantId}.example.com` }
|
|
41
|
+
])
|
|
42
|
+
)
|
|
43
|
+
})
|
|
44
|
+
}),
|
|
45
|
+
(pool) => Effect.logInfo(`Closing tenant pool ${pool.tenantId}#${pool.connectionId}`)
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// extend `LayerMap.Service` to create a `LayerMap` service
|
|
51
|
+
export class PoolMap extends LayerMap.Service<PoolMap>()("app/PoolMap", {
|
|
52
|
+
// `lookup` tells LayerMap how to build a layer for each tenant key.
|
|
53
|
+
lookup: (tenantId: string) => DatabasePool.layer(tenantId),
|
|
54
|
+
|
|
55
|
+
// You can also use the layers option for a static set of layers
|
|
56
|
+
// layers: {
|
|
57
|
+
// acme: DatabasePool.layer("acme"),
|
|
58
|
+
// globex: DatabasePool.layer("globex")
|
|
59
|
+
// },
|
|
60
|
+
|
|
61
|
+
// If a pool is not used for this duration, it is released automatically.
|
|
62
|
+
idleTimeToLive: "1 minute"
|
|
63
|
+
}) {}
|
|
64
|
+
|
|
65
|
+
const queryUsersForCurrentTenant = Effect.gen(function*() {
|
|
66
|
+
// Run a query agnostic of the tenant. The correct pool will be provided by
|
|
67
|
+
// the LayerMap.
|
|
68
|
+
const pool = yield* DatabasePool
|
|
69
|
+
return yield* pool.query("SELECT id, email FROM users ORDER BY id")
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
export const program = Effect.gen(function*() {
|
|
73
|
+
yield* queryUsersForCurrentTenant.pipe(
|
|
74
|
+
// Use `PoolMap.get` to access the pool for a specific tenant. The first
|
|
75
|
+
// time this is called for a tenant, the pool will be built using the
|
|
76
|
+
// `lookup` function defined in `PoolMap`. Subsequent calls will reuse the
|
|
77
|
+
// cached pool until it is idle for too long or invalidated.
|
|
78
|
+
Effect.provide(PoolMap.get("acme"))
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
// `PoolMap.invalidate` forces a key to rebuild on the next access.
|
|
82
|
+
yield* PoolMap.invalidate("acme")
|
|
83
|
+
}).pipe(
|
|
84
|
+
// Provide the `PoolMap` layer to the entire program.
|
|
85
|
+
Effect.provide(PoolMap.layer)
|
|
86
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Running effects with NodeRuntime and BunRuntime
|
|
3
|
+
*
|
|
4
|
+
* Use `NodeRuntime.runMain` to run an Effect program as your process entrypoint.
|
|
5
|
+
*/
|
|
6
|
+
import { BunRuntime } from "@effect/platform-bun"
|
|
7
|
+
import { NodeRuntime } from "@effect/platform-node"
|
|
8
|
+
import { Effect, Layer } from "effect"
|
|
9
|
+
|
|
10
|
+
const Worker = Layer.effectDiscard(Effect.gen(function*() {
|
|
11
|
+
yield* Effect.logInfo("Starting worker...")
|
|
12
|
+
yield* Effect.forkScoped(Effect.gen(function*() {
|
|
13
|
+
while (true) {
|
|
14
|
+
yield* Effect.logInfo("Working...")
|
|
15
|
+
yield* Effect.sleep("1 second")
|
|
16
|
+
}
|
|
17
|
+
}))
|
|
18
|
+
}))
|
|
19
|
+
|
|
20
|
+
const program = Layer.launch(Worker)
|
|
21
|
+
|
|
22
|
+
// `runMain` installs SIGINT / SIGTERM handlers and interrupts running fibers
|
|
23
|
+
// for graceful shutdown.
|
|
24
|
+
NodeRuntime.runMain(program, {
|
|
25
|
+
// Disable automatic error reporting if your app already centralizes it.
|
|
26
|
+
disableErrorReporting: true
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// Bun has the same API shape:
|
|
30
|
+
BunRuntime.runMain(program, { disableErrorReporting: true })
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Using Layer.launch as the application entry point
|
|
3
|
+
*
|
|
4
|
+
* Use `Layer.launch` to run a long-running Effect program as your process entrypoint.
|
|
5
|
+
*/
|
|
6
|
+
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node"
|
|
7
|
+
import { Effect, Layer } from "effect"
|
|
8
|
+
import { HttpRouter, HttpServerResponse } from "effect/unstable/http"
|
|
9
|
+
import { createServer } from "node:http"
|
|
10
|
+
|
|
11
|
+
// Build a tiny HTTP app with a health-check endpoint.
|
|
12
|
+
export const HealthRoutes = HttpRouter.use(Effect.fn(function*(router) {
|
|
13
|
+
yield* router.add("GET", "/health", Effect.succeed(HttpServerResponse.text("ok")))
|
|
14
|
+
yield* router.add("GET", "/healthz", Effect.succeed(HttpServerResponse.text("ok")))
|
|
15
|
+
}))
|
|
16
|
+
|
|
17
|
+
// Turn the routes into a server layer and provide the Node HTTP server backend.
|
|
18
|
+
export const HttpServerLive = HttpRouter.serve(HealthRoutes).pipe(
|
|
19
|
+
Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 }))
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
// `Layer.launch` converts the layer into a long-running Effect<never>.
|
|
23
|
+
export const main = Layer.launch(HttpServerLive)
|
|
24
|
+
|
|
25
|
+
// This entrypoint pattern works well when the whole app is represented as
|
|
26
|
+
// layers (for example: HTTP server + background workers).
|
|
27
|
+
NodeRuntime.runMain(main)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
## Running Effect programs
|