@effect/platform-node-shared 4.0.0-beta.99 → 4.0.0-rc.109

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 (130) hide show
  1. package/AGENTS.md +395 -0
  2. package/CLAUDE.md +395 -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/40_sql/10_basics.ts +175 -0
  52. package/ai-docs/src/40_sql/index.md +6 -0
  53. package/ai-docs/src/50_http-client/10_basics.ts +102 -0
  54. package/ai-docs/src/50_http-client/index.md +3 -0
  55. package/ai-docs/src/51_http-server/10_basics.ts +118 -0
  56. package/ai-docs/src/51_http-server/20_testing.ts +100 -0
  57. package/ai-docs/src/51_http-server/fixtures/api/Api.ts +14 -0
  58. package/ai-docs/src/51_http-server/fixtures/api/Authorization.ts +36 -0
  59. package/ai-docs/src/51_http-server/fixtures/api/System.ts +10 -0
  60. package/ai-docs/src/51_http-server/fixtures/api/Users.ts +107 -0
  61. package/ai-docs/src/51_http-server/fixtures/domain/User.ts +21 -0
  62. package/ai-docs/src/51_http-server/fixtures/domain/UserErrors.ts +22 -0
  63. package/ai-docs/src/51_http-server/fixtures/server/Authorization.ts +37 -0
  64. package/ai-docs/src/51_http-server/fixtures/server/Users/http.ts +80 -0
  65. package/ai-docs/src/51_http-server/fixtures/server/Users.ts +183 -0
  66. package/ai-docs/src/51_http-server/index.md +3 -0
  67. package/ai-docs/src/60_child-process/10_working-with-child-processes.ts +117 -0
  68. package/ai-docs/src/60_child-process/index.md +3 -0
  69. package/ai-docs/src/70_cli/10_basics.ts +160 -0
  70. package/ai-docs/src/70_cli/index.md +5 -0
  71. package/ai-docs/src/71_ai/10_language-model.ts +156 -0
  72. package/ai-docs/src/71_ai/20_tools.ts +226 -0
  73. package/ai-docs/src/71_ai/30_chat.ts +158 -0
  74. package/ai-docs/src/71_ai/fixtures/domain/LaunchPlan.ts +9 -0
  75. package/ai-docs/src/71_ai/index.md +5 -0
  76. package/ai-docs/src/80_cluster/10_entities.ts +97 -0
  77. package/ai-docs/src/80_cluster/index.md +4 -0
  78. package/ai-docs/src/index.md +10 -0
  79. package/ai-docs/tsconfig.json +24 -0
  80. package/dist/NodeChildProcessSpawner.d.ts.map +1 -1
  81. package/dist/NodeChildProcessSpawner.js +14 -10
  82. package/dist/NodeChildProcessSpawner.js.map +1 -1
  83. package/dist/NodeClusterSocket.js.map +1 -1
  84. package/dist/NodeCrypto.js.map +1 -1
  85. package/dist/NodeFileSystem.d.ts.map +1 -1
  86. package/dist/NodeFileSystem.js +7 -7
  87. package/dist/NodeFileSystem.js.map +1 -1
  88. package/dist/NodeHttpCompression.d.ts +36 -0
  89. package/dist/NodeHttpCompression.d.ts.map +1 -0
  90. package/dist/NodeHttpCompression.js +146 -0
  91. package/dist/NodeHttpCompression.js.map +1 -0
  92. package/dist/NodePath.d.ts.map +1 -1
  93. package/dist/NodePath.js +23 -20
  94. package/dist/NodePath.js.map +1 -1
  95. package/dist/NodeRuntime.js.map +1 -1
  96. package/dist/NodeSink.js.map +1 -1
  97. package/dist/NodeSocket.js.map +1 -1
  98. package/dist/NodeSocketServer.d.ts.map +1 -1
  99. package/dist/NodeSocketServer.js +29 -19
  100. package/dist/NodeSocketServer.js.map +1 -1
  101. package/dist/NodeStdio.d.ts.map +1 -1
  102. package/dist/NodeStdio.js +2 -0
  103. package/dist/NodeStdio.js.map +1 -1
  104. package/dist/NodeStream.d.ts.map +1 -1
  105. package/dist/NodeStream.js +10 -7
  106. package/dist/NodeStream.js.map +1 -1
  107. package/dist/NodeTerminal.d.ts.map +1 -1
  108. package/dist/NodeTerminal.js +63 -11
  109. package/dist/NodeTerminal.js.map +1 -1
  110. package/dist/index.d.ts +4 -0
  111. package/dist/index.d.ts.map +1 -1
  112. package/dist/index.js +4 -0
  113. package/dist/index.js.map +1 -1
  114. package/dist/internal/nodeChildProcessSpawner.d.ts +4 -0
  115. package/dist/internal/nodeChildProcessSpawner.d.ts.map +1 -0
  116. package/dist/internal/nodeChildProcessSpawner.js +10 -0
  117. package/dist/internal/nodeChildProcessSpawner.js.map +1 -0
  118. package/dist/internal/utils.js.map +1 -1
  119. package/package.json +11 -9
  120. package/src/NodeChildProcessSpawner.ts +19 -12
  121. package/src/NodeFileSystem.ts +16 -12
  122. package/src/NodeHttpCompression.ts +153 -0
  123. package/src/NodePath.ts +25 -27
  124. package/src/NodeSocketServer.ts +32 -19
  125. package/src/NodeStdio.ts +2 -0
  126. package/src/NodeStream.ts +10 -7
  127. package/src/NodeTerminal.ts +62 -13
  128. package/src/index.ts +5 -0
  129. package/src/internal/nodeChildProcessSpawner.ts +16 -0
  130. package/src/internal/utils.ts +1 -1
