effect 4.0.2 → 4.0.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 (243) hide show
  1. package/dist/Context.js +3 -3
  2. package/dist/Context.js.map +1 -1
  3. package/dist/Crypto.d.ts +33 -5
  4. package/dist/Crypto.d.ts.map +1 -1
  5. package/dist/Crypto.js +8 -2
  6. package/dist/Crypto.js.map +1 -1
  7. package/dist/DateTime.d.ts +9 -6
  8. package/dist/DateTime.d.ts.map +1 -1
  9. package/dist/DateTime.js +9 -6
  10. package/dist/DateTime.js.map +1 -1
  11. package/dist/Duration.d.ts +6 -6
  12. package/dist/Duration.d.ts.map +1 -1
  13. package/dist/Duration.js +27 -8
  14. package/dist/Duration.js.map +1 -1
  15. package/dist/FileSystem.d.ts +44 -2
  16. package/dist/FileSystem.d.ts.map +1 -1
  17. package/dist/FileSystem.js.map +1 -1
  18. package/dist/Schema.d.ts +473 -1
  19. package/dist/Schema.d.ts.map +1 -1
  20. package/dist/Schema.js +355 -4
  21. package/dist/Schema.js.map +1 -1
  22. package/dist/Stdio.d.ts +7 -1
  23. package/dist/Stdio.d.ts.map +1 -1
  24. package/dist/Stdio.js +1 -0
  25. package/dist/Stdio.js.map +1 -1
  26. package/dist/Stream.d.ts +11 -11
  27. package/dist/Stream.js +5 -5
  28. package/dist/ai/McpServer.d.ts +6 -0
  29. package/dist/ai/McpServer.d.ts.map +1 -1
  30. package/dist/ai/McpServer.js +65 -23
  31. package/dist/ai/McpServer.js.map +1 -1
  32. package/dist/ai/internal/mcpProtocol/v2026_07_28.js +5 -0
  33. package/dist/ai/internal/mcpProtocol/v2026_07_28.js.map +1 -1
  34. package/dist/ai/internal/mcpProtocol.js.map +1 -1
  35. package/dist/ai/internal/mcpRuntime.js +1 -0
  36. package/dist/ai/internal/mcpRuntime.js.map +1 -1
  37. package/dist/cli/CliOutput.d.ts +27 -0
  38. package/dist/cli/CliOutput.d.ts.map +1 -1
  39. package/dist/cli/CliOutput.js +33 -10
  40. package/dist/cli/CliOutput.js.map +1 -1
  41. package/dist/cli/Command.d.ts.map +1 -1
  42. package/dist/cli/Command.js +6 -3
  43. package/dist/cli/Command.js.map +1 -1
  44. package/dist/cli/Completions.d.ts +3 -3
  45. package/dist/cli/Completions.d.ts.map +1 -1
  46. package/dist/cli/Completions.js +8 -5
  47. package/dist/cli/Completions.js.map +1 -1
  48. package/dist/cli/GlobalFlag.d.ts +5 -4
  49. package/dist/cli/GlobalFlag.d.ts.map +1 -1
  50. package/dist/cli/GlobalFlag.js +3 -3
  51. package/dist/cli/GlobalFlag.js.map +1 -1
  52. package/dist/cli/Prompt.d.ts +17 -1
  53. package/dist/cli/Prompt.d.ts.map +1 -1
  54. package/dist/cli/Prompt.js +119 -114
  55. package/dist/cli/Prompt.js.map +1 -1
  56. package/dist/cli/internal/ansi.d.ts +0 -1
  57. package/dist/cli/internal/ansi.d.ts.map +1 -1
  58. package/dist/cli/internal/ansi.js +20 -3
  59. package/dist/cli/internal/ansi.js.map +1 -1
  60. package/dist/cli/internal/completions/powershell.d.ts +2 -0
  61. package/dist/cli/internal/completions/powershell.d.ts.map +1 -0
  62. package/dist/cli/internal/completions/powershell.js +273 -0
  63. package/dist/cli/internal/completions/powershell.js.map +1 -0
  64. package/dist/cli/internal/completions/zsh.js +3 -2
  65. package/dist/cli/internal/completions/zsh.js.map +1 -1
  66. package/dist/cli/internal/wizard.d.ts +3 -3
  67. package/dist/cli/internal/wizard.d.ts.map +1 -1
  68. package/dist/cli/internal/wizard.js +21 -18
  69. package/dist/cli/internal/wizard.js.map +1 -1
  70. package/dist/cluster/ClusterSchema.d.ts +28 -0
  71. package/dist/cluster/ClusterSchema.d.ts.map +1 -1
  72. package/dist/cluster/ClusterSchema.js +30 -0
  73. package/dist/cluster/ClusterSchema.js.map +1 -1
  74. package/dist/cluster/ClusterWorkflowEngine.d.ts.map +1 -1
  75. package/dist/cluster/ClusterWorkflowEngine.js +10 -18
  76. package/dist/cluster/ClusterWorkflowEngine.js.map +1 -1
  77. package/dist/cluster/MessageStorage.d.ts +18 -3
  78. package/dist/cluster/MessageStorage.d.ts.map +1 -1
  79. package/dist/cluster/MessageStorage.js +31 -11
  80. package/dist/cluster/MessageStorage.js.map +1 -1
  81. package/dist/cluster/Runners.d.ts.map +1 -1
  82. package/dist/cluster/Runners.js +8 -2
  83. package/dist/cluster/Runners.js.map +1 -1
  84. package/dist/cluster/Sharding.d.ts.map +1 -1
  85. package/dist/cluster/Sharding.js +181 -62
  86. package/dist/cluster/Sharding.js.map +1 -1
  87. package/dist/cluster/SqlMessageStorage.d.ts +59 -19
  88. package/dist/cluster/SqlMessageStorage.d.ts.map +1 -1
  89. package/dist/cluster/SqlMessageStorage.js +84 -55
  90. package/dist/cluster/SqlMessageStorage.js.map +1 -1
  91. package/dist/cluster/SqlRunnerStorage.d.ts +68 -19
  92. package/dist/cluster/SqlRunnerStorage.d.ts.map +1 -1
  93. package/dist/cluster/SqlRunnerStorage.js +191 -113
  94. package/dist/cluster/SqlRunnerStorage.js.map +1 -1
  95. package/dist/cluster/internal/entityManager.js +104 -37
  96. package/dist/cluster/internal/entityManager.js.map +1 -1
  97. package/dist/http/FindMyWay/internal/router.js +3 -6
  98. package/dist/http/FindMyWay/internal/router.js.map +1 -1
  99. package/dist/http/HttpClient.d.ts +6 -0
  100. package/dist/http/HttpClient.d.ts.map +1 -1
  101. package/dist/http/HttpClient.js +39 -2
  102. package/dist/http/HttpClient.js.map +1 -1
  103. package/dist/http/HttpTraceContext.d.ts.map +1 -1
  104. package/dist/http/HttpTraceContext.js +4 -2
  105. package/dist/http/HttpTraceContext.js.map +1 -1
  106. package/dist/internal/effect.js +12 -7
  107. package/dist/internal/effect.js.map +1 -1
  108. package/dist/internal/version.js +1 -1
  109. package/dist/net/AddressResolver.d.ts +145 -0
  110. package/dist/net/AddressResolver.d.ts.map +1 -0
  111. package/dist/net/AddressResolver.js +114 -0
  112. package/dist/net/AddressResolver.js.map +1 -0
  113. package/dist/net/Dns.d.ts +596 -0
  114. package/dist/net/Dns.d.ts.map +1 -0
  115. package/dist/net/Dns.js +549 -0
  116. package/dist/net/Dns.js.map +1 -0
  117. package/dist/net/Host.d.ts +320 -0
  118. package/dist/net/Host.d.ts.map +1 -0
  119. package/dist/net/Host.js +342 -0
  120. package/dist/net/Host.js.map +1 -0
  121. package/dist/net/IpInterface.d.ts +76 -3
  122. package/dist/net/IpInterface.d.ts.map +1 -1
  123. package/dist/net/IpInterface.js +53 -4
  124. package/dist/net/IpInterface.js.map +1 -1
  125. package/dist/net/IpNetwork.d.ts +82 -8
  126. package/dist/net/IpNetwork.d.ts.map +1 -1
  127. package/dist/net/IpNetwork.js +60 -4
  128. package/dist/net/IpNetwork.js.map +1 -1
  129. package/dist/net/NetAddress.d.ts +388 -35
  130. package/dist/net/NetAddress.d.ts.map +1 -1
  131. package/dist/net/NetAddress.js +350 -34
  132. package/dist/net/NetAddress.js.map +1 -1
  133. package/dist/net/index.d.ts +15 -0
  134. package/dist/net/index.d.ts.map +1 -1
  135. package/dist/net/index.js +15 -0
  136. package/dist/net/index.js.map +1 -1
  137. package/dist/observability/OtlpMetrics.d.ts.map +1 -1
  138. package/dist/observability/OtlpMetrics.js +15 -21
  139. package/dist/observability/OtlpMetrics.js.map +1 -1
  140. package/dist/observability/PrometheusMetrics.js +1 -0
  141. package/dist/observability/PrometheusMetrics.js.map +1 -1
  142. package/dist/persistence/PersistedQueue.d.ts +32 -3
  143. package/dist/persistence/PersistedQueue.d.ts.map +1 -1
  144. package/dist/persistence/PersistedQueue.js +75 -16
  145. package/dist/persistence/PersistedQueue.js.map +1 -1
  146. package/dist/rpc/RpcClient.d.ts +22 -6
  147. package/dist/rpc/RpcClient.d.ts.map +1 -1
  148. package/dist/rpc/RpcClient.js +12 -5
  149. package/dist/rpc/RpcClient.js.map +1 -1
  150. package/dist/rpc/RpcServer.d.ts +6 -0
  151. package/dist/rpc/RpcServer.d.ts.map +1 -1
  152. package/dist/rpc/RpcServer.js +42 -20
  153. package/dist/rpc/RpcServer.js.map +1 -1
  154. package/dist/socket/DatagramSocket.d.ts +456 -0
  155. package/dist/socket/DatagramSocket.d.ts.map +1 -0
  156. package/dist/socket/DatagramSocket.js +535 -0
  157. package/dist/socket/DatagramSocket.js.map +1 -0
  158. package/dist/socket/Socket.d.ts +14 -1
  159. package/dist/socket/Socket.d.ts.map +1 -1
  160. package/dist/socket/Socket.js +9 -1
  161. package/dist/socket/Socket.js.map +1 -1
  162. package/dist/socket/index.d.ts +5 -0
  163. package/dist/socket/index.d.ts.map +1 -1
  164. package/dist/socket/index.js +5 -0
  165. package/dist/socket/index.js.map +1 -1
  166. package/dist/sql/Migrator.d.ts.map +1 -1
  167. package/dist/sql/Migrator.js +7 -2
  168. package/dist/sql/Migrator.js.map +1 -1
  169. package/dist/sql/SqlClient.d.ts +7 -1
  170. package/dist/sql/SqlClient.d.ts.map +1 -1
  171. package/dist/sql/SqlClient.js +31 -6
  172. package/dist/sql/SqlClient.js.map +1 -1
  173. package/dist/testing/TestCrypto.d.ts +73 -0
  174. package/dist/testing/TestCrypto.d.ts.map +1 -0
  175. package/dist/testing/TestCrypto.js +87 -0
  176. package/dist/testing/TestCrypto.js.map +1 -0
  177. package/dist/testing/index.d.ts +5 -0
  178. package/dist/testing/index.d.ts.map +1 -1
  179. package/dist/testing/index.js +5 -0
  180. package/dist/testing/index.js.map +1 -1
  181. package/dist/workflow/DurableDeferred.d.ts +54 -0
  182. package/dist/workflow/DurableDeferred.d.ts.map +1 -1
  183. package/dist/workflow/DurableDeferred.js +78 -49
  184. package/dist/workflow/DurableDeferred.js.map +1 -1
  185. package/dist/workflow/WorkflowEngine.d.ts.map +1 -1
  186. package/dist/workflow/WorkflowEngine.js +2 -1
  187. package/dist/workflow/WorkflowEngine.js.map +1 -1
  188. package/package.json +1 -1
  189. package/src/Context.ts +3 -3
  190. package/src/Crypto.ts +43 -9
  191. package/src/DateTime.ts +9 -6
  192. package/src/Duration.ts +36 -16
  193. package/src/FileSystem.ts +48 -2
  194. package/src/Schema.ts +669 -5
  195. package/src/Stdio.ts +9 -2
  196. package/src/Stream.ts +11 -11
  197. package/src/ai/McpServer.ts +63 -23
  198. package/src/ai/internal/mcpProtocol/v2026_07_28.ts +5 -0
  199. package/src/ai/internal/mcpProtocol.ts +3 -0
  200. package/src/ai/internal/mcpRuntime.ts +2 -0
  201. package/src/cli/CliOutput.ts +33 -16
  202. package/src/cli/Command.ts +4 -3
  203. package/src/cli/Completions.ts +9 -6
  204. package/src/cli/GlobalFlag.ts +19 -11
  205. package/src/cli/Prompt.ts +159 -149
  206. package/src/cli/internal/ansi.ts +26 -3
  207. package/src/cli/internal/completions/powershell.ts +322 -0
  208. package/src/cli/internal/completions/zsh.ts +4 -2
  209. package/src/cli/internal/wizard.ts +34 -26
  210. package/src/cluster/ClusterSchema.ts +32 -0
  211. package/src/cluster/ClusterWorkflowEngine.ts +13 -24
  212. package/src/cluster/MessageStorage.ts +45 -14
  213. package/src/cluster/Runners.ts +8 -2
  214. package/src/cluster/Sharding.ts +207 -49
  215. package/src/cluster/SqlMessageStorage.ts +118 -84
  216. package/src/cluster/SqlRunnerStorage.ts +224 -128
  217. package/src/cluster/internal/entityManager.ts +151 -55
  218. package/src/http/FindMyWay/internal/router.ts +4 -7
  219. package/src/http/HttpClient.ts +56 -2
  220. package/src/http/HttpTraceContext.ts +8 -2
  221. package/src/internal/effect.ts +15 -13
  222. package/src/internal/version.ts +1 -1
  223. package/src/net/AddressResolver.ts +222 -0
  224. package/src/net/Dns.ts +1035 -0
  225. package/src/net/Host.ts +472 -0
  226. package/src/net/IpInterface.ts +117 -7
  227. package/src/net/IpNetwork.ts +116 -17
  228. package/src/net/NetAddress.ts +611 -76
  229. package/src/net/index.ts +18 -0
  230. package/src/observability/OtlpMetrics.ts +12 -23
  231. package/src/observability/PrometheusMetrics.ts +1 -0
  232. package/src/persistence/PersistedQueue.ts +146 -53
  233. package/src/rpc/RpcClient.ts +41 -19
  234. package/src/rpc/RpcServer.ts +60 -25
  235. package/src/socket/DatagramSocket.ts +919 -0
  236. package/src/socket/Socket.ts +22 -2
  237. package/src/socket/index.ts +6 -0
  238. package/src/sql/Migrator.ts +21 -5
  239. package/src/sql/SqlClient.ts +39 -15
  240. package/src/testing/TestCrypto.ts +94 -0
  241. package/src/testing/index.ts +6 -0
  242. package/src/workflow/DurableDeferred.ts +123 -48
  243. package/src/workflow/WorkflowEngine.ts +2 -1
