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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/AGENTS.md +381 -0
  2. package/CLAUDE.md +381 -0
  3. package/README.md +10 -3
  4. package/ai-docs/README.md +44 -0
  5. package/ai-docs/package.json +36 -0
  6. package/ai-docs/src/01_effect/01_basics/01_effect-gen.ts +30 -0
  7. package/ai-docs/src/01_effect/01_basics/02_effect-fn.ts +39 -0
  8. package/ai-docs/src/01_effect/01_basics/10_creating-effects.ts +74 -0
  9. package/ai-docs/src/01_effect/01_basics/index.md +5 -0
  10. package/ai-docs/src/01_effect/02_schema/10_schema-basics.ts +43 -0
  11. package/ai-docs/src/01_effect/02_schema/index.md +7 -0
  12. package/ai-docs/src/01_effect/03_services/01_service.ts +45 -0
  13. package/ai-docs/src/01_effect/03_services/10_reference.ts +10 -0
  14. package/ai-docs/src/01_effect/03_services/20_layer-composition.ts +70 -0
  15. package/ai-docs/src/01_effect/03_services/20_layer-unwrap.ts +66 -0
  16. package/ai-docs/src/01_effect/03_services/index.md +5 -0
  17. package/ai-docs/src/01_effect/04_errors/01_error-handling.ts +30 -0
  18. package/ai-docs/src/01_effect/04_errors/10_catch-tags.ts +24 -0
  19. package/ai-docs/src/01_effect/04_errors/20_reason-errors.ts +64 -0
  20. package/ai-docs/src/01_effect/04_errors/index.md +1 -0
  21. package/ai-docs/src/01_effect/05_resources/10_acquire-release.ts +105 -0
  22. package/ai-docs/src/01_effect/05_resources/20_layer-side-effects.ts +31 -0
  23. package/ai-docs/src/01_effect/05_resources/30_layer-map.ts +86 -0
  24. package/ai-docs/src/01_effect/05_resources/index.md +3 -0
  25. package/ai-docs/src/01_effect/06_running/10_run-main.ts +30 -0
  26. package/ai-docs/src/01_effect/06_running/20_layer-launch.ts +27 -0
  27. package/ai-docs/src/01_effect/06_running/index.md +1 -0
  28. package/ai-docs/src/01_effect/07_pubsub/10_pubsub.ts +56 -0
  29. package/ai-docs/src/01_effect/07_pubsub/index.md +3 -0
  30. package/ai-docs/src/03_stream/10_creating-streams.ts +103 -0
  31. package/ai-docs/src/03_stream/20_consuming-streams.ts +137 -0
  32. package/ai-docs/src/03_stream/30_encoding.ts +165 -0
  33. package/ai-docs/src/03_stream/index.md +4 -0
  34. package/ai-docs/src/04_integration/10_managed-runtime.ts +129 -0
  35. package/ai-docs/src/04_integration/index.md +5 -0
  36. package/ai-docs/src/05_batching/10_request-resolver.ts +89 -0
  37. package/ai-docs/src/05_batching/index.md +3 -0
  38. package/ai-docs/src/06_schedule/10_schedules.ts +110 -0
  39. package/ai-docs/src/06_schedule/index.md +3 -0
  40. package/ai-docs/src/07_datetime/10_creating-and-formatting.ts +30 -0
  41. package/ai-docs/src/07_datetime/20_time-zones.ts +44 -0
  42. package/ai-docs/src/07_datetime/index.md +5 -0
  43. package/ai-docs/src/08_observability/10_logging.ts +66 -0
  44. package/ai-docs/src/08_observability/20_otlp-tracing.ts +95 -0
  45. package/ai-docs/src/08_observability/index.md +7 -0
  46. package/ai-docs/src/09_testing/10_effect-tests.ts +55 -0
  47. package/ai-docs/src/09_testing/20_layer-tests.ts +138 -0
  48. package/ai-docs/src/09_testing/index.md +1 -0
  49. package/ai-docs/src/10_predicate/01_basics.ts +14 -0
  50. package/ai-docs/src/10_predicate/index.md +9 -0
  51. package/ai-docs/src/50_http-client/10_basics.ts +102 -0
  52. package/ai-docs/src/50_http-client/index.md +3 -0
  53. package/ai-docs/src/51_http-server/10_basics.ts +116 -0
  54. package/ai-docs/src/51_http-server/fixtures/api/Api.ts +14 -0
  55. package/ai-docs/src/51_http-server/fixtures/api/Authorization.ts +36 -0
  56. package/ai-docs/src/51_http-server/fixtures/api/System.ts +10 -0
  57. package/ai-docs/src/51_http-server/fixtures/api/Users.ts +91 -0
  58. package/ai-docs/src/51_http-server/fixtures/domain/User.ts +12 -0
  59. package/ai-docs/src/51_http-server/fixtures/domain/UserErrors.ts +22 -0
  60. package/ai-docs/src/51_http-server/fixtures/server/Authorization.ts +36 -0
  61. package/ai-docs/src/51_http-server/fixtures/server/Users/http.ts +71 -0
  62. package/ai-docs/src/51_http-server/fixtures/server/Users.ts +62 -0
  63. package/ai-docs/src/51_http-server/index.md +3 -0
  64. package/ai-docs/src/60_child-process/10_working-with-child-processes.ts +117 -0
  65. package/ai-docs/src/60_child-process/index.md +3 -0
  66. package/ai-docs/src/70_cli/10_basics.ts +136 -0
  67. package/ai-docs/src/70_cli/index.md +5 -0
  68. package/ai-docs/src/71_ai/10_language-model.ts +156 -0
  69. package/ai-docs/src/71_ai/20_tools.ts +226 -0
  70. package/ai-docs/src/71_ai/30_chat.ts +158 -0
  71. package/ai-docs/src/71_ai/fixtures/domain/LaunchPlan.ts +9 -0
  72. package/ai-docs/src/71_ai/index.md +5 -0
  73. package/ai-docs/src/80_cluster/10_entities.ts +97 -0
  74. package/ai-docs/src/80_cluster/index.md +4 -0
  75. package/ai-docs/src/index.md +10 -0
  76. package/ai-docs/tsconfig.json +24 -0
  77. package/dist/OpfsWorker.d.ts +1 -1
  78. package/dist/OpfsWorker.d.ts.map +1 -1
  79. package/dist/OpfsWorker.js +2 -2
  80. package/dist/OpfsWorker.js.map +1 -1
  81. package/dist/SqliteClient.d.ts +2 -2
  82. package/dist/SqliteClient.d.ts.map +1 -1
  83. package/dist/SqliteClient.js +15 -6
  84. package/dist/SqliteClient.js.map +1 -1
  85. package/dist/SqliteMigrator.d.ts +2 -2
  86. package/dist/SqliteMigrator.js +2 -2
  87. package/dist/SqliteMigrator.js.map +1 -1
  88. package/dist/index.js.map +1 -1
  89. package/package.json +10 -8
  90. package/src/OpfsWorker.ts +5 -2
  91. package/src/SqliteClient.ts +16 -8
  92. package/src/SqliteMigrator.ts +2 -2
@@ -0,0 +1,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,5 @@
1
+ ## Writing Effect services
2
+
3
+ Effect services are the most common way to structure Effect code. Prefer using
4
+ services to encapsulate behaviour over other approaches, as it ensures that your
5
+ code is modular, testable, and maintainable.
@@ -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,3 @@
1
+ ## Managing resources and `Scope`s
2
+
3
+ Learn how to safely manage resources in Effect using `Scope`s and finalizers.
@@ -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