@effect/platform-cloudflare 0.0.0-stage → 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.
Files changed (237) hide show
  1. package/AGENTS.md +434 -0
  2. package/CLAUDE.md +434 -0
  3. package/LICENSE +21 -0
  4. package/README.md +505 -2
  5. package/ai-docs/README.md +44 -0
  6. package/ai-docs/package.json +36 -0
  7. package/ai-docs/src/01_effect/01_basics/01_effect-gen.ts +30 -0
  8. package/ai-docs/src/01_effect/01_basics/02_effect-fn.ts +52 -0
  9. package/ai-docs/src/01_effect/01_basics/10_creating-effects.ts +74 -0
  10. package/ai-docs/src/01_effect/01_basics/index.md +7 -0
  11. package/ai-docs/src/01_effect/02_schema/10_schema-basics.ts +43 -0
  12. package/ai-docs/src/01_effect/02_schema/index.md +7 -0
  13. package/ai-docs/src/01_effect/03_services/01_service.ts +45 -0
  14. package/ai-docs/src/01_effect/03_services/10_reference.ts +10 -0
  15. package/ai-docs/src/01_effect/03_services/20_layer-composition.ts +70 -0
  16. package/ai-docs/src/01_effect/03_services/20_layer-unwrap.ts +66 -0
  17. package/ai-docs/src/01_effect/03_services/index.md +5 -0
  18. package/ai-docs/src/01_effect/04_errors/01_error-handling.ts +30 -0
  19. package/ai-docs/src/01_effect/04_errors/10_catch-tags.ts +24 -0
  20. package/ai-docs/src/01_effect/04_errors/20_reason-errors.ts +64 -0
  21. package/ai-docs/src/01_effect/04_errors/index.md +1 -0
  22. package/ai-docs/src/01_effect/05_resources/10_acquire-release.ts +105 -0
  23. package/ai-docs/src/01_effect/05_resources/20_layer-side-effects.ts +31 -0
  24. package/ai-docs/src/01_effect/05_resources/30_layer-map.ts +86 -0
  25. package/ai-docs/src/01_effect/05_resources/index.md +3 -0
  26. package/ai-docs/src/01_effect/06_running/10_run-main.ts +30 -0
  27. package/ai-docs/src/01_effect/06_running/20_layer-launch.ts +27 -0
  28. package/ai-docs/src/01_effect/06_running/index.md +1 -0
  29. package/ai-docs/src/01_effect/07_pubsub/10_pubsub.ts +56 -0
  30. package/ai-docs/src/01_effect/07_pubsub/index.md +3 -0
  31. package/ai-docs/src/03_stream/10_creating-streams.ts +103 -0
  32. package/ai-docs/src/03_stream/20_consuming-streams.ts +137 -0
  33. package/ai-docs/src/03_stream/30_encoding.ts +163 -0
  34. package/ai-docs/src/03_stream/index.md +4 -0
  35. package/ai-docs/src/04_integration/10_managed-runtime.ts +129 -0
  36. package/ai-docs/src/04_integration/index.md +5 -0
  37. package/ai-docs/src/05_batching/10_request-resolver.ts +89 -0
  38. package/ai-docs/src/05_batching/index.md +3 -0
  39. package/ai-docs/src/06_schedule/10_schedules.ts +111 -0
  40. package/ai-docs/src/06_schedule/index.md +3 -0
  41. package/ai-docs/src/07_datetime/10_creating-and-formatting.ts +30 -0
  42. package/ai-docs/src/07_datetime/20_time-zones.ts +44 -0
  43. package/ai-docs/src/07_datetime/index.md +5 -0
  44. package/ai-docs/src/08_observability/10_logging.ts +66 -0
  45. package/ai-docs/src/08_observability/20_otlp-tracing.ts +95 -0
  46. package/ai-docs/src/08_observability/index.md +7 -0
  47. package/ai-docs/src/09_testing/10_effect-tests.ts +55 -0
  48. package/ai-docs/src/09_testing/20_layer-tests.ts +138 -0
  49. package/ai-docs/src/09_testing/index.md +1 -0
  50. package/ai-docs/src/10_predicate/01_basics.ts +14 -0
  51. package/ai-docs/src/10_predicate/index.md +9 -0
  52. package/ai-docs/src/40_sql/10_basics.ts +175 -0
  53. package/ai-docs/src/40_sql/index.md +6 -0
  54. package/ai-docs/src/50_http-client/10_basics.ts +102 -0
  55. package/ai-docs/src/50_http-client/index.md +3 -0
  56. package/ai-docs/src/51_http-server/10_basics.ts +118 -0
  57. package/ai-docs/src/51_http-server/20_testing.ts +100 -0
  58. package/ai-docs/src/51_http-server/fixtures/api/Api.ts +14 -0
  59. package/ai-docs/src/51_http-server/fixtures/api/Authorization.ts +36 -0
  60. package/ai-docs/src/51_http-server/fixtures/api/System.ts +10 -0
  61. package/ai-docs/src/51_http-server/fixtures/api/Users.ts +107 -0
  62. package/ai-docs/src/51_http-server/fixtures/domain/User.ts +21 -0
  63. package/ai-docs/src/51_http-server/fixtures/domain/UserErrors.ts +22 -0
  64. package/ai-docs/src/51_http-server/fixtures/server/Authorization.ts +37 -0
  65. package/ai-docs/src/51_http-server/fixtures/server/Users/http.ts +80 -0
  66. package/ai-docs/src/51_http-server/fixtures/server/Users.ts +183 -0
  67. package/ai-docs/src/51_http-server/index.md +3 -0
  68. package/ai-docs/src/52_graphql/10_basics.ts +117 -0
  69. package/ai-docs/src/52_graphql/20_testing.ts +45 -0
  70. package/ai-docs/src/52_graphql/30_batching.ts +118 -0
  71. package/ai-docs/src/52_graphql/40_aliased-batching.ts +124 -0
  72. package/ai-docs/src/52_graphql/50_errors-and-retries.ts +74 -0
  73. package/ai-docs/src/52_graphql/index.md +6 -0
  74. package/ai-docs/src/60_child-process/10_working-with-child-processes.ts +117 -0
  75. package/ai-docs/src/60_child-process/index.md +3 -0
  76. package/ai-docs/src/70_cli/10_basics.ts +162 -0
  77. package/ai-docs/src/70_cli/index.md +5 -0
  78. package/ai-docs/src/71_ai/10_language-model.ts +156 -0
  79. package/ai-docs/src/71_ai/20_tools.ts +226 -0
  80. package/ai-docs/src/71_ai/30_chat.ts +158 -0
  81. package/ai-docs/src/71_ai/fixtures/domain/LaunchPlan.ts +9 -0
  82. package/ai-docs/src/71_ai/index.md +5 -0
  83. package/ai-docs/src/80_cluster/10_entities.ts +97 -0
  84. package/ai-docs/src/80_cluster/index.md +4 -0
  85. package/ai-docs/src/index.md +10 -0
  86. package/ai-docs/tsconfig.json +28 -0
  87. package/dist/AlchemyCloudflareCluster.d.ts +134 -0
  88. package/dist/AlchemyCloudflareCluster.d.ts.map +1 -0
  89. package/dist/AlchemyCloudflareCluster.js +111 -0
  90. package/dist/AlchemyCloudflareCluster.js.map +1 -0
  91. package/dist/CloudflareCluster.d.ts +196 -0
  92. package/dist/CloudflareCluster.d.ts.map +1 -0
  93. package/dist/CloudflareCluster.js +533 -0
  94. package/dist/CloudflareCluster.js.map +1 -0
  95. package/dist/CloudflareDurableObjectPrograms.d.ts +279 -0
  96. package/dist/CloudflareDurableObjectPrograms.d.ts.map +1 -0
  97. package/dist/CloudflareDurableObjectPrograms.js +295 -0
  98. package/dist/CloudflareDurableObjectPrograms.js.map +1 -0
  99. package/dist/CloudflareDurableObjects.d.ts +149 -0
  100. package/dist/CloudflareDurableObjects.d.ts.map +1 -0
  101. package/dist/CloudflareDurableObjects.js +337 -0
  102. package/dist/CloudflareDurableObjects.js.map +1 -0
  103. package/dist/CloudflarePersistedQueue.d.ts +36 -0
  104. package/dist/CloudflarePersistedQueue.d.ts.map +1 -0
  105. package/dist/CloudflarePersistedQueue.js +122 -0
  106. package/dist/CloudflarePersistedQueue.js.map +1 -0
  107. package/dist/CloudflareWorkflowEngine.d.ts +72 -0
  108. package/dist/CloudflareWorkflowEngine.d.ts.map +1 -0
  109. package/dist/CloudflareWorkflowEngine.js +187 -0
  110. package/dist/CloudflareWorkflowEngine.js.map +1 -0
  111. package/dist/index.d.ts +29 -0
  112. package/dist/index.d.ts.map +1 -0
  113. package/dist/index.js +33 -0
  114. package/dist/index.js.map +1 -0
  115. package/dist/internal/alchemyCluster.d.ts +2 -0
  116. package/dist/internal/alchemyCluster.d.ts.map +1 -0
  117. package/dist/internal/alchemyCluster.js +75 -0
  118. package/dist/internal/alchemyCluster.js.map +1 -0
  119. package/dist/internal/boundedMap.d.ts +3 -0
  120. package/dist/internal/boundedMap.d.ts.map +1 -0
  121. package/dist/internal/boundedMap.js +15 -0
  122. package/dist/internal/boundedMap.js.map +1 -0
  123. package/dist/internal/cluster.d.ts +57 -0
  124. package/dist/internal/cluster.d.ts.map +1 -0
  125. package/dist/internal/cluster.js +2 -0
  126. package/dist/internal/cluster.js.map +1 -0
  127. package/dist/internal/clusterName.d.ts +2 -0
  128. package/dist/internal/clusterName.d.ts.map +1 -0
  129. package/dist/internal/clusterName.js +16 -0
  130. package/dist/internal/clusterName.js.map +1 -0
  131. package/dist/internal/entityKeepAlive.d.ts +2 -0
  132. package/dist/internal/entityKeepAlive.d.ts.map +1 -0
  133. package/dist/internal/entityKeepAlive.js +73 -0
  134. package/dist/internal/entityKeepAlive.js.map +1 -0
  135. package/dist/internal/entityMailbox.d.ts +2 -0
  136. package/dist/internal/entityMailbox.d.ts.map +1 -0
  137. package/dist/internal/entityMailbox.js +160 -0
  138. package/dist/internal/entityMailbox.js.map +1 -0
  139. package/dist/internal/entityRegistry.d.ts +16 -0
  140. package/dist/internal/entityRegistry.d.ts.map +1 -0
  141. package/dist/internal/entityRegistry.js +22 -0
  142. package/dist/internal/entityRegistry.js.map +1 -0
  143. package/dist/internal/entityReply.d.ts +2 -0
  144. package/dist/internal/entityReply.d.ts.map +1 -0
  145. package/dist/internal/entityReply.js +35 -0
  146. package/dist/internal/entityReply.js.map +1 -0
  147. package/dist/internal/entityRuntime.d.ts +2 -0
  148. package/dist/internal/entityRuntime.d.ts.map +1 -0
  149. package/dist/internal/entityRuntime.js +823 -0
  150. package/dist/internal/entityRuntime.js.map +1 -0
  151. package/dist/internal/entitySql.d.ts +2 -0
  152. package/dist/internal/entitySql.d.ts.map +1 -0
  153. package/dist/internal/entitySql.js +29 -0
  154. package/dist/internal/entitySql.js.map +1 -0
  155. package/dist/internal/entityStorage.d.ts +2 -0
  156. package/dist/internal/entityStorage.d.ts.map +1 -0
  157. package/dist/internal/entityStorage.js +98 -0
  158. package/dist/internal/entityStorage.js.map +1 -0
  159. package/dist/internal/entityWire.d.ts +2 -0
  160. package/dist/internal/entityWire.d.ts.map +1 -0
  161. package/dist/internal/entityWire.js +146 -0
  162. package/dist/internal/entityWire.js.map +1 -0
  163. package/dist/internal/queueRuntime.d.ts +2 -0
  164. package/dist/internal/queueRuntime.d.ts.map +1 -0
  165. package/dist/internal/queueRuntime.js +88 -0
  166. package/dist/internal/queueRuntime.js.map +1 -0
  167. package/dist/internal/queueStorage.d.ts +2 -0
  168. package/dist/internal/queueStorage.d.ts.map +1 -0
  169. package/dist/internal/queueStorage.js +129 -0
  170. package/dist/internal/queueStorage.js.map +1 -0
  171. package/dist/internal/registry.d.ts +10 -0
  172. package/dist/internal/registry.d.ts.map +1 -0
  173. package/dist/internal/registry.js +24 -0
  174. package/dist/internal/registry.js.map +1 -0
  175. package/dist/internal/singletonRegistry.d.ts +2 -0
  176. package/dist/internal/singletonRegistry.d.ts.map +1 -0
  177. package/dist/internal/singletonRegistry.js +9 -0
  178. package/dist/internal/singletonRegistry.js.map +1 -0
  179. package/dist/internal/singletonRuntime.d.ts +2 -0
  180. package/dist/internal/singletonRuntime.d.ts.map +1 -0
  181. package/dist/internal/singletonRuntime.js +42 -0
  182. package/dist/internal/singletonRuntime.js.map +1 -0
  183. package/dist/internal/singletonStorage.d.ts +2 -0
  184. package/dist/internal/singletonStorage.d.ts.map +1 -0
  185. package/dist/internal/singletonStorage.js +34 -0
  186. package/dist/internal/singletonStorage.js.map +1 -0
  187. package/dist/internal/transport.d.ts +2 -0
  188. package/dist/internal/transport.d.ts.map +1 -0
  189. package/dist/internal/transport.js +49 -0
  190. package/dist/internal/transport.js.map +1 -0
  191. package/dist/internal/workflowRegistry.d.ts +2 -0
  192. package/dist/internal/workflowRegistry.d.ts.map +1 -0
  193. package/dist/internal/workflowRegistry.js +29 -0
  194. package/dist/internal/workflowRegistry.js.map +1 -0
  195. package/dist/internal/workflowRuntime.d.ts +2 -0
  196. package/dist/internal/workflowRuntime.d.ts.map +1 -0
  197. package/dist/internal/workflowRuntime.js +322 -0
  198. package/dist/internal/workflowRuntime.js.map +1 -0
  199. package/dist/internal/workflowStorage.d.ts +2 -0
  200. package/dist/internal/workflowStorage.d.ts.map +1 -0
  201. package/dist/internal/workflowStorage.js +113 -0
  202. package/dist/internal/workflowStorage.js.map +1 -0
  203. package/dist/internal/workflowWire.d.ts +2 -0
  204. package/dist/internal/workflowWire.d.ts.map +1 -0
  205. package/dist/internal/workflowWire.js +52 -0
  206. package/dist/internal/workflowWire.js.map +1 -0
  207. package/package.json +74 -3
  208. package/src/AlchemyCloudflareCluster.ts +200 -0
  209. package/src/CloudflareCluster.ts +704 -0
  210. package/src/CloudflareDurableObjectPrograms.ts +543 -0
  211. package/src/CloudflareDurableObjects.ts +430 -0
  212. package/src/CloudflarePersistedQueue.ts +189 -0
  213. package/src/CloudflareWorkflowEngine.ts +220 -0
  214. package/src/index.ts +38 -0
  215. package/src/internal/alchemyCluster.ts +120 -0
  216. package/src/internal/boundedMap.ts +15 -0
  217. package/src/internal/cluster.ts +57 -0
  218. package/src/internal/clusterName.ts +19 -0
  219. package/src/internal/entityKeepAlive.ts +121 -0
  220. package/src/internal/entityMailbox.ts +324 -0
  221. package/src/internal/entityRegistry.ts +46 -0
  222. package/src/internal/entityReply.ts +54 -0
  223. package/src/internal/entityRuntime.ts +1318 -0
  224. package/src/internal/entitySql.ts +60 -0
  225. package/src/internal/entityStorage.ts +168 -0
  226. package/src/internal/entityWire.ts +204 -0
  227. package/src/internal/queueRuntime.ts +174 -0
  228. package/src/internal/queueStorage.ts +255 -0
  229. package/src/internal/registry.ts +31 -0
  230. package/src/internal/singletonRegistry.ts +21 -0
  231. package/src/internal/singletonRuntime.ts +88 -0
  232. package/src/internal/singletonStorage.ts +64 -0
  233. package/src/internal/transport.ts +67 -0
  234. package/src/internal/workflowRegistry.ts +78 -0
  235. package/src/internal/workflowRuntime.ts +443 -0
  236. package/src/internal/workflowStorage.ts +234 -0
  237. package/src/internal/workflowWire.ts +123 -0
