@effect/platform-cloudflare 0.0.0 → 4.1.3
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 +434 -0
- package/CLAUDE.md +434 -0
- package/LICENSE +21 -0
- package/README.md +506 -0
- 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 +52 -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 +7 -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 +163 -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 +111 -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/40_sql/10_basics.ts +175 -0
- package/ai-docs/src/40_sql/index.md +6 -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 +118 -0
- package/ai-docs/src/51_http-server/20_testing.ts +100 -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 +107 -0
- package/ai-docs/src/51_http-server/fixtures/domain/User.ts +21 -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 +37 -0
- package/ai-docs/src/51_http-server/fixtures/server/Users/http.ts +80 -0
- package/ai-docs/src/51_http-server/fixtures/server/Users.ts +183 -0
- package/ai-docs/src/51_http-server/index.md +3 -0
- package/ai-docs/src/52_graphql/10_basics.ts +117 -0
- package/ai-docs/src/52_graphql/20_testing.ts +45 -0
- package/ai-docs/src/52_graphql/30_batching.ts +118 -0
- package/ai-docs/src/52_graphql/40_aliased-batching.ts +124 -0
- package/ai-docs/src/52_graphql/50_errors-and-retries.ts +74 -0
- package/ai-docs/src/52_graphql/index.md +6 -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 +162 -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 +28 -0
- package/dist/AlchemyCloudflareCluster.d.ts +134 -0
- package/dist/AlchemyCloudflareCluster.d.ts.map +1 -0
- package/dist/AlchemyCloudflareCluster.js +111 -0
- package/dist/AlchemyCloudflareCluster.js.map +1 -0
- package/dist/CloudflareCluster.d.ts +196 -0
- package/dist/CloudflareCluster.d.ts.map +1 -0
- package/dist/CloudflareCluster.js +533 -0
- package/dist/CloudflareCluster.js.map +1 -0
- package/dist/CloudflareDurableObjectPrograms.d.ts +279 -0
- package/dist/CloudflareDurableObjectPrograms.d.ts.map +1 -0
- package/dist/CloudflareDurableObjectPrograms.js +295 -0
- package/dist/CloudflareDurableObjectPrograms.js.map +1 -0
- package/dist/CloudflareDurableObjects.d.ts +149 -0
- package/dist/CloudflareDurableObjects.d.ts.map +1 -0
- package/dist/CloudflareDurableObjects.js +337 -0
- package/dist/CloudflareDurableObjects.js.map +1 -0
- package/dist/CloudflarePersistedQueue.d.ts +36 -0
- package/dist/CloudflarePersistedQueue.d.ts.map +1 -0
- package/dist/CloudflarePersistedQueue.js +122 -0
- package/dist/CloudflarePersistedQueue.js.map +1 -0
- package/dist/CloudflareWorkflowEngine.d.ts +72 -0
- package/dist/CloudflareWorkflowEngine.d.ts.map +1 -0
- package/dist/CloudflareWorkflowEngine.js +187 -0
- package/dist/CloudflareWorkflowEngine.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/alchemyCluster.d.ts +2 -0
- package/dist/internal/alchemyCluster.d.ts.map +1 -0
- package/dist/internal/alchemyCluster.js +75 -0
- package/dist/internal/alchemyCluster.js.map +1 -0
- package/dist/internal/boundedMap.d.ts +3 -0
- package/dist/internal/boundedMap.d.ts.map +1 -0
- package/dist/internal/boundedMap.js +15 -0
- package/dist/internal/boundedMap.js.map +1 -0
- package/dist/internal/cluster.d.ts +57 -0
- package/dist/internal/cluster.d.ts.map +1 -0
- package/dist/internal/cluster.js +2 -0
- package/dist/internal/cluster.js.map +1 -0
- package/dist/internal/clusterName.d.ts +2 -0
- package/dist/internal/clusterName.d.ts.map +1 -0
- package/dist/internal/clusterName.js +16 -0
- package/dist/internal/clusterName.js.map +1 -0
- package/dist/internal/entityKeepAlive.d.ts +2 -0
- package/dist/internal/entityKeepAlive.d.ts.map +1 -0
- package/dist/internal/entityKeepAlive.js +73 -0
- package/dist/internal/entityKeepAlive.js.map +1 -0
- package/dist/internal/entityMailbox.d.ts +2 -0
- package/dist/internal/entityMailbox.d.ts.map +1 -0
- package/dist/internal/entityMailbox.js +160 -0
- package/dist/internal/entityMailbox.js.map +1 -0
- package/dist/internal/entityRegistry.d.ts +16 -0
- package/dist/internal/entityRegistry.d.ts.map +1 -0
- package/dist/internal/entityRegistry.js +22 -0
- package/dist/internal/entityRegistry.js.map +1 -0
- package/dist/internal/entityReply.d.ts +2 -0
- package/dist/internal/entityReply.d.ts.map +1 -0
- package/dist/internal/entityReply.js +35 -0
- package/dist/internal/entityReply.js.map +1 -0
- package/dist/internal/entityRuntime.d.ts +2 -0
- package/dist/internal/entityRuntime.d.ts.map +1 -0
- package/dist/internal/entityRuntime.js +823 -0
- package/dist/internal/entityRuntime.js.map +1 -0
- package/dist/internal/entitySql.d.ts +2 -0
- package/dist/internal/entitySql.d.ts.map +1 -0
- package/dist/internal/entitySql.js +29 -0
- package/dist/internal/entitySql.js.map +1 -0
- package/dist/internal/entityStorage.d.ts +2 -0
- package/dist/internal/entityStorage.d.ts.map +1 -0
- package/dist/internal/entityStorage.js +98 -0
- package/dist/internal/entityStorage.js.map +1 -0
- package/dist/internal/entityWire.d.ts +2 -0
- package/dist/internal/entityWire.d.ts.map +1 -0
- package/dist/internal/entityWire.js +146 -0
- package/dist/internal/entityWire.js.map +1 -0
- package/dist/internal/queueRuntime.d.ts +2 -0
- package/dist/internal/queueRuntime.d.ts.map +1 -0
- package/dist/internal/queueRuntime.js +88 -0
- package/dist/internal/queueRuntime.js.map +1 -0
- package/dist/internal/queueStorage.d.ts +2 -0
- package/dist/internal/queueStorage.d.ts.map +1 -0
- package/dist/internal/queueStorage.js +129 -0
- package/dist/internal/queueStorage.js.map +1 -0
- package/dist/internal/registry.d.ts +10 -0
- package/dist/internal/registry.d.ts.map +1 -0
- package/dist/internal/registry.js +24 -0
- package/dist/internal/registry.js.map +1 -0
- package/dist/internal/singletonRegistry.d.ts +2 -0
- package/dist/internal/singletonRegistry.d.ts.map +1 -0
- package/dist/internal/singletonRegistry.js +9 -0
- package/dist/internal/singletonRegistry.js.map +1 -0
- package/dist/internal/singletonRuntime.d.ts +2 -0
- package/dist/internal/singletonRuntime.d.ts.map +1 -0
- package/dist/internal/singletonRuntime.js +42 -0
- package/dist/internal/singletonRuntime.js.map +1 -0
- package/dist/internal/singletonStorage.d.ts +2 -0
- package/dist/internal/singletonStorage.d.ts.map +1 -0
- package/dist/internal/singletonStorage.js +34 -0
- package/dist/internal/singletonStorage.js.map +1 -0
- package/dist/internal/transport.d.ts +2 -0
- package/dist/internal/transport.d.ts.map +1 -0
- package/dist/internal/transport.js +49 -0
- package/dist/internal/transport.js.map +1 -0
- package/dist/internal/workflowRegistry.d.ts +2 -0
- package/dist/internal/workflowRegistry.d.ts.map +1 -0
- package/dist/internal/workflowRegistry.js +29 -0
- package/dist/internal/workflowRegistry.js.map +1 -0
- package/dist/internal/workflowRuntime.d.ts +2 -0
- package/dist/internal/workflowRuntime.d.ts.map +1 -0
- package/dist/internal/workflowRuntime.js +322 -0
- package/dist/internal/workflowRuntime.js.map +1 -0
- package/dist/internal/workflowStorage.d.ts +2 -0
- package/dist/internal/workflowStorage.d.ts.map +1 -0
- package/dist/internal/workflowStorage.js +113 -0
- package/dist/internal/workflowStorage.js.map +1 -0
- package/dist/internal/workflowWire.d.ts +2 -0
- package/dist/internal/workflowWire.d.ts.map +1 -0
- package/dist/internal/workflowWire.js +52 -0
- package/dist/internal/workflowWire.js.map +1 -0
- package/package.json +77 -1
- package/src/AlchemyCloudflareCluster.ts +200 -0
- package/src/CloudflareCluster.ts +704 -0
- package/src/CloudflareDurableObjectPrograms.ts +543 -0
- package/src/CloudflareDurableObjects.ts +430 -0
- package/src/CloudflarePersistedQueue.ts +189 -0
- package/src/CloudflareWorkflowEngine.ts +220 -0
- package/src/index.ts +38 -0
- package/src/internal/alchemyCluster.ts +120 -0
- package/src/internal/boundedMap.ts +15 -0
- package/src/internal/cluster.ts +57 -0
- package/src/internal/clusterName.ts +19 -0
- package/src/internal/entityKeepAlive.ts +121 -0
- package/src/internal/entityMailbox.ts +324 -0
- package/src/internal/entityRegistry.ts +46 -0
- package/src/internal/entityReply.ts +54 -0
- package/src/internal/entityRuntime.ts +1318 -0
- package/src/internal/entitySql.ts +60 -0
- package/src/internal/entityStorage.ts +168 -0
- package/src/internal/entityWire.ts +204 -0
- package/src/internal/queueRuntime.ts +174 -0
- package/src/internal/queueStorage.ts +255 -0
- package/src/internal/registry.ts +31 -0
- package/src/internal/singletonRegistry.ts +21 -0
- package/src/internal/singletonRuntime.ts +88 -0
- package/src/internal/singletonStorage.ts +64 -0
- package/src/internal/transport.ts +67 -0
- package/src/internal/workflowRegistry.ts +78 -0
- package/src/internal/workflowRuntime.ts +443 -0
- package/src/internal/workflowStorage.ts +234 -0
- package/src/internal/workflowWire.ts +123 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Creating effects from common sources
|
|
3
|
+
*
|
|
4
|
+
* Learn how to create effects from various sources, including plain values,
|
|
5
|
+
* synchronous code, Promise APIs, optional values, and callback-based APIs.
|
|
6
|
+
*/
|
|
7
|
+
import { Effect, Schema } from "effect"
|
|
8
|
+
|
|
9
|
+
class InvalidPayload extends Schema.TaggedError<InvalidPayload>()("InvalidPayload", {
|
|
10
|
+
input: Schema.String,
|
|
11
|
+
cause: Schema.Defect()
|
|
12
|
+
}) {}
|
|
13
|
+
|
|
14
|
+
class UserLookupError extends Schema.TaggedError<UserLookupError>()("UserLookupError", {
|
|
15
|
+
userId: Schema.Int,
|
|
16
|
+
cause: Schema.Defect()
|
|
17
|
+
}) {}
|
|
18
|
+
|
|
19
|
+
class MissingWorkspaceId extends Schema.TaggedError<MissingWorkspaceId>()("MissingWorkspaceId", {}) {}
|
|
20
|
+
|
|
21
|
+
// Some request fields are optional and may be absent.
|
|
22
|
+
const requestHeaders = new Map<string, string>([
|
|
23
|
+
["x-request-id", "req_1"]
|
|
24
|
+
])
|
|
25
|
+
|
|
26
|
+
// `Effect.succeed` wraps values you already have in memory.
|
|
27
|
+
export const fromValue = Effect.succeed({ env: "prod", retries: 3 })
|
|
28
|
+
|
|
29
|
+
// `Effect.sync` wraps synchronous side effects that should not throw.
|
|
30
|
+
export const fromSyncSideEffect = Effect.sync(() => Date.now())
|
|
31
|
+
|
|
32
|
+
// `Effect.try` wraps synchronous code that may throw.
|
|
33
|
+
export const parsePayload = Effect.fn("parsePayload")((input: string) =>
|
|
34
|
+
Effect.try({
|
|
35
|
+
try: () => JSON.parse(input) as { readonly userId: number },
|
|
36
|
+
catch: (cause) => new InvalidPayload({ input, cause })
|
|
37
|
+
})
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
const users = new Map<number, { readonly id: number; readonly name: string }>([
|
|
41
|
+
[1, { id: 1, name: "Ada" }],
|
|
42
|
+
[2, { id: 2, name: "Lin" }]
|
|
43
|
+
])
|
|
44
|
+
|
|
45
|
+
// `Effect.tryPromise` wraps Promise-based APIs that can reject or throw.
|
|
46
|
+
export const fetchUser = Effect.fn("fetchUser")((userId: number) =>
|
|
47
|
+
Effect.tryPromise({
|
|
48
|
+
async try() {
|
|
49
|
+
const user = users.get(userId)
|
|
50
|
+
if (!user) {
|
|
51
|
+
throw new Error(`Missing user ${userId}`)
|
|
52
|
+
}
|
|
53
|
+
return user
|
|
54
|
+
},
|
|
55
|
+
catch: (cause) => new UserLookupError({ userId, cause })
|
|
56
|
+
})
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
// `Effect.fromNullishOr` turns nullable values into a typed effect.
|
|
60
|
+
export const fromNullishHeader = Effect.fromNullishOr(requestHeaders.get("x-workspace-id")).pipe(
|
|
61
|
+
Effect.mapError(() => new MissingWorkspaceId())
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
// `Effect.callback` wraps callback-style asynchronous APIs.
|
|
65
|
+
export const fromCallback = Effect.callback<number>((resume) => {
|
|
66
|
+
const timeoutId = setTimeout(() => {
|
|
67
|
+
resume(Effect.succeed(200))
|
|
68
|
+
}, 10)
|
|
69
|
+
|
|
70
|
+
// Return a finalizer so interruption can cancel the callback source.
|
|
71
|
+
return Effect.sync(() => {
|
|
72
|
+
clearTimeout(timeoutId)
|
|
73
|
+
})
|
|
74
|
+
})
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
## Writing `Effect` code
|
|
2
|
+
|
|
3
|
+
Prefer `Effect.gen` for inline Effect code. For reusable functions, prefer
|
|
4
|
+
`Effect.fn("name")` when tracing is useful and `Effect.fnUntraced` when it is not,
|
|
5
|
+
particularly in library implementations and hot paths. Avoid functions that only
|
|
6
|
+
wrap and return `Effect.gen`. Attach additional behaviour with combinators; this
|
|
7
|
+
style is more readable and easier to maintain than using combinators alone.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Schema basics
|
|
3
|
+
*
|
|
4
|
+
* Define `Schema.Class`s, decode unknown input into typed values, and
|
|
5
|
+
* encode typed values back into their external representation.
|
|
6
|
+
*/
|
|
7
|
+
import { Effect, Schema } from "effect"
|
|
8
|
+
|
|
9
|
+
// Schema.Class defines both a runtime validator and a TypeScript class.
|
|
10
|
+
// This is useful for domain models that should only be constructed from valid
|
|
11
|
+
// data.
|
|
12
|
+
//
|
|
13
|
+
// The static `Type` and `Encoded` members are available when you need
|
|
14
|
+
// the decoded or encoded TypeScript representation.
|
|
15
|
+
export class User extends Schema.Class<User>("path/to/module/User")({
|
|
16
|
+
id: Schema.Int,
|
|
17
|
+
name: Schema.NonEmptyString,
|
|
18
|
+
email: Schema.String,
|
|
19
|
+
role: Schema.Literals(["admin", "member"])
|
|
20
|
+
}) {}
|
|
21
|
+
|
|
22
|
+
// `UserType` will be the type `User`, as schema classes use the class type as
|
|
23
|
+
// the validated type.
|
|
24
|
+
export type UserType = typeof User["Type"]
|
|
25
|
+
|
|
26
|
+
// Access the encoded type with `typeof YourSchema["Encoded"]`.
|
|
27
|
+
export type UserEncoded = typeof User["Encoded"]
|
|
28
|
+
|
|
29
|
+
// Reuse parsers at the edges of your application instead of rebuilding them for
|
|
30
|
+
// every request. Use the Effect-returning APIs when you are already inside
|
|
31
|
+
// Effect code so validation errors remain typed in the error channel.
|
|
32
|
+
export const decodeUser = Schema.decodeUnknownEffect(User)
|
|
33
|
+
export const encodeUser = Schema.encodeEffect(User)
|
|
34
|
+
|
|
35
|
+
export class InvalidUserPayload extends Schema.TaggedError<InvalidUserPayload>()("InvalidUserPayload", {
|
|
36
|
+
message: Schema.String
|
|
37
|
+
}) {}
|
|
38
|
+
|
|
39
|
+
export const parseUserPayload = Effect.fn("parseUserPayload")((input: unknown) =>
|
|
40
|
+
decodeUser(input).pipe(
|
|
41
|
+
Effect.mapError((error) => new InvalidUserPayload({ message: error.message }))
|
|
42
|
+
)
|
|
43
|
+
)
|
|
@@ -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/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
|
+
)
|