@@ -799,13 +799,18 @@ export interface WebSocketLike {
799
799
  }
800
800
 
801
801
  /**
802
- * Common options understood by a WebSocket client implementation.
802
+ * Client options for Node and Bun WebSocket implementations, allowing
803
+ * handshake headers and subprotocols to be supplied together.
803
804
  *
804
805
  * @stability unstable
805
806
  * @category models
806
807
  * @since 4.0.0
807
808
  */
808
809
  export interface WebSocketClientOptions {
810
+ /**
811
+ * Subprotocols to offer in the opening handshake.
812
+ */
813
+ readonly protocols?: string | Array<string> | undefined
809
814
  /**
810
815
  * Headers to include in the opening handshake.
811
816
  *
@@ -857,6 +862,7 @@ export class WebSocketConstructor extends Context.Service<
857
862
 
858
863
  /**
859
864
  * Layer that provides `WebSocketConstructor` using `globalThis.WebSocket`.
865
+ * Rejects client options objects, including those with handshake headers.
860
866
  *
861
867
  * @stability unstable
862
868
  * @category layers
@@ -874,6 +880,8 @@ export const layerWebSocketConstructorGlobal: Layer.Layer<WebSocketConstructor>
874
880
  /**
875
881
  * Creates a `Socket` backed by a `WebSocketConstructor`, dialing the
876
882
  * WebSocket for each reader acquisition.
883
+ * Optional handshake headers require a Node or Bun constructor; without
884
+ * headers, protocols are passed directly for browser compatibility.
877
885
  *
878
886
  * @stability unstable
879
887
  * @category constructors
@@ -882,13 +890,21 @@ export const layerWebSocketConstructorGlobal: Layer.Layer<WebSocketConstructor>
882
890
  export const makeWebSocket = (url: string | Effect.Effect<string>, options?: {
883
891
  readonly openTimeout?: Duration.Input | undefined
884
892
  readonly protocols?: string | Array<string> | undefined
893
+ readonly headers?: Readonly<Record<string, string>> | undefined
885
894
  readonly highWaterMark?: number | undefined
886
895
  }): Effect.Effect<Socket, never, WebSocketConstructor> =>
887
896
  WebSocketConstructor.use((makeWs) =>
888
897
  fromWebSocket(
889
898
  Effect.acquireRelease(
890
899
  (typeof url === "string" ? Effect.succeed(url) : url).pipe(
891
- Effect.map((url) => makeWs(url, options?.protocols))
900
+ Effect.map((url) =>
901
+ makeWs(
902
+ url,
903
+ options?.headers === undefined
904
+ ? options?.protocols
905
+ : { protocols: options.protocols, headers: options.headers }
906
+ )
907
+ )
892
908
  ),
893
909
  (ws) => Effect.sync(() => ws.close(1000))
894
910
  ),
@@ -1165,6 +1181,7 @@ const defaultHighWaterMark = 64 * 1024
1165
1181
  /**
1166
1182
  * Creates a binary `Channel` backed by a WebSocket URL, requiring a
1167
1183
  * `WebSocketConstructor` service.
1184
+ * Supports subprotocols and, with a Node or Bun constructor, handshake headers.
1168
1185
  *
1169
1186
  * @stability unstable
1170
1187
  * @category constructors
@@ -1175,6 +1192,7 @@ export const makeWebSocketChannel = <IE = never>(
1175
1192
  options?: {
1176
1193
  readonly openTimeout?: Duration.Input | undefined
1177
1194
  readonly protocols?: string | Array<string> | undefined
1195
+ readonly headers?: Readonly<Record<string, string>> | undefined
1178
1196
  readonly highWaterMark?: number | undefined
1179
1197
  }
1180
1198
  ): Channel.Channel<
@@ -1193,6 +1211,7 @@ export const makeWebSocketChannel = <IE = never>(
1193
1211
  /**
1194
1212
  * Layer that provides a `Socket` service backed by a WebSocket URL or URL
1195
1213
  * effect.
1214
+ * Supports subprotocols and, with a Node or Bun constructor, handshake headers.
1196
1215
  *
1197
1216
  * @stability unstable
1198
1217
  * @category layers
@@ -1203,6 +1222,7 @@ export const layerWebSocket: (
1203
1222
  options?: {
1204
1223
  readonly openTimeout?: Duration.Input | undefined
1205
1224
  readonly protocols?: string | Array<string> | undefined
1225
+ readonly headers?: Readonly<Record<string, string>> | undefined
1206
1226
  readonly highWaterMark?: number | undefined
1207
1227
  } | undefined
1208
1228
  ) => Layer.Layer<Socket, never, WebSocketConstructor> = flow(makeWebSocket, Layer.effect(Socket))
@@ -5,6 +5,12 @@
5
5
 
6
6
  // @barrel: Auto-generated exports. Do not edit manually.
7
7
 
8
+ /**
9
+ * @stability experimental
10
+ * @since 4.0.0
11
+ */
12
+ export * as DatagramSocket from "./DatagramSocket.ts"
13
+
8
14
  /**
9
15
  * @stability unstable
10
16
  * @since 4.0.0
@@ -140,14 +140,21 @@ export const make = <RD = never>({
140
140
  PRIMARY KEY (migration_id)
141
141
  )`,
142
142
  pg: () =>
143
- Effect.catch(
144
- sql`select ${table}::regclass`,
145
- () =>
146
- sql`CREATE TABLE ${sql(table)} (
143
+ Effect.flatMap(
144
+ migrationsTableExists(sql, table),
145
+ (exists) =>
146
+ exists ? Effect.void : Effect.asVoid(sql`CREATE TABLE ${sql(table)} (
147
147
  migration_id integer primary key,
148
148
  created_at timestamp with time zone not null default now(),
149
149
  name text not null
150
- )`
150
+ )`).pipe(
151
+ Effect.catch((error) =>
152
+ Effect.flatMap(
153
+ migrationsTableExists(sql, table),
154
+ (exists) => exists ? Effect.void : Effect.fail(error)
155
+ )
156
+ )
157
+ )
151
158
  ),
152
159
  orElse: () =>
153
160
  sql`CREATE TABLE IF NOT EXISTS ${sql(table)} (
@@ -328,6 +335,15 @@ export const make = <RD = never>({
328
335
  return completed
329
336
  })
330
337
 
338
+ const migrationsTableExists = (sql: Client.SqlClient, table: string): Effect.Effect<boolean, SqlError> => {
339
+ // Match query quoting: to_regclass folds unquoted names to lowercase.
340
+ const quotedTable = sql`${sql(table)}`.compile(true)[0]
341
+ return Effect.map(
342
+ sql`SELECT 1 AS found WHERE to_regclass(${quotedTable}) IS NOT NULL`.withoutTransform,
343
+ (rows) => rows.length > 0
344
+ )
345
+ }
346
+
331
347
  const migrationOrder = Order.make<ResolvedMigration>(([a], [b]) => Order.Number(a, b))
332
348
 
333
349
  const isConstraintConflict = (error: SqlError): boolean =>
@@ -9,7 +9,7 @@
9
9
  * @stability unstable
10
10
  * @since 4.0.0
11
11
  */
12
- import type * as Cause from "../Cause.ts"
12
+ import * as Cause from "../Cause.ts"
13
13
  import { Clock } from "../Clock.ts"
14
14
  import * as Context from "../Context.ts"
15
15
  import * as Effect from "../Effect.ts"
@@ -280,6 +280,12 @@ export const make = Effect.fnUntraced(function*(options: SqlClient.MakeOptions)
280
280
  *
281
281
  * **Details**
282
282
  *
283
+ * Both acquirers lease the connection into the current scope and hold the
284
+ * semaphore until that scope closes, so a statement or stream keeps the
285
+ * connection until it finishes and another fiber cannot begin a transaction
286
+ * underneath it. Effects that use the connection directly, such as an export,
287
+ * have to run inside `Effect.scoped`.
288
+ *
283
289
  * SQLite can keep a transaction open after COMMIT fails, for example on a
284
290
  * deferred foreign key violation. `onCommitFailure` rolls it back before the
285
291
  * connection is reused. If that ROLLBACK fails, the connection is rejected, and
@@ -300,7 +306,7 @@ export const makeSqliteAcquirers = <C extends Connection.Connection>(options: {
300
306
  readonly semaphore: Semaphore.Semaphore
301
307
  readonly isTransaction?: ((conn: C) => boolean) | undefined
302
308
  }): {
303
- readonly acquirer: Effect.Effect<C, SqlError>
309
+ readonly acquirer: Effect.Effect<C, SqlError, Scope.Scope>
304
310
  readonly transactionAcquirer: Effect.Effect<C, SqlError, Scope.Scope>
305
311
  readonly onCommitFailure: (conn: Connection.Connection) => Effect.Effect<void, SqlError>
306
312
  } => {
@@ -336,15 +342,17 @@ export const makeSqliteAcquirers = <C extends Connection.Connection>(options: {
336
342
  })
337
343
  })
338
344
 
345
+ const lease = Effect.uninterruptibleMask((restore) =>
346
+ Effect.flatMap(Effect.scope, (scope) =>
347
+ restore(semaphore.take(1)).pipe(
348
+ Effect.andThen(Scope.addFinalizer(scope, semaphore.release(1))),
349
+ Effect.andThen(available)
350
+ ))
351
+ )
352
+
339
353
  return {
340
- acquirer: semaphore.withPermits(1)(available),
341
- transactionAcquirer: Effect.uninterruptibleMask((restore) =>
342
- Effect.flatMap(Effect.scope, (scope) =>
343
- restore(semaphore.take(1)).pipe(
344
- Effect.andThen(Scope.addFinalizer(scope, semaphore.release(1))),
345
- Effect.andThen(available)
346
- ))
347
- ),
354
+ acquirer: lease,
355
+ transactionAcquirer: lease,
348
356
  onCommitFailure: (conn) => Effect.tapCause(rollback(conn as C), (cause) => Effect.sync(() => poison(cause)))
349
357
  }
350
358
  }