@@ -0,0 +1,45 @@
1
+ /**
2
+ * @title Testing code that uses a GraphQL client
3
+ *
4
+ * Replace a client service with `GraphQLClient.layerTest`, answering each
5
+ * operation from a handler instead of a server.
6
+ */
7
+ import { assert, describe, it } from "@effect/vitest"
8
+ import { Effect } from "effect"
9
+ import { GitHub } from "./10_basics.ts"
10
+
11
+ // Code under test that depends on the `GitHub` client service.
12
+ const issueTitles = (owner: string, name: string) =>
13
+ GitHub.use((github) => github.RepoIssues({ owner, name })).pipe(
14
+ Effect.map(({ repository }) => repository?.issues.nodes?.map((issue) => issue?.title) ?? [])
15
+ )
16
+
17
+ // The service binds `GraphQLClient.layerTest(GitHub, GitHubGraphQLGroup)` once
18
+ // as a static helper. Handlers are keyed by operation name, receive the
19
+ // caller's variables and options, and return results in their decoded form.
20
+ // Nothing is encoded or decoded and no middleware runs. An operation without
21
+ // a handler dies with an `UnimplementedError` defect.
22
+ const GitHubTest = GitHub.layerTest({
23
+ RepoIssues: ({ owner, name }) =>
24
+ Effect.succeed({
25
+ repository: {
26
+ issues: {
27
+ pageInfo: { hasNextPage: false, endCursor: null },
28
+ nodes: [{ number: 1, title: `Hello from ${owner}/${name}` }]
29
+ }
30
+ }
31
+ })
32
+ })
33
+
34
+ describe("issueTitles", () => {
35
+ it.effect("returns the titles of the first page", () =>
36
+ Effect.gen(function*() {
37
+ const titles = yield* issueTitles("Effect-TS", "effect")
38
+ assert.deepStrictEqual(titles, ["Hello from Effect-TS/effect"])
39
+ }).pipe(Effect.provide(GitHubTest)))
40
+ })
41
+
42
+ // `layerTest` uses `makeTest`, which skips the protocol and cannot return
43
+ // partial data together with `errors`. To test that, or the encode and decode
44
+ // path, build the real client with `GraphQLClient.make` over a scripted
45
+ // `GraphQLProtocol` layer (see `GraphQLProtocol`).
@@ -0,0 +1,118 @@
1
+ /**
2
+ * @title Batching lookups with GraphQLResolver
3
+ *
4
+ * Collect concurrent lookups by id into one `nodes(ids:)` request with
5
+ * `GraphQLResolver`, instead of one request per id.
6
+ */
7
+ import { type Cause, Context, Effect, Layer, RequestResolver, Schema } from "effect"
8
+ import { GraphQL, GraphQLClient, GraphQLGroup, GraphQLProtocol, GraphQLResolver } from "effect/graphql"
9
+ import { FetchHttpClient } from "effect/http"
10
+ import { GitHubAuth } from "./10_basics.ts"
11
+
12
+ // The generator's output for `repo-nodes.graphql`:
13
+ //
14
+ // query RepoNodes($ids: [ID!]!) {
15
+ // nodes(ids: $ids) { __typename ... on Repository { nameWithOwner } }
16
+ // }
17
+ //
18
+ // Generated modules import shared codecs and the possible `__typename`s of
19
+ // each interface from the module the generator writes next to the schema
20
+ // (`import * as Shared from "../schema.graphql.ts"`); here `Shared` is
21
+ // declared inline.
22
+ const Shared = { ID: Schema.String }
23
+ declare namespace Shared {
24
+ export namespace Typename {
25
+ export type Node = "Issue" | "PullRequest" | "Repository" | "User"
26
+ }
27
+ }
28
+
29
+ export const Repository = Schema.Struct({
30
+ __typename: Schema.Literal("Repository"),
31
+ nameWithOwner: Schema.String
32
+ })
33
+ export interface Repository extends Schema.Struct.Type<typeof Repository.fields> {}
34
+
35
+ // Unselected node types are represented by `otherTypename`.
36
+ export const NodeOther = Schema.Struct({
37
+ __typename: GraphQL.otherTypename<Shared.Typename.Node>()(["Repository"])
38
+ })
39
+ export interface NodeOther extends Schema.Struct.Type<typeof NodeOther.fields> {}
40
+
41
+ /** `query RepoNodes` */
42
+ export const RepoNodes = GraphQL.query("RepoNodes", {
43
+ document: {
44
+ variables: [
45
+ { name: "ids", body: "[ID!]!" }
46
+ ],
47
+ selections: [
48
+ { name: "nodes", body: ["(ids:$", "ids", "){__typename...on Repository{nameWithOwner}}"] }
49
+ ],
50
+ fragments: []
51
+ },
52
+ variables: {
53
+ ids: Schema.Array(Shared.ID)
54
+ },
55
+ result: Schema.Struct({
56
+ nodes: Schema.Array(Schema.NullOr(Schema.Union([
57
+ Repository,
58
+ NodeOther
59
+ ])))
60
+ })
61
+ })
62
+ export declare namespace RepoNodes {
63
+ export type Variables = typeof RepoNodes.variables.Type
64
+ export type Result = typeof RepoNodes.result.Type
65
+ }
66
+
67
+ /** Every operation in repo-nodes.graphql. */
68
+ export const RepoNodesGraphQLGroup = GraphQLGroup.make(RepoNodes)
69
+
70
+ // Your code: attach the middleware to the generated group.
71
+ export const GitHubGraphQLGroup = RepoNodesGraphQLGroup.middleware(GitHubAuth)
72
+
73
+ export class GitHub extends Context.Service<
74
+ GitHub,
75
+ GraphQLClient.FromGroup<typeof GitHubGraphQLGroup> & {
76
+ getRepo(id: string): Effect.Effect<
77
+ Repository,
78
+ GraphQL.Error<typeof RepoNodes> | Cause.NoSuchElementError
79
+ >
80
+ }
81
+ >()("app/GitHub") {
82
+ static readonly layer = Layer.effect(
83
+ GitHub,
84
+ Effect.gen(function*() {
85
+ const client = yield* GraphQLClient.make(GitHubGraphQLGroup)
86
+
87
+ // Null entries and non-Repository nodes fail with NoSuchElementError.
88
+ // GitHub accepts at most 100 ids per request.
89
+ //
90
+ // GitHubAuth reads one token from config, so every lookup can share a
91
+ // batch. If a middleware `requires` a per-request service, such as the
92
+ // signed-in user, `batchKey` is required and keeps lookups for different
93
+ // users in separate calls:
94
+ //
95
+ // batchKey: (context) => Context.get(context, CurrentUser).id
96
+ const RepoById = GraphQLResolver.ordered(client.RepoNodes, {
97
+ variables: (ids: ReadonlyArray<string>) => ({ ids }),
98
+ path: ["nodes"],
99
+ typename: "Repository"
100
+ }).pipe(RequestResolver.batchN(100))
101
+ return { ...client, getRepo: GraphQLResolver.request(RepoById) }
102
+ })
103
+ ).pipe(
104
+ Layer.provide(GitHubAuth.layer),
105
+ Layer.provide(GraphQLProtocol.layerHttp({ url: "https://api.github.com/graphql" })),
106
+ Layer.provide(FetchHttpClient.layer)
107
+ )
108
+ }
109
+
110
+ export const repoNames = Effect.fn("repoNames")(function*(ids: ReadonlyArray<string>) {
111
+ const github = yield* GitHub
112
+ const repos = yield* Effect.forEach(ids, github.getRepo, { concurrency: "unbounded" })
113
+ return repos.map((repo) => repo.nameWithOwner)
114
+ })
115
+
116
+ export const program = repoNames(["R_1", "R_2"]).pipe(
117
+ Effect.provide(GitHub.layer)
118
+ )
@@ -0,0 +1,124 @@
1
+ /**
2
+ * @title Batching single-item lookups with aliases
3
+ *
4
+ * When an API has no field that takes a list of keys, `GraphQLResolver.aliased`
5
+ * repeats a generated operation under aliases, so concurrent lookups share one
6
+ * request and keep their generated types.
7
+ */
8
+ import { Context, Effect, Layer, Schema } from "effect"
9
+ import { GraphQL, GraphQLClient, GraphQLGroup, GraphQLProtocol, GraphQLResolver } from "effect/graphql"
10
+ import { FetchHttpClient } from "effect/http"
11
+ import { GitHubAuth } from "./10_basics.ts"
12
+
13
+ // The generator's output for `pr.graphql`:
14
+ //
15
+ // query PullRequest($owner: String!, $name: String!, $number: Int!) {
16
+ // repository(owner: $owner, name: $name) {
17
+ // pullRequest(number: $number) { number title }
18
+ // }
19
+ // }
20
+ //
21
+ // Generated modules import shared codecs, such as `Int`, from the module the
22
+ // generator writes next to the schema (`import * as Shared from
23
+ // "../schema.graphql.ts"`); here `Shared` is declared inline.
24
+ const Shared = { Int: Schema.Number.check(Schema.isInt32()) }
25
+
26
+ export const PullRequestPullRequest = Schema.Struct({
27
+ number: Shared.Int,
28
+ title: Schema.String
29
+ })
30
+ export interface PullRequestPullRequest extends Schema.Struct.Type<typeof PullRequestPullRequest.fields> {}
31
+
32
+ export const Repository = Schema.Struct({
33
+ pullRequest: Schema.NullOr(PullRequestPullRequest)
34
+ })
35
+ export interface Repository extends Schema.Struct.Type<typeof Repository.fields> {}
36
+
37
+ // The generator gives queries and mutations their document as a
38
+ // `GraphQL.DocumentTemplate`. The odd entries of each `body` are the names
39
+ // `aliased` renames per slice; joined together they are the printed document.
40
+ /** `query PullRequest` */
41
+ export const PullRequest = GraphQL.query("PullRequest", {
42
+ document: {
43
+ variables: [
44
+ { name: "owner", body: "String!" },
45
+ { name: "name", body: "String!" },
46
+ { name: "number", body: "Int!" }
47
+ ],
48
+ selections: [
49
+ {
50
+ name: "repository",
51
+ body: ["(owner:$", "owner", " name:$", "name", "){pullRequest(number:$", "number", "){number title}}"]
52
+ }
53
+ ],
54
+ fragments: []
55
+ },
56
+ variables: {
57
+ owner: Schema.String,
58
+ name: Schema.String,
59
+ number: Shared.Int
60
+ },
61
+ result: Schema.Struct({
62
+ repository: Schema.NullOr(Repository)
63
+ })
64
+ })
65
+ export declare namespace PullRequest {
66
+ export type Variables = typeof PullRequest.variables.Type
67
+ export type Result = typeof PullRequest.result.Type
68
+ }
69
+
70
+ /** Every operation in pr.graphql. */
71
+ export const PrGraphQLGroup = GraphQLGroup.make(PullRequest)
72
+
73
+ // Your code: attach the middleware to the generated group.
74
+ export const GitHubGraphQLGroup = PrGraphQLGroup.middleware(GitHubAuth)
75
+
76
+ export class GitHub extends Context.Service<
77
+ GitHub,
78
+ GraphQLClient.FromGroup<typeof GitHubGraphQLGroup> & {
79
+ readonly getPullRequest: (
80
+ variables: GraphQL.Variables<typeof PullRequest>
81
+ ) => Effect.Effect<GraphQL.Result<typeof PullRequest>, GraphQL.Error<typeof PullRequest>>
82
+ }
83
+ >()("app/GitHub") {
84
+ static readonly layer = Layer.effect(
85
+ GitHub,
86
+ Effect.gen(function*() {
87
+ const client = yield* GraphQLClient.make(GitHubGraphQLGroup)
88
+
89
+ // Concurrent calls go out as one POST whose root fields are aliased
90
+ // `_0_repository`, `_1_repository`, and so on. Each slice is decoded
91
+ // with the generated result Schema, and a GraphQL error under
92
+ // `_1_repository` fails only that call.
93
+ //
94
+ // A batch holds 10 slices by default, because GitHub's query cost and
95
+ // timeouts grow with the size of the query. Pipe
96
+ // `RequestResolver.batchN` to change it.
97
+ const PullRequests = GraphQLResolver.aliased(client.PullRequest)
98
+
99
+ // `request` turns the resolver into a lookup by the operation's
100
+ // variables, as for `ordered` and `findById`.
101
+ return { ...client, getPullRequest: GraphQLResolver.request(PullRequests) }
102
+ })
103
+ ).pipe(
104
+ Layer.provide(GitHubAuth.layer),
105
+ Layer.provide(GraphQLProtocol.layerHttp({ url: "https://api.github.com/graphql" })),
106
+ Layer.provide(FetchHttpClient.layer)
107
+ )
108
+ }
109
+
110
+ export const pullRequestTitles = Effect.fn("pullRequestTitles")(function*(numbers: ReadonlyArray<number>) {
111
+ const github = yield* GitHub
112
+ const results = yield* Effect.forEach(
113
+ numbers,
114
+ (number) => github.getPullRequest({ owner: "Effect-TS", name: "effect", number }),
115
+ { concurrency: "unbounded" }
116
+ )
117
+ // `repository` and `pullRequest` are nullable, so a missing pull request is
118
+ // `null` rather than an error that would fail every slice.
119
+ return results.map((result) => result.repository?.pullRequest?.title ?? null)
120
+ })
121
+
122
+ export const program = pullRequestTitles([1, 2, 3]).pipe(
123
+ Effect.provide(GitHub.layer)
124
+ )
@@ -0,0 +1,74 @@
1
+ /**
2
+ * @title Handling GraphQL errors, retries and rate limits
3
+ *
4
+ * Read rate-limit headers in a middleware, tell GraphQL errors apart by their
5
+ * fields, and retry timeouts and rate limits with `Effect.retry`.
6
+ */
7
+ import { Duration, Effect, Layer, Schedule } from "effect"
8
+ import { type GraphQLClientError, GraphQLMiddleware } from "effect/graphql"
9
+ import { GitHub } from "./10_basics.ts"
10
+
11
+ // Middleware see each response's HTTP `status` and `headers` on the result
12
+ // `next` returns, including responses with GraphQL `errors`. A failed call
13
+ // keeps them, and `GraphQLClientError` exposes them as `headers` and
14
+ // `status`. Attach it with `GraphQLGroup.make(...).middleware(RateLimit)`.
15
+ export class RateLimit extends GraphQLMiddleware.Service<RateLimit>()("app/RateLimit") {
16
+ static readonly layer = Layer.succeed(RateLimit, {
17
+ execute: ({ next, request }) =>
18
+ Effect.tap(next(request), (result) => {
19
+ const remaining = Number(result.headers?.["x-ratelimit-remaining"])
20
+ return remaining < 500
21
+ ? Effect.logWarning(`GitHub rate limit: ${remaining} points left`)
22
+ : Effect.void
23
+ }),
24
+ subscribe: ({ next, request }) => next(request)
25
+ })
26
+ }
27
+
28
+ // GraphQL errors keep fields the specification doesn't define as JSON, such
29
+ // as GitHub's top-level `type` (`NOT_FOUND`, `FORBIDDEN`, `RATE_LIMITED`).
30
+ export const hasErrorType = (error: GraphQLClientError.GraphQLClientError, type: string) =>
31
+ error.reason._tag === "ResponseError" && error.reason.errors.every((entry) => entry.type === type)
32
+
33
+ // `isRetryable` is true for network failures, for 429 or 5xx responses, and
34
+ // for any error with a `Retry-After` header, whatever its status. That covers
35
+ // GitHub's 502 and 504 query timeouts, even with a GraphQL `errors` body, and
36
+ // its secondary rate limits (403 or 429 with `retry-after`). A primary rate
37
+ // limit can come without `retry-after`, with `x-ratelimit-remaining: 0`, so
38
+ // check that header too.
39
+ const rateLimited = (error: GraphQLClientError.GraphQLClientError) => error.headers?.["x-ratelimit-remaining"] === "0"
40
+
41
+ // Wait for `retryAfter` when it is set, and until `x-ratelimit-reset` (epoch
42
+ // seconds) on a primary rate limit. GitHub may ban an integration that keeps
43
+ // retrying while rate limited, so don't retry with a plain backoff.
44
+ const gitHubRetry = Schedule.exponential("1 second").pipe(
45
+ Schedule.setInputType<GraphQLClientError.GraphQLClientError>(),
46
+ Schedule.modifyDelay(({ duration, input, now }) => {
47
+ if (input.retryAfter !== undefined) return Effect.succeed(input.retryAfter)
48
+ if (!rateLimited(input)) return Effect.succeed(duration)
49
+ const reset = Number(input.headers?.["x-ratelimit-reset"]) * 1000
50
+ return Effect.succeed(Number.isFinite(reset) ? Duration.millis(Math.max(0, reset - now)) : Duration.minutes(1))
51
+ })
52
+ )
53
+
54
+ // The client never retries queries or mutations on its own. Retry only
55
+ // mutations that are safe to send twice, and with `GraphQLClient.pages`,
56
+ // retry inside a middleware, because `Stream.retry` starts again from the
57
+ // first page. Subscriptions reconnect on their own after a retryable failure,
58
+ // on `GraphQLClient.defaultSubscriptionRetry`, which waits for `retryAfter`.
59
+ // A primary rate limit without `retry-after` isn't retryable, so it ends the
60
+ // subscription; wait until `x-ratelimit-reset` before subscribing again.
61
+ export const retryGitHub = <A, R>(effect: Effect.Effect<A, GraphQLClientError.GraphQLClientError, R>) =>
62
+ Effect.retry(effect, {
63
+ schedule: gitHubRetry,
64
+ while: (error) => error.isRetryable || rateLimited(error),
65
+ times: 5
66
+ })
67
+
68
+ export const program = Effect.gen(function*() {
69
+ const github = yield* GitHub
70
+ const result = yield* retryGitHub(github.RepoIssues({ owner: "Effect-TS", name: "missing" })).pipe(
71
+ Effect.catchIf((error) => hasErrorType(error, "NOT_FOUND"), () => Effect.succeed(null))
72
+ )
73
+ yield* Effect.log(result === null ? "No such repository" : `${result.repository?.issues.nodes?.length} issues`)
74
+ }).pipe(Effect.provide(GitHub.layer))
@@ -0,0 +1,6 @@
1
+ ## Typed GraphQL clients with `effect/graphql`
2
+
3
+ Use the `effect/graphql` modules to call GraphQL APIs with typed
4
+ operations. Run `graphqlgen` from `@effect/graphql-generator` to generate
5
+ operations from `.graphql` documents, then build a client with
6
+ `GraphQLClient.make` and provide a transport from `GraphQLProtocol`.
@@ -0,0 +1,117 @@
1
+ /**
2
+ * @title Working with child processes
3
+ *
4
+ * This example shows how to collect process output, compose pipelines, and stream long-running command output.
5
+ */
6
+ import { NodeServices } from "@effect/platform-node"
7
+ import { Console, Context, Effect, Layer, Schema, Stream, String } from "effect"
8
+ import { ChildProcess, ChildProcessSpawner } from "effect/process"
9
+
10
+ export class DevToolsError extends Schema.TaggedError<DevToolsError>()("DevToolsError", {
11
+ cause: Schema.Defect()
12
+ }) {}
13
+
14
+ export class DevTools extends Context.Service<DevTools, {
15
+ readonly nodeVersion: Effect.Effect<string, DevToolsError>
16
+ readonly recentCommitSubjects: Effect.Effect<ReadonlyArray<string>, DevToolsError>
17
+ readonly runLintFix: Effect.Effect<void, DevToolsError>
18
+ changedTypeScriptFiles(baseRef: string): Effect.Effect<ReadonlyArray<string>, DevToolsError>
19
+ }>()("docs/DevTools") {
20
+ static readonly layer = Layer.effect(
21
+ DevTools,
22
+ Effect.gen(function*() {
23
+ // To run child processes, we need access to a `ChildProcessSpawner`.
24
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
25
+
26
+ // Use `spawner.string` when you want to collect the entire output of a
27
+ // command as a string. This runs `node --version` and collects the
28
+ // output.
29
+ const nodeVersion = spawner.string(
30
+ ChildProcess.make("node", ["--version"])
31
+ ).pipe(
32
+ Effect.map(String.trim),
33
+ Effect.mapError((cause) => new DevToolsError({ cause }))
34
+ )
35
+
36
+ const changedTypeScriptFiles = Effect.fn("DevTools.changedTypeScriptFiles")(function*(baseRef: string) {
37
+ yield* Effect.annotateCurrentSpan({ baseRef })
38
+
39
+ // `spawner.lines` is a convenience helper for line-oriented command
40
+ // output.
41
+ const files = yield* spawner.lines(
42
+ ChildProcess.make("git", ["diff", "--name-only", `${baseRef}...HEAD`])
43
+ ).pipe(
44
+ Effect.mapError((cause) => new DevToolsError({ cause }))
45
+ )
46
+
47
+ return files.filter((file) => file.endsWith(".ts"))
48
+ })
49
+
50
+ // Build a pipeline from two command values. This runs:
51
+ // `git log --pretty=format:%s -n 20 | head -n 5`
52
+ const recentCommitSubjects = spawner.lines(
53
+ ChildProcess.make("git", ["log", "--pretty=format:%s", "-n", "20"]).pipe(
54
+ ChildProcess.pipeTo(ChildProcess.make("head", ["-n", "5"]))
55
+ )
56
+ ).pipe(
57
+ Effect.mapError((cause) => new DevToolsError({ cause }))
58
+ )
59
+
60
+ const runLintFix = Effect.gen(function*() {
61
+ // Use `spawn` when you want the process handle and stream output while
62
+ // the process is still running.
63
+ const handle = yield* spawner.spawn(
64
+ ChildProcess.make("pnpm", ["lint-fix"], {
65
+ env: { FORCE_COLOR: "1" },
66
+ extendEnv: true
67
+ })
68
+ ).pipe(
69
+ Effect.mapError((cause) => new DevToolsError({ cause }))
70
+ )
71
+
72
+ yield* handle.all.pipe(
73
+ Stream.decodeText(),
74
+ Stream.splitLines,
75
+ Stream.runForEach((line) => Console.log(`[lint-fix] ${line}`)),
76
+ Effect.mapError((cause) => new DevToolsError({ cause }))
77
+ )
78
+
79
+ const exitCode = yield* handle.exitCode.pipe(
80
+ Effect.mapError((cause) => new DevToolsError({ cause }))
81
+ )
82
+
83
+ if (exitCode !== ChildProcessSpawner.ExitCode(0)) {
84
+ return yield* new DevToolsError({
85
+ cause: new Error(`pnpm lint-fix failed with exit code ${exitCode}`)
86
+ })
87
+ }
88
+ }).pipe(
89
+ // `spawner.spawn` adds a `Scope` requirement to manage the lifecycle of
90
+ // the child process. We can use `Effect.scoped` to provide a `Scope`
91
+ // and close it when the effect completes.
92
+ Effect.scoped
93
+ )
94
+
95
+ return DevTools.of({
96
+ nodeVersion,
97
+ changedTypeScriptFiles,
98
+ recentCommitSubjects,
99
+ runLintFix
100
+ })
101
+ })
102
+ ).pipe(
103
+ // Provide the `ChildProcessSpawner` dependency from `NodeServices.layer`.
104
+ Layer.provide(NodeServices.layer)
105
+ )
106
+ }
107
+
108
+ export const program = Effect.gen(function*() {
109
+ const tools = yield* DevTools
110
+
111
+ const version = yield* tools.nodeVersion
112
+ yield* Effect.log(`node=${version}`)
113
+ }).pipe(
114
+ // `ChildProcess` requires a platform implementation of
115
+ // `ChildProcessSpawner`. In Node.js, `NodeServices.layer` provides it.
116
+ Effect.provide(DevTools.layer)
117
+ )
@@ -0,0 +1,3 @@
1
+ ## Working with child processes
2
+
3
+ Use the `effect/process` modules to define child processes and run them with `ChildProcessSpawner`.
@@ -0,0 +1,162 @@
1
+ /**
2
+ * @title Getting started with Effect CLI modules
3
+ *
4
+ * Build a command-line app with typed arguments and flags, then wire subcommand
5
+ * handlers into a single executable command.
6
+ */
7
+ import { NodeRuntime, NodeServices } from "@effect/platform-node"
8
+ import { Console, Effect, Option, Schema } from "effect"
9
+ import { Argument, Command, Flag } from "effect/cli"
10
+
11
+ // You can define flags outside of commands and reuse them across multiple
12
+ // commands.
13
+ const workspace = Flag.String("workspace").pipe(
14
+ Flag.withAlias("w"),
15
+ Flag.withDescription("Workspace to operate on"),
16
+ Flag.withDefault("personal")
17
+ )
18
+
19
+ // Start with a root command and explicitly share the parent flags that should
20
+ // be available to all subcommands.
21
+ const tasks = Command.make("tasks").pipe(
22
+ Command.withSharedFlags({
23
+ workspace,
24
+ verbose: Flag.Boolean("verbose").pipe(
25
+ Flag.withAlias("v"),
26
+ Flag.withDescription("Print diagnostic output"),
27
+ Flag.withDefault(false)
28
+ )
29
+ }),
30
+ Command.withDescription("Track and manage tasks")
31
+ )
32
+
33
+ // Arguments and flags parse plain strings; use `withSchema` to validate or
34
+ // transform the parsed value with any schema.
35
+ const Email = Schema.String.pipe(
36
+ Schema.check(Schema.isPattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, {
37
+ message: "Expected a valid email address"
38
+ }))
39
+ )
40
+
41
+ const create = Command.make(
42
+ "create",
43
+ {
44
+ title: Argument.String("title").pipe(
45
+ Argument.withDescription("Task title"),
46
+ // Reject empty titles at parse time, so the handler only ever sees
47
+ // valid input
48
+ Argument.withSchema(Schema.NonEmptyString)
49
+ ),
50
+ priority: Flag.Literals("priority", ["low", "normal", "high"]).pipe(
51
+ Flag.withDescription("Priority for the new task"),
52
+ Flag.withDefault("normal")
53
+ ),
54
+ assignee: Flag.String("assignee").pipe(
55
+ Flag.withDescription("Email address of the person to assign"),
56
+ Flag.withSchema(Email),
57
+ Flag.optional
58
+ )
59
+ },
60
+ Effect.fn(function*({ assignee, priority, title }) {
61
+ // Subcommands can read parent command input by yielding the parent command.
62
+ const root = yield* tasks
63
+
64
+ if (root.verbose) {
65
+ yield* Console.log(`workspace=${root.workspace} action=create`)
66
+ }
67
+
68
+ yield* Console.log(`Created "${title}" in ${root.workspace} with ${priority} priority`)
69
+
70
+ if (Option.isSome(assignee)) {
71
+ yield* Console.log(`Assigned to ${assignee.value}`)
72
+ }
73
+ })
74
+ ).pipe(
75
+ Command.withDescription("Create a task"),
76
+ Command.withExamples([
77
+ {
78
+ command: "tasks create \"Ship 4.0\" --priority high",
79
+ description: "Create a high-priority task"
80
+ },
81
+ {
82
+ command: "tasks create \"Ship 4.0\" --assignee dev@acme.com",
83
+ description: "Create a task assigned to a team member"
84
+ }
85
+ ])
86
+ )
87
+
88
+ const list = Command.make(
89
+ "list",
90
+ {
91
+ status: Flag.Literals("status", ["open", "done", "all"]).pipe(
92
+ Flag.withDescription("Filter tasks by status"),
93
+ Flag.withDefault("open")
94
+ ),
95
+ json: Flag.Boolean("json").pipe(
96
+ Flag.withDescription("Print machine-readable output"),
97
+ Flag.withDefault(false)
98
+ )
99
+ },
100
+ Effect.fn(function*({ status, json }) {
101
+ const root = yield* tasks
102
+ const items = [
103
+ { title: "Ship 4.0", status: "open" },
104
+ { title: "Update onboarding guide", status: "done" }
105
+ ] as const
106
+ const filtered = status === "all"
107
+ ? items
108
+ : items.filter((item) => item.status === status)
109
+
110
+ if (root.verbose) {
111
+ yield* Console.log(`workspace=${root.workspace} action=list`)
112
+ }
113
+
114
+ if (json) {
115
+ yield* Console.log(JSON.stringify(
116
+ {
117
+ workspace: root.workspace,
118
+ status,
119
+ items: filtered
120
+ },
121
+ null,
122
+ 2
123
+ ))
124
+ return
125
+ }
126
+
127
+ yield* Console.log(`Listing ${status} tasks in ${root.workspace}`)
128
+ if (filtered.length === 0) {
129
+ yield* Console.log("- No tasks found")
130
+ return
131
+ }
132
+
133
+ for (const item of filtered) {
134
+ yield* Console.log(`- ${item.title}`)
135
+ }
136
+ })
137
+ ).pipe(
138
+ Command.withDescription("List tasks"),
139
+ Command.withAlias("ls"),
140
+ Command.withExamples([
141
+ {
142
+ command: "tasks --workspace team-a list --status open",
143
+ description: "List open tasks in a specific workspace"
144
+ },
145
+ {
146
+ command: "tasks --workspace team-b ls --status open",
147
+ description: "List open tasks in another workspace"
148
+ }
149
+ ])
150
+ )
151
+
152
+ // Finally, compose the subcommands into a single command and then run it.
153
+ tasks.pipe(
154
+ Command.withSubcommands([create, list]),
155
+ Command.run({
156
+ version: "1.0.0"
157
+ }),
158
+ // Provide the services for the platform you are targeting. In this case,
159
+ // Node.js
160
+ Effect.provide(NodeServices.layer),
161
+ NodeRuntime.runMain
162
+ )
@@ -0,0 +1,5 @@
1
+ ## Building CLI applications
2
+
3
+ Use the "effect/cli" modules to build CLI applications. These modules
4
+ provide utilities for parsing command-line arguments, handling user input, and
5
+ managing the flow of a CLI application.