@@ -0,0 +1,21 @@
1
+ import { Schema } from "effect"
2
+ import { Model } from "effect/unstable/schema"
3
+
4
+ export const UserId = Schema.String.pipe(Schema.brand("UserId"))
5
+ export type UserId = typeof UserId.Type
6
+
7
+ // `Model.Class` derives variants for the database (`User`, `User.insert`,
8
+ // `User.update`) and the JSON API (`User.json`, `User.jsonCreate`,
9
+ // `User.jsonUpdate`) from a single field declaration.
10
+ export class User extends Model.Class<User>("User")({
11
+ // A UUID v4 primary key generated by the application on insert. It is
12
+ // excluded from the `jsonCreate` / `jsonUpdate` variants, so API clients can
13
+ // never set it.
14
+ id: Model.UuidV4Insert(UserId),
15
+ name: Schema.String,
16
+ email: Schema.String,
17
+ // Timestamps are managed by the model: set on insert, refreshed on update,
18
+ // and also excluded from the JSON create / update variants.
19
+ createdAt: Model.DateTimeInsert,
20
+ updatedAt: Model.DateTimeUpdate
21
+ }) {}
@@ -0,0 +1,22 @@
1
+ import { Schema } from "effect"
2
+
3
+ export class UserNotFound extends Schema.TaggedError<UserNotFound>()(
4
+ "UserNotFound",
5
+ {},
6
+ // You can specify the status code for this error inline
7
+ { httpApiStatus: 404 }
8
+ ) {}
9
+
10
+ export class SearchQueryTooShort
11
+ extends Schema.TaggedError<SearchQueryTooShort>()("SearchQueryTooShort", {}, { httpApiStatus: 422 })
12
+ {
13
+ static readonly minimumLength = 2
14
+ }
15
+
16
+ // Create a wrapper error class for all errors in the Users API.
17
+ //
18
+ // This prevents adding too many error types to services / endpoint definitions.
19
+ //
20
+ export class UsersError extends Schema.TaggedError<UsersError>()("UsersError", {
21
+ reason: Schema.Union([UserNotFound, SearchQueryTooShort])
22
+ }) {}
@@ -0,0 +1,37 @@
1
+ import { DateTime, Effect, Layer, Redacted } from "effect"
2
+ import { Authorization, CurrentUser, Unauthorized } from "../api/Authorization.ts"
3
+ import { User, UserId } from "../domain/User.ts"
4
+
5
+ const fixedTimestamp = DateTime.makeUnsafe("2026-01-01T00:00:00Z")
6
+ const devUser = new User({
7
+ id: UserId.make("bf3dbe33-0ad2-4c9c-9c9e-733e57bdcbee"),
8
+ name: "Dev User",
9
+ email: "dev@acme.com",
10
+ createdAt: fixedTimestamp,
11
+ updatedAt: fixedTimestamp
12
+ })
13
+
14
+ // The implementation of the Authorization middleware. It is seperate from the
15
+ // service definition to avoid leaking it into a client.
16
+ export const AuthorizationLayer = Layer.effect(
17
+ Authorization,
18
+ Effect.gen(function*() {
19
+ // Here you could access services required by the middleware, like a
20
+ // database or an external auth provider.
21
+ yield* Effect.logInfo("Starting Authorization middleware")
22
+
23
+ return Authorization.of({
24
+ bearer: Effect.fn(function*(httpEffect, { credential }) {
25
+ // Validate the token and return an Unauthorized error if it's invalid.
26
+ const token = Redacted.value(credential)
27
+ if (token !== "dev-token") {
28
+ return yield* new Unauthorized({ message: "Missing or invalid bearer token" })
29
+ }
30
+
31
+ // Provide the current user to the rest of the stack. This will be
32
+ // available in any endpoint or middleware that runs after this one.
33
+ return yield* Effect.provideService(httpEffect, CurrentUser, devUser)
34
+ })
35
+ })
36
+ })
37
+ )
@@ -0,0 +1,80 @@
1
+ import { Effect, Layer } from "effect"
2
+ import { HttpApiBuilder, HttpApiError } from "effect/unstable/httpapi"
3
+ import { Api } from "../../api/Api.ts"
4
+ import { CurrentUser } from "../../api/Authorization.ts"
5
+ import { AuthorizationLayer } from "../Authorization.ts"
6
+ import { Users } from "../Users.ts"
7
+
8
+ // The handlers without their dependencies provided, so tests can supply an
9
+ // alternative `Users` implementation.
10
+ export const UsersApiHandlersNoDeps = HttpApiBuilder.group(
11
+ Api,
12
+ "users",
13
+ Effect.fn(function*(handlers) {
14
+ const users = yield* Users
15
+
16
+ return handlers.handleAll({
17
+ list: ({ query }) =>
18
+ users.list(query.search).pipe(
19
+ // The list endpoint expects no errors, so we convert any potential
20
+ // errors into a 500 Internal Server Error.
21
+ Effect.orDie
22
+ ),
23
+ search: Effect.fn(function*({ payload }) {
24
+ if (payload.search === "bad-request") {
25
+ // You can use the built in error types like any other
26
+ // Schema.TaggedError
27
+ return yield* new HttpApiError.RequestTimeout()
28
+ }
29
+ return yield* users.list(payload.search).pipe(
30
+ Effect.catchReason(
31
+ "UsersError",
32
+ "SearchQueryTooShort",
33
+ // Re-fail the "SearchQueryTooShort" reason
34
+ Effect.fail,
35
+ // All other reasons are unexpected, so we convert them into a 500
36
+ // Internal Server Error.
37
+ Effect.die
38
+ )
39
+ )
40
+ }),
41
+ getById: ({ params }) =>
42
+ users.getById(params.id).pipe(
43
+ // You can also use Effect.catchReasons to handle multiple error
44
+ // reasons at once
45
+ Effect.catchReasons("UsersError", {
46
+ UserNotFound: (e) => Effect.fail(e)
47
+ }, Effect.die)
48
+ ),
49
+ create: ({ payload }) =>
50
+ users.create(payload).pipe(
51
+ Effect.orDie
52
+ // You could alse use Effect.unwrapReason to moves rror reasons up to
53
+ // the top level, so you can handle them with Effect.catch or
54
+ // Effect.catchTag etc.
55
+ //
56
+ // Effect.unwrapReason("UsersError"),
57
+ // Effect.catchTags({
58
+ // UserNotFound: Effect.die,
59
+ // SearchQueryTooShort: Effect.die
60
+ // })
61
+ ),
62
+ update: ({ params, payload }) =>
63
+ users.update(params.id, payload).pipe(
64
+ Effect.catchReasons("UsersError", {
65
+ UserNotFound: (e) => Effect.fail(e)
66
+ }, Effect.die)
67
+ ),
68
+ me: () =>
69
+ // The Authorization middleware provides the CurrentUser service, so we
70
+ // can access it here.
71
+ CurrentUser
72
+ })
73
+ })
74
+ )
75
+
76
+ // The handlers with all dependencies provided, ready to serve. The SQL-backed
77
+ // `Users.layer` keeps the database wiring out of the server entrypoint.
78
+ export const UsersApiHandlers = UsersApiHandlersNoDeps.pipe(
79
+ Layer.provide([Users.layer, AuthorizationLayer])
80
+ )
@@ -0,0 +1,183 @@
1
+ import { SqliteClient, SqliteMigrator } from "@effect/sql-sqlite-node"
2
+ import { Context, Effect, Layer, Schema } from "effect"
3
+ import { SqlClient, SqlModel, SqlSchema } from "effect/unstable/sql"
4
+ import { User } from "../domain/User.ts"
5
+ import type { UserId } from "../domain/User.ts"
6
+ import { SearchQueryTooShort, UserNotFound, UsersError } from "../domain/UserErrors.ts"
7
+
8
+ // The SqlClient layer determines which database the SQL implementation talks
9
+ // to. Swap it for another driver package to target a different database.
10
+ const SqlLayer = SqliteClient.layer({ filename: ":memory:" })
11
+
12
+ // Migrations are effects keyed by `<id>_<name>` that run once, in id order. A
13
+ // real application would keep each migration in its own file and load them
14
+ // with `SqliteMigrator.fromFileSystem` instead of an inline record.
15
+ const MigratorLayer = SqliteMigrator.layer({
16
+ loader: SqliteMigrator.fromRecord({
17
+ "0001_create_users": Effect.gen(function*() {
18
+ const sql = yield* SqlClient.SqlClient
19
+ yield* sql`
20
+ CREATE TABLE users (
21
+ id TEXT PRIMARY KEY,
22
+ name TEXT NOT NULL,
23
+ email TEXT NOT NULL,
24
+ createdAt TEXT NOT NULL,
25
+ updatedAt TEXT NOT NULL
26
+ )
27
+ `
28
+ })
29
+ })
30
+ })
31
+
32
+ export class Users extends Context.Service<Users, {
33
+ list(search: string | undefined): Effect.Effect<Array<User>, UsersError>
34
+ getById(id: UserId): Effect.Effect<User, UsersError>
35
+ create(input: typeof User.jsonCreate.Type): Effect.Effect<User, UsersError>
36
+ update(id: UserId, input: typeof User.jsonUpdate.Type): Effect.Effect<User, UsersError>
37
+ }>()("acme/Users") {
38
+ // The SQL implementation only requires a `SqlClient`, so entrypoints and
39
+ // tests decide how the database is provided.
40
+ static readonly layerNoDeps = Layer.effect(
41
+ Users,
42
+ Effect.gen(function*() {
43
+ const sql = yield* SqlClient.SqlClient
44
+
45
+ // CRUD goes through a repository derived from the `User` model. Each
46
+ // operation uses the matching model variant to encode its input and
47
+ // decodes rows with the full model schema.
48
+ const repo = yield* SqlModel.makeRepository(User, {
49
+ tableName: "users",
50
+ spanPrefix: "Users",
51
+ idColumn: "id"
52
+ })
53
+
54
+ // Queries the repository does not cover are written with the `sql` tag
55
+ // and decoded with the model schema.
56
+ const listAll = SqlSchema.findAll({
57
+ Request: Schema.Void,
58
+ Result: User,
59
+ execute: () => sql`SELECT * FROM users ORDER BY createdAt`
60
+ })
61
+
62
+ const searchUsers = SqlSchema.findAll({
63
+ Request: Schema.String,
64
+ Result: User,
65
+ execute: (search) => {
66
+ const pattern = `%${search}%`
67
+ return sql`SELECT * FROM users WHERE name LIKE ${pattern} OR email LIKE ${pattern}`
68
+ }
69
+ })
70
+
71
+ const list = Effect.fn("Users.list")(function*(search: string | undefined) {
72
+ if (search === undefined || search.length === 0) {
73
+ return yield* Effect.orDie(listAll())
74
+ } else if (search.length < SearchQueryTooShort.minimumLength) {
75
+ return yield* new UsersError({
76
+ reason: new SearchQueryTooShort()
77
+ })
78
+ }
79
+ yield* Effect.annotateCurrentSpan({ search })
80
+ return yield* Effect.orDie(searchUsers(search))
81
+ })
82
+
83
+ const getById = Effect.fn("Users.getById")((id: UserId) =>
84
+ repo.findById(id).pipe(
85
+ Effect.catchTags({
86
+ NoSuchElementError: () => new UsersError({ reason: new UserNotFound() }),
87
+ // Database and encoding failures are unexpected, so treat them as
88
+ // defects to keep the service interface focused on domain errors.
89
+ SchemaError: Effect.die,
90
+ SqlError: Effect.die
91
+ })
92
+ )
93
+ )
94
+
95
+ const create = Effect.fn("Users.create")((input: typeof User.jsonCreate.Type) =>
96
+ // `User.insert.makeEffect` fills in the generated id and timestamps
97
+ // using the Effect clock, so tests can control them with `TestClock`.
98
+ User.insert.makeEffect(input).pipe(
99
+ Effect.flatMap(repo.insert),
100
+ Effect.orDie
101
+ )
102
+ )
103
+
104
+ const update = Effect.fn("Users.update")(function*(id: UserId, input: typeof User.jsonUpdate.Type) {
105
+ // Ensure the user exists first, so a missing id fails with the domain
106
+ // error instead of a defect.
107
+ yield* getById(id)
108
+ const update = yield* User.update.makeEffect({ id, ...input }).pipe(Effect.orDie)
109
+ return yield* repo.update(update).pipe(Effect.orDie)
110
+ })
111
+
112
+ return Users.of({ list, getById, create, update })
113
+ })
114
+ )
115
+
116
+ // The fully provided SQL implementation: the database client and migrations
117
+ // are implementation details, so this layer requires nothing.
118
+ static readonly layer: Layer.Layer<Users> = this.layerNoDeps.pipe(
119
+ Layer.provide(MigratorLayer.pipe(Layer.provideMerge(SqlLayer))),
120
+ Layer.orDie
121
+ )
122
+
123
+ // An in-memory implementation for tests, so the HTTP stack can be exercised
124
+ // without a database.
125
+ static readonly layerMemory = Layer.effect(
126
+ Users,
127
+ Effect.gen(function*() {
128
+ const users = new Map<UserId, User>()
129
+
130
+ const makeUser = (input: typeof User.jsonCreate.Type) =>
131
+ User.insert.makeEffect(input).pipe(
132
+ Effect.map((user) => new User(user)),
133
+ Effect.orDie
134
+ )
135
+
136
+ const admin = yield* makeUser({ name: "Admin", email: "admin@acme.dev" })
137
+ users.set(admin.id, admin)
138
+
139
+ const list = Effect.fn("Users.list")(function*(search: string | undefined) {
140
+ const allUsers = Array.from(users.values())
141
+ if (search === undefined || search.length === 0) {
142
+ return allUsers
143
+ } else if (search.length < SearchQueryTooShort.minimumLength) {
144
+ return yield* new UsersError({
145
+ reason: new SearchQueryTooShort()
146
+ })
147
+ }
148
+ yield* Effect.annotateCurrentSpan({ search })
149
+ const normalized = search.toLowerCase()
150
+ return allUsers.filter((user) =>
151
+ user.name.toLowerCase().includes(normalized) || user.email.toLowerCase().includes(normalized)
152
+ )
153
+ })
154
+
155
+ const getById = Effect.fn("Users.getById")(function*(id: UserId) {
156
+ yield* Effect.annotateCurrentSpan({ id })
157
+ const user = users.get(id)
158
+ if (user === undefined) {
159
+ return yield* new UsersError({
160
+ reason: new UserNotFound()
161
+ })
162
+ }
163
+ return user
164
+ })
165
+
166
+ const create = Effect.fn("Users.create")(function*(input: typeof User.jsonCreate.Type) {
167
+ const user = yield* makeUser(input)
168
+ users.set(user.id, user)
169
+ return user
170
+ })
171
+
172
+ const update = Effect.fn("Users.update")(function*(id: UserId, input: typeof User.jsonUpdate.Type) {
173
+ const existing = yield* getById(id)
174
+ const update = yield* User.update.makeEffect({ id, ...input }).pipe(Effect.orDie)
175
+ const updated = new User({ ...existing, ...update })
176
+ users.set(id, updated)
177
+ return updated
178
+ })
179
+
180
+ return Users.of({ list, getById, create, update })
181
+ })
182
+ )
183
+ }
@@ -0,0 +1,3 @@
1
+ ## Building HttpApi servers
2
+
3
+ `HttpApi` gives you schema-first, type-safe HTTP APIs with runtime validation, typed clients, and OpenAPI docs from one definition.
@@ -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/unstable/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/unstable/process` modules to define child processes and run them with `ChildProcessSpawner`.
@@ -0,0 +1,160 @@
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/unstable/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
+ )
28
+ }),
29
+ Command.withDescription("Track and manage tasks")
30
+ )
31
+
32
+ // Arguments and flags parse plain strings; use `withSchema` to validate or
33
+ // transform the parsed value with any schema.
34
+ const Email = Schema.String.pipe(
35
+ Schema.check(Schema.isPattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, {
36
+ message: "Expected a valid email address"
37
+ }))
38
+ )
39
+
40
+ const create = Command.make(
41
+ "create",
42
+ {
43
+ title: Argument.string("title").pipe(
44
+ Argument.withDescription("Task title"),
45
+ // Reject empty titles at parse time, so the handler only ever sees
46
+ // valid input
47
+ Argument.withSchema(Schema.NonEmptyString)
48
+ ),
49
+ priority: Flag.choice("priority", ["low", "normal", "high"]).pipe(
50
+ Flag.withDescription("Priority for the new task"),
51
+ Flag.withDefault("normal")
52
+ ),
53
+ assignee: Flag.string("assignee").pipe(
54
+ Flag.withDescription("Email address of the person to assign"),
55
+ Flag.withSchema(Email),
56
+ Flag.optional
57
+ )
58
+ },
59
+ Effect.fn(function*({ assignee, priority, title }) {
60
+ // Subcommands can read parent command input by yielding the parent command.
61
+ const root = yield* tasks
62
+
63
+ if (root.verbose) {
64
+ yield* Console.log(`workspace=${root.workspace} action=create`)
65
+ }
66
+
67
+ yield* Console.log(`Created "${title}" in ${root.workspace} with ${priority} priority`)
68
+
69
+ if (Option.isSome(assignee)) {
70
+ yield* Console.log(`Assigned to ${assignee.value}`)
71
+ }
72
+ })
73
+ ).pipe(
74
+ Command.withDescription("Create a task"),
75
+ Command.withExamples([
76
+ {
77
+ command: "tasks create \"Ship 4.0\" --priority high",
78
+ description: "Create a high-priority task"
79
+ },
80
+ {
81
+ command: "tasks create \"Ship 4.0\" --assignee dev@acme.com",
82
+ description: "Create a task assigned to a team member"
83
+ }
84
+ ])
85
+ )
86
+
87
+ const list = Command.make(
88
+ "list",
89
+ {
90
+ status: Flag.choice("status", ["open", "done", "all"]).pipe(
91
+ Flag.withDescription("Filter tasks by status"),
92
+ Flag.withDefault("open")
93
+ ),
94
+ json: Flag.boolean("json").pipe(
95
+ Flag.withDescription("Print machine-readable output")
96
+ )
97
+ },
98
+ Effect.fn(function*({ status, json }) {
99
+ const root = yield* tasks
100
+ const items = [
101
+ { title: "Ship 4.0", status: "open" },
102
+ { title: "Update onboarding guide", status: "done" }
103
+ ] as const
104
+ const filtered = status === "all"
105
+ ? items
106
+ : items.filter((item) => item.status === status)
107
+
108
+ if (root.verbose) {
109
+ yield* Console.log(`workspace=${root.workspace} action=list`)
110
+ }
111
+
112
+ if (json) {
113
+ yield* Console.log(JSON.stringify(
114
+ {
115
+ workspace: root.workspace,
116
+ status,
117
+ items: filtered
118
+ },
119
+ null,
120
+ 2
121
+ ))
122
+ return
123
+ }
124
+
125
+ yield* Console.log(`Listing ${status} tasks in ${root.workspace}`)
126
+ if (filtered.length === 0) {
127
+ yield* Console.log("- No tasks found")
128
+ return
129
+ }
130
+
131
+ for (const item of filtered) {
132
+ yield* Console.log(`- ${item.title}`)
133
+ }
134
+ })
135
+ ).pipe(
136
+ Command.withDescription("List tasks"),
137
+ Command.withAlias("ls"),
138
+ Command.withExamples([
139
+ {
140
+ command: "tasks --workspace team-a list --status open",
141
+ description: "List open tasks in a specific workspace"
142
+ },
143
+ {
144
+ command: "tasks --workspace team-b ls --status open",
145
+ description: "List open tasks in another workspace"
146
+ }
147
+ ])
148
+ )
149
+
150
+ // Finally, compose the subcommands into a single command and then run it.
151
+ tasks.pipe(
152
+ Command.withSubcommands([create, list]),
153
+ Command.run({
154
+ version: "1.0.0"
155
+ }),
156
+ // Provide the services for the platform you are targeting. In this case,
157
+ // Node.js
158
+ Effect.provide(NodeServices.layer),
159
+ NodeRuntime.runMain
160
+ )
@@ -0,0 +1,5 @@
1
+ ## Building CLI applications
2
+
3
+ Use the "effect/unstable/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.