@@ -359,6 +367,18 @@ const isNoTransactionError = (error: SqlError): boolean => {
359
367
  return /no transaction is active/i.test(message);
360
368
  }
361
369
 
370
+ /**
371
+ * A failed COMMIT cleanup leaves the connection in an unsafe state, so both the
372
+ * COMMIT error and the cleanup error are promoted to defects: typed recovery
373
+ * must not be able to hide them.
374
+ */
375
+ const failuresToDefects = <E>(cause: Cause.Cause<E>): Cause.Cause<never> =>
376
+ Cause.fromReasons(cause.reasons.map((reason) =>
377
+ Cause.isFailReason(reason)
378
+ ? Cause.makeDieReason(reason.error).annotate(Cause.reasonAnnotations(reason))
379
+ : reason
380
+ ))
381
+
362
382
  /**
363
383
  * Builds a transaction wrapper that begins top-level transactions, uses
364
384
  * savepoints for nested transactions, commits on success, and rolls back on
@@ -424,15 +444,19 @@ export const makeWithTransaction = <I, S>(options: {
424
444
  )
425
445
  ),
426
446
  (exit) => {
427
- let effect: Effect.Effect<void>
447
+ let effect: Effect.Effect<void, SqlError>
428
448
  if (Exit.isSuccess(exit)) {
429
449
  if (id === 0) {
430
450
  span.event("effect.sql.transaction.commit", clock.currentTimeNanosUnsafe())
431
451
  const onCommitFailure = options.onCommitFailure
432
- effect = Effect.orDie(options.commit(conn))
433
- if (onCommitFailure) {
434
- effect = Effect.onError(effect, () => Effect.orDie(onCommitFailure(conn)))
435
- }
452
+ effect = onCommitFailure
453
+ ? Effect.catchCause(options.commit(conn), (commitCause) =>
454
+ Effect.matchCauseEffect(onCommitFailure(conn), {
455
+ onFailure: (cleanupCause) =>
456
+ Effect.failCause(failuresToDefects(Cause.combine(commitCause, cleanupCause))),
457
+ onSuccess: () => Effect.failCause(commitCause)
458
+ }))
459
+ : options.commit(conn)
436
460
  } else {
437
461
  span.event("effect.sql.transaction.savepoint", clock.currentTimeNanosUnsafe())
438
462
  effect = Effect.void
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Provides a deterministic implementation of the Effect `Crypto` service for
3
+ * tests.
4
+ *
5
+ * Random bytes, numbers, shuffles, UUIDs, and ULIDs are derived from a seeded
6
+ * pseudo-random generator. Digest operations are delegated to an underlying
7
+ * platform `Crypto` service.
8
+ *
9
+ * @stability unstable
10
+ * @since 4.0.0
11
+ */
12
+ import * as Crypto from "../Crypto.ts"
13
+ import * as Effect from "../Effect.ts"
14
+ import * as Layer from "../Layer.ts"
15
+ import * as Random from "../Random.ts"
16
+
17
+ /**
18
+ * Creates a deterministic `Crypto` service using a seeded pseudo-random
19
+ * generator.
20
+ *
21
+ * **When to use**
22
+ *
23
+ * Use to construct a `Crypto` service whose random operations are reproducible
24
+ * in tests while preserving the digest implementation of a platform `Crypto`
25
+ * service.
26
+ *
27
+ * **Details**
28
+ *
29
+ * The effect requires an existing `Crypto` service and delegates digest
30
+ * operations to it. Each evaluation starts a new random sequence from `seed`;
31
+ * every other random operation is derived from that sequence.
32
+ *
33
+ * **Gotchas**
34
+ *
35
+ * The generated values are deterministic and must not be used for
36
+ * security-sensitive purposes. The sequence depends on the order in which
37
+ * random operations are evaluated. UUIDv7 and ULID values also include the
38
+ * current `Clock` time, which must be controlled separately when their complete
39
+ * output needs to be reproducible.
40
+ *
41
+ * @see {@link layer} for providing the service as a layer
42
+ *
43
+ * @stability unstable
44
+ * @category constructors
45
+ * @since 4.0.0
46
+ */
47
+ export const make: (
48
+ seed: string | number
49
+ ) => Effect.Effect<Crypto.Crypto, never, Crypto.Crypto> = Effect.fnUntraced(
50
+ function*(_seed: string | number) {
51
+ const crypto = yield* Crypto.Crypto
52
+ const seededRandom = yield* Random.Random
53
+ return Crypto.make({
54
+ randomBytes: (size) => {
55
+ const bytes = new Uint8Array(size)
56
+ for (let i = 0; i < size; i++) {
57
+ bytes[i] = Math.floor(seededRandom.nextDoubleUnsafe() * 256)
58
+ }
59
+ return bytes
60
+ },
61
+ digest: crypto.digest
62
+ })
63
+ },
64
+ (effect, seed) => Random.withSeed(effect, seed)
65
+ )
66
+
67
+ /**
68
+ * Creates a layer that provides deterministic `Crypto` random operations from
69
+ * a seed.
70
+ *
71
+ * **When to use**
72
+ *
73
+ * Use to provide reproducible random bytes, numbers, shuffles, UUIDs, and ULIDs
74
+ * to an Effect program under test.
75
+ *
76
+ * **Details**
77
+ *
78
+ * The layer requires an existing platform `Crypto` service whose digest
79
+ * implementation is retained. Each layer build starts the random sequence from
80
+ * `seed`.
81
+ *
82
+ * **Gotchas**
83
+ *
84
+ * UUIDv7 and ULID values also depend on the current `Clock`. Provide a test
85
+ * clock when their complete output needs to be reproducible.
86
+ *
87
+ * @see {@link make} for constructing the service directly
88
+ *
89
+ * @stability unstable
90
+ * @category layers
91
+ * @since 4.0.0
92
+ */
93
+ export const layer = (seed: string | number): Layer.Layer<Crypto.Crypto, never, Crypto.Crypto> =>
94
+ Layer.effect(Crypto.Crypto, make(seed))
@@ -17,6 +17,12 @@ export * as TestClock from "./TestClock.ts"
17
17
  */
