@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
@@ -52,12 +52,14 @@ export const make = Effect.fnUntraced(function*(
52
52
  options: Net.ServerOpts & Net.ListenOptions
53
53
  ) {
54
54
  const errorDeferred = Deferred.makeUnsafe<never, Error>()
55
- const pending = new Set<Net.Socket>()
55
+ const pending = new Map<Net.Socket, () => void>()
56
56
  function defaultOnConnection(conn: Net.Socket) {
57
- pending.add(conn)
58
57
  const remove = () => {
59
58
  pending.delete(conn)
59
+ conn.off("close", remove)
60
+ conn.off("error", remove)
60
61
  }
62
+ pending.set(conn, remove)
61
63
  conn.on("close", remove)
62
64
  conn.on("error", remove)
63
65
  }
@@ -66,6 +68,10 @@ export const make = Effect.fnUntraced(function*(
66
68
  let server: Net.Server | undefined
67
69
  yield* Effect.addFinalizer(() =>
68
70
  Effect.callback<void>((resume) => {
71
+ pending.forEach((remove, conn) => {
72
+ remove()
73
+ conn.destroy()
74
+ })
69
75
  server?.close(() => resume(Effect.void))
70
76
  })
71
77
  )
@@ -129,12 +135,10 @@ export const make = Effect.fnUntraced(function*(
129
135
  trackFiber
130
136
  )
131
137
  }
132
- pending.forEach((conn) => {
133
- conn.removeAllListeners("error")
134
- conn.removeAllListeners("close")
138
+ pending.forEach((remove, conn) => {
139
+ remove()
135
140
  onConnection(conn)
136
141
  })
137
- pending.clear()
138
142
  return yield* Effect.callback<never>((_resume) => {
139
143
  return Effect.suspend(() => {
140
144
  onConnection = prevOnConnection
@@ -190,20 +194,29 @@ export const makeWebSocket: (
190
194
  > = Effect.fnUntraced(function*(
191
195
  options: NodeWS.ServerOptions
192
196
  ) {
197
+ const pendingConnections = new Map<
198
+ globalThis.WebSocket,
199
+ readonly [request: Http.IncomingMessage, remove: () => void]
200
+ >()
193
201
  const server = yield* Effect.acquireRelease(
194
202
  Effect.sync(() => new NodeWS.WebSocketServer(options)),
195
203
  (server) =>
196
204
  Effect.callback<void>((resume) => {
205
+ pendingConnections.forEach(([, remove], conn) => {
206
+ remove()
207
+ const socket = conn as unknown as NodeWS.WebSocket
208
+ socket.terminate()
209
+ })
197
210
  server.close(() => resume(Effect.void))
198
211
  })
199
212
  )
200
- const pendingConnections = new Set<readonly [globalThis.WebSocket, Http.IncomingMessage]>()
201
213
  function defaultHandler(conn: globalThis.WebSocket, req: Http.IncomingMessage) {
202
- const entry = [conn, req] as const
203
- pendingConnections.add(entry)
204
- conn.addEventListener("close", () => {
205
- pendingConnections.delete(entry)
206
- })
214
+ const remove = () => {
215
+ pendingConnections.delete(conn)
216
+ conn.removeEventListener("close", remove)
217
+ }
218
+ pendingConnections.set(conn, [req, remove])
219
+ conn.addEventListener("close", remove)
207
220
  }
208
221
  let onConnection = defaultHandler
209
222
  server.on("connection", (conn, req) => onConnection(conn as any, req))
@@ -229,9 +242,9 @@ export const makeWebSocket: (
229
242
  const trackFiber = Fiber.runIn(scope)
230
243
  const prevOnConnection = onConnection
231
244
  onConnection = function(conn: globalThis.WebSocket, req: Http.IncomingMessage) {
232
- const map = new Map(services.mapUnsafe)
233
- map.set(IncomingMessage.key, req)
234
- map.set(Socket.WebSocket.key, conn as any)
245
+ let context = services
246
+ context = Context.add(context, IncomingMessage, req)
247
+ context = Context.add(context, Socket.WebSocket, conn as any)
235
248
  pipe(
236
249
  Socket.fromWebSocket(
237
250
  Effect.acquireRelease(
@@ -244,14 +257,14 @@ export const makeWebSocket: (
244
257
  ),
245
258
  Effect.flatMap(handler),
246
259
  Effect.catchCause(reportUnhandledError),
247
- Effect.runForkWith(Context.makeUnsafe(map)),
260
+ Effect.runForkWith(context),
248
261
  trackFiber
249
262
  )
250
263
  }
251
- for (const [conn, req] of pendingConnections) {
264
+ pendingConnections.forEach(([req, remove], conn) => {
265
+ remove()
252
266
  onConnection(conn, req)
253
- }
254
- pendingConnections.clear()
267
+ })
255
268
  return yield* Effect.callback<never>((_resume) => {
256
269
  return Effect.sync(() => {
257
270
  onConnection = prevOnConnection
package/src/NodeStdio.ts CHANGED
@@ -28,6 +28,8 @@ export const layer: Layer.Layer<Stdio.Stdio> = Layer.succeed(
28
28
  Stdio.Stdio,
29
29
  Stdio.make({
30
30
  args: Effect.sync(() => process.argv.slice(2)),
31
+ stdinIsTerminal: Effect.sync(() => process.stdin.isTTY === true),
32
+ stdoutIsTerminal: Effect.sync(() => process.stdout.isTTY === true),
31
33
  stdout: (options) =>
32
34
  fromWritable({
33
35
  evaluate: () => process.stdout,
package/src/NodeStream.ts CHANGED
@@ -245,7 +245,7 @@ export const toString = <E = Cause.UnknownError>(
245
245
  readonly maxBytes?: SizeInput | undefined
246
246
  }
247
247
  ): Effect.Effect<string, E> => {
248
- const maxBytesNumber = options?.maxBytes ? Number(options.maxBytes) : undefined
248
+ const maxBytesNumber = options?.maxBytes !== undefined ? Number(options.maxBytes) : undefined
249
249
  const onError = options?.onError ?? defaultOnError
250
250
  const encoding = options?.encoding ?? "utf8"
251
251
  return Effect.callback((resume) => {
@@ -258,9 +258,6 @@ export const toString = <E = Cause.UnknownError>(
258
258
  }
259
259
  resume(Effect.fail(onError(err) as E))
260
260
  })
261
- stream.once("error", (err) => {
262
- resume(Effect.fail(onError(err) as E))
263
- })
264
261
 
265
262
  let string = ""
266
263
  let bytes = 0
@@ -270,7 +267,10 @@ export const toString = <E = Cause.UnknownError>(
270
267
  stream.on("data", (chunk) => {
271
268
  string += chunk
272
269
  bytes += Buffer.byteLength(chunk)
273
- if (maxBytesNumber && bytes > maxBytesNumber) {
270
+ if (maxBytesNumber !== undefined && bytes > maxBytesNumber) {
271
+ if ("closed" in stream && !stream.closed) {
272
+ stream.destroy()
273
+ }
274
274
  resume(Effect.fail(onError(new Error("maxBytes exceeded")) as E))
275
275
  }
276
276
  })
@@ -297,7 +297,7 @@ export const toArrayBuffer = <E = Cause.UnknownError>(
297
297
  readonly maxBytes?: SizeInput | undefined
298
298
  }
299
299
  ): Effect.Effect<ArrayBuffer, E> => {
300
- const maxBytesNumber = options?.maxBytes ? Number(options.maxBytes) : undefined
300
+ const maxBytesNumber = options?.maxBytes !== undefined ? Number(options.maxBytes) : undefined
301
301
  const onError = options?.onError ?? defaultOnError
302
302
  return Effect.callback((resume) => {
303
303
  const stream = readable() as Readable
@@ -321,7 +321,10 @@ export const toArrayBuffer = <E = Cause.UnknownError>(
321
321
  stream.on("data", (chunk) => {
322
322
  buffers.push(chunk)
323
323
  bytes += chunk.length
324
- if (maxBytesNumber && bytes > maxBytesNumber) {
324
+ if (maxBytesNumber !== undefined && bytes > maxBytesNumber) {
325
+ if ("closed" in stream && !stream.closed) {
326
+ stream.destroy()
327
+ }
325
328
  resume(Effect.fail(onError(new Error("maxBytes exceeded")) as E))
326
329
  }
327
330
  })
@@ -35,34 +35,61 @@ export const make: (
35
35
  function*(shouldQuit: (input: Terminal.UserInput) => boolean = defaultShouldQuit) {
36
36
  const stdin = process.stdin
37
37
  const stdout = process.stdout
38
+ const lines = yield* Queue.make<string, Cause.Done>()
39
+
40
+ // stdin "end" fires once per process, so remember end-of-input for readers
41
+ // created after the event (Bun never sets `readableEnded`).
42
+ let inputEnded = stdin.readableEnded
43
+ let readlineActive = false
44
+ const onStdinEnd = () => {
45
+ inputEnded = true
46
+ if (!readlineActive) {
47
+ Queue.endUnsafe(lines)
48
+ }
49
+ }
50
+ stdin.once("end", onStdinEnd)
51
+ yield* Effect.addFinalizer(() => Effect.sync(() => stdin.off("end", onStdinEnd)))
38
52
 
39
- // Acquire readline interface with TTY setup/cleanup inside the scope
40
53
  const rlRef = yield* RcRef.make({
41
54
  acquire: Effect.acquireRelease(
42
55
  Effect.sync(() => {
43
56
  const rl = readline.createInterface({ input: stdin, escapeCodeTimeout: 50 })
57
+ const onLine = (line: string) => Queue.offerUnsafe(lines, line)
58
+ const onClose = () => {
59
+ readlineActive = false
60
+ Queue.endUnsafe(lines)
61
+ }
62
+ readlineActive = true
44
63
  readline.emitKeypressEvents(stdin, rl)
64
+ rl.on("line", onLine)
65
+ rl.once("close", onClose)
45
66
 
46
67
  if (stdin.isTTY) {
47
68
  stdin.setRawMode(true)
48
69
  }
49
- return rl
70
+ return { rl, onClose, onLine }
50
71
  }),
51
- (rl) =>
72
+ ({ rl, onClose, onLine }) =>
52
73
  Effect.sync(() => {
74
+ readlineActive = false
75
+ rl.off("line", onLine)
76
+ rl.off("close", onClose)
53
77
  if (stdin.isTTY) {
54
78
  stdin.setRawMode(false)
55
79
  }
56
80
  rl.close()
81
+ if (inputEnded) {
82
+ Queue.endUnsafe(lines)
83
+ }
57
84
  })
58
- )
85
+ ),
86
+ idleTimeToLive: "10 millis"
59
87
  })
60
88
 
61
89
  const columns = Effect.sync(() => stdout.columns ?? 0)
62
90
  const rows = Effect.sync(() => stdout.rows ?? 0)
63
91
 
64
92
  const readInput = Effect.gen(function*() {
65
- yield* RcRef.get(rlRef)
66
93
  const queue = yield* Queue.make<Terminal.UserInput, Cause.Done>()
67
94
  const handleKeypress = (s: string | undefined, k: readline.Key) => {
68
95
  const userInput = {
@@ -74,18 +101,40 @@ export const make: (
74
101
  Queue.endUnsafe(queue)
75
102
  }
76
103
  }
77
- yield* Effect.addFinalizer(() => Effect.sync(() => stdin.off("keypress", handleKeypress)))
104
+ // Deno's `process.stdin` shim does not keep the event loop alive, so a
105
+ // program blocked on input can exit before `end` is ever delivered. A
106
+ // timer holds the loop open for as long as this reader is active.
107
+ const keepAlive = setInterval(() => {}, 2147483647)
108
+ // Without this, consumers (e.g. `Prompt.run`) hang forever on closed stdin.
109
+ const handleEnd = () => {
110
+ clearInterval(keepAlive)
111
+ Queue.endUnsafe(queue)
112
+ }
113
+ yield* Effect.addFinalizer(() =>
114
+ Effect.sync(() => {
115
+ clearInterval(keepAlive)
116
+ stdin.off("keypress", handleKeypress)
117
+ stdin.off("end", handleEnd)
118
+ })
119
+ )
78
120
  stdin.on("keypress", handleKeypress)
121
+ if (inputEnded) {
122
+ handleEnd()
123
+ } else {
124
+ yield* RcRef.get(rlRef)
125
+ stdin.once("end", handleEnd)
126
+ }
79
127
  return queue as Queue.Dequeue<Terminal.UserInput, Cause.Done>
80
128
  })
81
129
 
82
- const readLine = Effect.scoped(
83
- Effect.flatMap(RcRef.get(rlRef), (readlineInterface) =>
84
- Effect.callback<string, Terminal.QuitError>((resume) => {
85
- const onLine = (line: string) => resume(Effect.succeed(line))
86
- readlineInterface.once("line", onLine)
87
- return Effect.sync(() => readlineInterface.off("line", onLine))
88
- }))
130
+ const readLine = Effect.suspend(() =>
131
+ Queue.poll(lines).pipe(
132
+ Effect.flatMap(Option.match({
133
+ onNone: () => Effect.scoped(Effect.andThen(RcRef.get(rlRef), Queue.take(lines))),
134
+ onSome: Effect.succeed
135
+ })),
136
+ Effect.mapError(() => new Terminal.QuitError({}))
137
+ )
89
138
  )
90
139
 
91
140
  const display = (prompt: string) =>
package/src/index.ts CHANGED
@@ -24,6 +24,11 @@ export * as NodeCrypto from "./NodeCrypto.ts"
24
24
  */
25
25
  export * as NodeFileSystem from "./NodeFileSystem.ts"
26
26
 
27
+ /**
28
+ * @since 4.0.0
29
+ */
30
+ export * as NodeHttpCompression from "./NodeHttpCompression.ts"
31
+
27
32
  /**
28
33
  * @since 4.0.0
29
34
  */
@@ -0,0 +1,16 @@
1
+ import type * as ChildProcess from "effect/unstable/process/ChildProcess"
2
+ import type * as NodeChildProcess from "node:child_process"
3
+
4
+ export const buildSpawnOptions = (
5
+ options: ChildProcess.CommandOptions,
6
+ base: Pick<NodeChildProcess.SpawnOptions, "cwd" | "env" | "stdio">,
7
+ platform: NodeJS.Platform
8
+ ): NodeChildProcess.SpawnOptions => {
9
+ const detached = options.detached ?? platform !== "win32"
10
+ return {
11
+ ...base,
12
+ detached,
13
+ shell: options.shell,
14
+ windowsHide: options.windowsHide ?? !detached
15
+ }
16
+ }
@@ -6,7 +6,7 @@ import type { PathLike } from "node:fs"
6
6
  export const handleErrnoException = (module: SystemError["module"], method: string) =>
7
7
  (
8
8
  err: NodeJS.ErrnoException,
9
- [path]: [path: PathLike | number | string | readonly string[], ...args: Array<any>]
9
+ [path]: [path: PathLike | number | string | ReadonlyArray<string>, ...args: Array<any>]
10
10
  ): PlatformError.PlatformError => {
11
11
  let reason: SystemErrorTag = "Unknown"
12
12