18
18
  export * as TestConsole from "./TestConsole.ts"
19
19
 
20
+ /**
21
+ * @stability unstable
22
+ * @since 4.0.0
23
+ */
24
+ export * as TestCrypto from "./TestCrypto.ts"
25
+
20
26
  /**
21
27
  * @stability unstable
22
28
  * @since 4.0.0
@@ -10,7 +10,6 @@
10
10
  * @stability unstable
11
11
  * @since 4.0.0
12
12
  */
13
- import * as Arr from "../Array.ts"
14
13
  import type { NonEmptyReadonlyArray } from "../Array.ts"
15
14
  import type * as Brand from "../Brand.ts"
16
15
  import * as Cause from "../Cause.ts"
@@ -18,8 +17,9 @@ import * as Context from "../Context.ts"
18
17
  import * as Effect from "../Effect.ts"
19
18
  import * as Base64Url from "../encoding/Base64Url.ts"
20
19
  import * as Exit from "../Exit.ts"
21
- import * as Filter from "../Filter.ts"
20
+ import * as Fiber from "../Fiber.ts"
22
21
  import { dual } from "../Function.ts"
22
+ import * as Latch from "../Latch.ts"
23
23
  import * as Option from "../Option.ts"
24
24
  import * as Schema from "../Schema.ts"
25
25
  import * as SchemaGetter from "../SchemaGetter.ts"
@@ -180,6 +180,20 @@ export {
180
180
  * Runs an effect and records its exit into the durable deferred, resuming
181
181
  * workflows that are waiting on that deferred.
182
182
  *
183
+ * **Details**
184
+ *
185
+ * The effect and the write of its exit count as one in-flight activity of the
186
+ * workflow, so concurrent branches parked on durable waits do not suspend the
187
+ * run until the exit is recorded. This applies even when the effect itself is
188
+ * not an activity. If the effect parks on a durable wait, the run suspends
189
+ * once the other in-flight activities finish, and nothing is recorded.
190
+ *
191
+ * **Gotchas**
192
+ *
193
+ * A long-running effect keeps the run active instead of letting a parked
194
+ * sibling suspend it. Completing a deferred the run is waiting on still
195
+ * interrupts and replays the run.
196
+ *
183
197
  * @stability unstable
184
198
  * @category combinators
185
199
  * @since 4.0.0
@@ -189,6 +203,20 @@ export const into: {
189
203
  * Runs an effect and records its exit into the durable deferred, resuming
190
204
  * workflows that are waiting on that deferred.
191
205
  *
206
+ * **Details**
207
+ *
208
+ * The effect and the write of its exit count as one in-flight activity of the
209
+ * workflow, so concurrent branches parked on durable waits do not suspend the
210
+ * run until the exit is recorded. This applies even when the effect itself is
211
+ * not an activity. If the effect parks on a durable wait, the run suspends
212
+ * once the other in-flight activities finish, and nothing is recorded.
213
+ *
214
+ * **Gotchas**
215
+ *
216
+ * A long-running effect keeps the run active instead of letting a parked
217
+ * sibling suspend it. Completing a deferred the run is waiting on still
218
+ * interrupts and replays the run.
219
+ *
192
220
  * @stability unstable
193
221
  * @category combinators
194
222
  * @since 4.0.0
@@ -210,6 +238,20 @@ export const into: {
210
238
  * Runs an effect and records its exit into the durable deferred, resuming
211
239
  * workflows that are waiting on that deferred.
212
240
  *
241
+ * **Details**
242
+ *
243
+ * The effect and the write of its exit count as one in-flight activity of the
244
+ * workflow, so concurrent branches parked on durable waits do not suspend the
245
+ * run until the exit is recorded. This applies even when the effect itself is
246
+ * not an activity. If the effect parks on a durable wait, the run suspends
247
+ * once the other in-flight activities finish, and nothing is recorded.
248
+ *
249
+ * **Gotchas**
250
+ *
251
+ * A long-running effect keeps the run active instead of letting a parked
252
+ * sibling suspend it. Completing a deferred the run is waiting on still
253
+ * interrupts and replays the run.
254
+ *
213
255
  * @stability unstable
214
256
  * @category combinators
215
257
  * @since 4.0.0
@@ -243,43 +285,86 @@ export const into: {
243
285
  | Error["DecodingServices"]
244
286
  | Success["EncodingServices"]
245
287
  | Error["EncodingServices"]
246
- > =>
247
- Effect.contextWith(
248
- (context: Context.Context<WorkflowEngine | WorkflowInstance>) => {
249
- const engine = Context.get(context, EngineTag)
250
- const parentInstance = Context.get(context, InstanceTag)
251
- const instance = { ...parentInstance }
252
- return Effect.onExit(
253
- Effect.provideService(effect, InstanceTag, instance),
254
- Effect.fnUntraced(function*(exit) {
255
- if (Exit.isFailure(exit)) {
256
- const [interrupts, reasons] = Arr.partition(
257
- exit.cause.reasons,
258
- Filter.fromPredicate(Cause.isInterruptReason)
259
- )
260
- if (interrupts.length === exit.cause.reasons.length) {
261
- // An interrupt-only exit is never a result: the effect was
262
- // suspended, preempted or interrupted, so record nothing.
263
- if (instance.suspended) {
264
- parentInstance.suspended = true
265
- }
266
- return
267
- } else if (interrupts.length > 0) {
268
- exit = Exit.failCause(Cause.fromReasons(reasons))
269
- }
270
- }
271
- yield* engine.deferredDone(self, {
272
- workflowName: instance.workflow._tag,
273
- executionId: instance.executionId,
274
- deferredName: self.name,
275
- exit
276
- })
277
- })
278
- )
279
- }
280
- )
288
+ > => recordHeld(effect, self, false)
281
289
  )
282
290
 
291
+ /**
292
+ * Records `exit` into `self`. An interrupt-only exit is never a result: the
293
+ * effect was suspended, preempted or interrupted, so nothing is recorded.
294
+ * Interrupts mixed into a failure are dropped from the recorded cause.
295
+ */
296
+ const recordExit = (
297
+ engine: WorkflowEngine["Service"],
298
+ instance: WorkflowInstance["Service"],
299
+ self: DurableDeferred<any, any>,
300
+ exit: Exit.Exit<any, any>
301
+ ) => {
302
+ if (Exit.isFailure(exit)) {
303
+ if (Cause.hasInterruptsOnly(exit.cause)) return Effect.void
304
+ if (Cause.hasInterrupts(exit.cause)) {
305
+ exit = Exit.failCause(Cause.fromReasons(
306
+ exit.cause.reasons.filter((reason) => !Cause.isInterruptReason(reason))
307
+ ))
308
+ }
309
+ }
310
+ return engine.deferredDone(self, {
311
+ workflowName: instance.workflow._tag,
312
+ executionId: instance.executionId,
313
+ deferredName: self.name,
314
+ exit
315
+ })
316
+ }
317
+
318
+ /**
319
+ * Runs `effect` as one activity of the enclosing workflow and records its exit
320
+ * into `self`, returning a previously recorded exit instead when `checkCache`
321
+ * is set. The hold spans the lookup, `effect` and the write, so a parked
322
+ * sibling cannot suspend the run before the continuation starts.
323
+ */
324
+ const recordHeld = Effect.fnUntraced(function*<A, E, R>(
325
+ effect: Effect.Effect<A, E, R>,
326
+ self: DurableDeferred<any, any>,
327
+ checkCache: boolean
328
+ ): Effect.fn.Return<A, E, R | WorkflowEngine | WorkflowInstance> {
329
+ const engine = yield* EngineTag
330
+ const instance = yield* InstanceTag
331
+ const exit = yield* Workflow.wrapActivityResult(
332
+ Effect.gen(function*() {
333
+ if (checkCache) {
334
+ const cached = yield* engine.deferredResult(self)
335
+ if (Option.isSome(cached)) return cached
336
+ }
337
+ // `effect` counts on its own activity state, so a durable await inside
338
+ // it waits for its own siblings and not for the hold above. The copy
339
+ // starts unsuspended: a parked arm of an enclosing race leaves that
340
+ // copy marked suspended while only interrupting its own fiber.
341
+ const local: WorkflowInstance["Service"] = {
342
+ ...instance,
343
+ suspended: false,
344
+ activityState: { count: 0, latch: Latch.makeUnsafe() }
345
+ }
346
+ // A parked `effect` interrupts the fiber it runs on. The child fiber
347
+ // keeps that from skipping the suspension handling below.
348
+ const fiber = yield* effect.pipe(
349
+ Effect.provideService(InstanceTag, local),
350
+ Effect.onExit((exit) => recordExit(engine, instance, self, exit)),
351
+ Effect.forkChild({ startImmediately: true })
352
+ )
353
+ const exit = yield* Effect.onInterrupt(Fiber.await(fiber), () => Fiber.interrupt(fiber))
354
+ return local.suspended && Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause)
355
+ ? Option.none()
356
+ : Option.some(exit)
357
+ }),
358
+ // Every branch of `effect` parked: release the hold and let the enclosing
359
+ // activities finish before suspending. External preemption does not wait.
360
+ Option.isNone
361
+ )
362
+ if (Option.isNone(exit)) {
363
+ return yield* Workflow.suspend(instance)
364
+ }
365
+ return yield* exit.value
366
+ })
367
+
283
368
  /**
284
369
  * Runs effects as a durable race, returning a previously persisted result when
285
370
  * present or completing a named deferred with the first result.
@@ -312,17 +397,7 @@ export const raceAll = <
312
397
  success: options.success,
313
398
  error: options.error
314
399
  })
315
- return Effect.gen(function*() {
316
- const engine = yield* EngineTag
317
- const exit = yield* engine.deferredResult(deferred)
318
- if (Option.isSome(exit)) {
319
- return yield* exit.value
320
- }
321
- return yield* into(
322
- Effect.raceAll(options.effects),
323
- deferred
324
- )
325
- })
400
+ return recordHeld(Effect.raceAll(options.effects), deferred, true)
326
401
  }
327
402
 
328
403
  /**
@@ -826,7 +826,8 @@ export const layerMemory: Layer.Layer<WorkflowEngine> = Layer.effect(WorkflowEng
826
826
  Workflow.intoResult,
827
827
  Effect.provideService(WorkflowInstance, activityInstance),
828
828
  Effect.onExit((exit) => {
829
- state.exit = exit
829
+ // A preempted activity must run again on the next replay.
830
+ state.exit = Exit.isFailure(exit) && Cause.hasInterruptsOnly(exit.cause) ? undefined : exit
830
831
  return Effect.void
831
832
  })
832
833
  )