effect 4.0.4 → 4.0.5

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 (308) hide show
  1. package/ai-docs/src/52_graphql/10_basics.ts +19 -13
  2. package/ai-docs/src/52_graphql/30_batching.ts +6 -4
  3. package/ai-docs/src/52_graphql/40_aliased-batching.ts +6 -4
  4. package/dist/Array.d.ts +90 -6
  5. package/dist/Array.d.ts.map +1 -1
  6. package/dist/Array.js +4 -2
  7. package/dist/Array.js.map +1 -1
  8. package/dist/Channel.d.ts +6 -1
  9. package/dist/Channel.d.ts.map +1 -1
  10. package/dist/Channel.js +17 -3
  11. package/dist/Channel.js.map +1 -1
  12. package/dist/ConfigProvider.d.ts.map +1 -1
  13. package/dist/ConfigProvider.js +33 -32
  14. package/dist/ConfigProvider.js.map +1 -1
  15. package/dist/Duration.d.ts.map +1 -1
  16. package/dist/Duration.js +2 -5
  17. package/dist/Duration.js.map +1 -1
  18. package/dist/Effect.d.ts +61 -19
  19. package/dist/Effect.d.ts.map +1 -1
  20. package/dist/Effect.js +58 -39
  21. package/dist/Effect.js.map +1 -1
  22. package/dist/FileSystem.d.ts +23 -9
  23. package/dist/FileSystem.d.ts.map +1 -1
  24. package/dist/FileSystem.js.map +1 -1
  25. package/dist/Match.d.ts +20 -6
  26. package/dist/Match.d.ts.map +1 -1
  27. package/dist/Match.js +2 -2
  28. package/dist/Match.js.map +1 -1
  29. package/dist/Path.d.ts +37 -5
  30. package/dist/Path.d.ts.map +1 -1
  31. package/dist/Path.js +36 -5
  32. package/dist/Path.js.map +1 -1
  33. package/dist/Schema.d.ts +7 -2
  34. package/dist/Schema.d.ts.map +1 -1
  35. package/dist/Schema.js +9 -2
  36. package/dist/Schema.js.map +1 -1
  37. package/dist/String.d.ts +12 -0
  38. package/dist/String.d.ts.map +1 -1
  39. package/dist/String.js +17 -4
  40. package/dist/String.js.map +1 -1
  41. package/dist/TxRef.d.ts.map +1 -1
  42. package/dist/TxRef.js +3 -2
  43. package/dist/TxRef.js.map +1 -1
  44. package/dist/ai/AiError.d.ts +10 -10
  45. package/dist/ai/AiError.d.ts.map +1 -1
  46. package/dist/ai/AiError.js +1 -1
  47. package/dist/ai/AiError.js.map +1 -1
  48. package/dist/ai/Decision.d.ts +70 -19
  49. package/dist/ai/Decision.d.ts.map +1 -1
  50. package/dist/ai/Decision.js +37 -3
  51. package/dist/ai/Decision.js.map +1 -1
  52. package/dist/ai/DecisionModel.js +1 -1
  53. package/dist/ai/DecisionModel.js.map +1 -1
  54. package/dist/ai/LanguageModel.d.ts.map +1 -1
  55. package/dist/ai/LanguageModel.js +47 -35
  56. package/dist/ai/LanguageModel.js.map +1 -1
  57. package/dist/ai/McpServer.d.ts +6 -52
  58. package/dist/ai/McpServer.d.ts.map +1 -1
  59. package/dist/ai/McpServer.js +2 -28
  60. package/dist/ai/McpServer.js.map +1 -1
  61. package/dist/ai/Response.d.ts +1 -1
  62. package/dist/cli/Completions.d.ts +5 -0
  63. package/dist/cli/Completions.d.ts.map +1 -1
  64. package/dist/cli/Completions.js.map +1 -1
  65. package/dist/cli/Flag.d.ts +100 -0
  66. package/dist/cli/Flag.d.ts.map +1 -1
  67. package/dist/cli/Flag.js +33 -0
  68. package/dist/cli/Flag.js.map +1 -1
  69. package/dist/cli/Param.d.ts +87 -0
  70. package/dist/cli/Param.d.ts.map +1 -1
  71. package/dist/cli/Param.js +30 -0
  72. package/dist/cli/Param.js.map +1 -1
  73. package/dist/cli/internal/command.d.ts.map +1 -1
  74. package/dist/cli/internal/command.js +2 -1
  75. package/dist/cli/internal/command.js.map +1 -1
  76. package/dist/cli/internal/completions/bash.js +4 -3
  77. package/dist/cli/internal/completions/bash.js.map +1 -1
  78. package/dist/cli/internal/completions/descriptor.js +8 -1
  79. package/dist/cli/internal/completions/descriptor.js.map +1 -1
  80. package/dist/cli/internal/completions/fish.js +4 -4
  81. package/dist/cli/internal/completions/fish.js.map +1 -1
  82. package/dist/cli/internal/completions/powershell.js +3 -2
  83. package/dist/cli/internal/completions/powershell.js.map +1 -1
  84. package/dist/cli/internal/completions/zsh.js +7 -3
  85. package/dist/cli/internal/completions/zsh.js.map +1 -1
  86. package/dist/cli/internal/parser.js +11 -3
  87. package/dist/cli/internal/parser.js.map +1 -1
  88. package/dist/cli/internal/wizard.d.ts.map +1 -1
  89. package/dist/cli/internal/wizard.js +2 -1
  90. package/dist/cli/internal/wizard.js.map +1 -1
  91. package/dist/cluster/ClusterWorkflowEngine.d.ts +1 -0
  92. package/dist/cluster/ClusterWorkflowEngine.d.ts.map +1 -1
  93. package/dist/cluster/ClusterWorkflowEngine.js +67 -13
  94. package/dist/cluster/ClusterWorkflowEngine.js.map +1 -1
  95. package/dist/cluster/MessageStorage.d.ts.map +1 -1
  96. package/dist/cluster/MessageStorage.js +20 -12
  97. package/dist/cluster/MessageStorage.js.map +1 -1
  98. package/dist/cluster/Runners.d.ts.map +1 -1
  99. package/dist/cluster/Runners.js +54 -34
  100. package/dist/cluster/Runners.js.map +1 -1
  101. package/dist/cluster/internal/entityManager.js +1 -1
  102. package/dist/cluster/internal/entityManager.js.map +1 -1
  103. package/dist/encoding/Sse.js +1 -1
  104. package/dist/encoding/Sse.js.map +1 -1
  105. package/dist/graphql/GraphQLClient.d.ts.map +1 -1
  106. package/dist/graphql/GraphQLClient.js +12 -5
  107. package/dist/graphql/GraphQLClient.js.map +1 -1
  108. package/dist/graphql/GraphQLProtocol.d.ts +249 -11
  109. package/dist/graphql/GraphQLProtocol.d.ts.map +1 -1
  110. package/dist/graphql/GraphQLProtocol.js +98 -8
  111. package/dist/graphql/GraphQLProtocol.js.map +1 -1
  112. package/dist/graphql/GraphQLResolver.d.ts +1 -1
  113. package/dist/graphql/GraphQLResolver.d.ts.map +1 -1
  114. package/dist/graphql/GraphQLResolver.js +46 -16
  115. package/dist/graphql/GraphQLResolver.js.map +1 -1
  116. package/dist/http/FetchHttpClient.d.ts +6 -3
  117. package/dist/http/FetchHttpClient.d.ts.map +1 -1
  118. package/dist/http/FetchHttpClient.js +6 -3
  119. package/dist/http/FetchHttpClient.js.map +1 -1
  120. package/dist/http/FindMyWay/internal/router.js +2 -1
  121. package/dist/http/FindMyWay/internal/router.js.map +1 -1
  122. package/dist/http/Headers.d.ts +3 -3
  123. package/dist/http/Headers.d.ts.map +1 -1
  124. package/dist/http/Headers.js +6 -5
  125. package/dist/http/Headers.js.map +1 -1
  126. package/dist/http/HttpClient.d.ts +12 -0
  127. package/dist/http/HttpClient.d.ts.map +1 -1
  128. package/dist/http/HttpClient.js +14 -1
  129. package/dist/http/HttpClient.js.map +1 -1
  130. package/dist/http/HttpClientError.d.ts.map +1 -1
  131. package/dist/http/HttpClientError.js +8 -6
  132. package/dist/http/HttpClientError.js.map +1 -1
  133. package/dist/http/HttpClientRequest.d.ts +147 -4
  134. package/dist/http/HttpClientRequest.d.ts.map +1 -1
  135. package/dist/http/HttpClientRequest.js +47 -1
  136. package/dist/http/HttpClientRequest.js.map +1 -1
  137. package/dist/http/HttpMiddleware.d.ts +60 -0
  138. package/dist/http/HttpMiddleware.d.ts.map +1 -1
  139. package/dist/http/HttpMiddleware.js +67 -5
  140. package/dist/http/HttpMiddleware.js.map +1 -1
  141. package/dist/http/HttpRouter.d.ts.map +1 -1
  142. package/dist/http/HttpRouter.js +52 -13
  143. package/dist/http/HttpRouter.js.map +1 -1
  144. package/dist/http/HttpServerError.d.ts.map +1 -1
  145. package/dist/http/HttpServerError.js +6 -4
  146. package/dist/http/HttpServerError.js.map +1 -1
  147. package/dist/http/HttpServerRequest.js +2 -2
  148. package/dist/http/HttpServerRequest.js.map +1 -1
  149. package/dist/http/HttpStaticServer.d.ts.map +1 -1
  150. package/dist/http/HttpStaticServer.js +4 -10
  151. package/dist/http/HttpStaticServer.js.map +1 -1
  152. package/dist/http/HttpTraceContext.js +1 -1
  153. package/dist/http/HttpTraceContext.js.map +1 -1
  154. package/dist/http/Url.d.ts.map +1 -1
  155. package/dist/http/Url.js +2 -1
  156. package/dist/http/Url.js.map +1 -1
  157. package/dist/http/UrlParams.d.ts +112 -8
  158. package/dist/http/UrlParams.d.ts.map +1 -1
  159. package/dist/http/UrlParams.js +141 -14
  160. package/dist/http/UrlParams.js.map +1 -1
  161. package/dist/http/internal/compression.d.ts +1 -1
  162. package/dist/http/internal/compression.d.ts.map +1 -1
  163. package/dist/http/internal/compression.js +2 -10
  164. package/dist/http/internal/compression.js.map +1 -1
  165. package/dist/http/internal/compressionBody.d.ts +2 -0
  166. package/dist/http/internal/compressionBody.d.ts.map +1 -0
  167. package/dist/http/internal/compressionBody.js +36 -0
  168. package/dist/http/internal/compressionBody.js.map +1 -0
  169. package/dist/http/internal/decompression.d.ts +2 -0
  170. package/dist/http/internal/decompression.d.ts.map +1 -0
  171. package/dist/http/internal/decompression.js +162 -0
  172. package/dist/http/internal/decompression.js.map +1 -0
  173. package/dist/http/internal/tracing.js +14 -4
  174. package/dist/http/internal/tracing.js.map +1 -1
  175. package/dist/http/internal/urlParams.d.ts +2 -0
  176. package/dist/http/internal/urlParams.d.ts.map +1 -0
  177. package/dist/http/internal/urlParams.js +4 -0
  178. package/dist/http/internal/urlParams.js.map +1 -0
  179. package/dist/http-api/HttpApiClient.d.ts.map +1 -1
  180. package/dist/http-api/HttpApiClient.js +2 -1
  181. package/dist/http-api/HttpApiClient.js.map +1 -1
  182. package/dist/internal/core.js +17 -1
  183. package/dist/internal/core.js.map +1 -1
  184. package/dist/internal/effect.js +5 -3
  185. package/dist/internal/effect.js.map +1 -1
  186. package/dist/internal/graphql.js +12 -2
  187. package/dist/internal/graphql.js.map +1 -1
  188. package/dist/internal/version.js +1 -1
  189. package/dist/observability/Otlp.d.ts +15 -0
  190. package/dist/observability/Otlp.d.ts.map +1 -1
  191. package/dist/observability/Otlp.js +3 -0
  192. package/dist/observability/Otlp.js.map +1 -1
  193. package/dist/observability/OtlpExporter.d.ts +8 -0
  194. package/dist/observability/OtlpExporter.d.ts.map +1 -1
  195. package/dist/observability/OtlpExporter.js +10 -1
  196. package/dist/observability/OtlpExporter.js.map +1 -1
  197. package/dist/observability/OtlpLogger.d.ts +17 -0
  198. package/dist/observability/OtlpLogger.d.ts.map +1 -1
  199. package/dist/observability/OtlpLogger.js +14 -1
  200. package/dist/observability/OtlpLogger.js.map +1 -1
  201. package/dist/observability/OtlpMetrics.d.ts +17 -0
  202. package/dist/observability/OtlpMetrics.d.ts.map +1 -1
  203. package/dist/observability/OtlpMetrics.js +13 -0
  204. package/dist/observability/OtlpMetrics.js.map +1 -1
  205. package/dist/observability/OtlpTracer.d.ts +17 -0
  206. package/dist/observability/OtlpTracer.d.ts.map +1 -1
  207. package/dist/observability/OtlpTracer.js +15 -2
  208. package/dist/observability/OtlpTracer.js.map +1 -1
  209. package/dist/reactivity/AsyncResult.d.ts +6 -0
  210. package/dist/reactivity/AsyncResult.d.ts.map +1 -1
  211. package/dist/reactivity/AsyncResult.js +27 -0
  212. package/dist/reactivity/AsyncResult.js.map +1 -1
  213. package/dist/reactivity/AtomRegistry.js +3 -1
  214. package/dist/reactivity/AtomRegistry.js.map +1 -1
  215. package/dist/rpc/RpcClient.d.ts.map +1 -1
  216. package/dist/rpc/RpcClient.js +6 -1
  217. package/dist/rpc/RpcClient.js.map +1 -1
  218. package/dist/sql/SqlClient.d.ts +8 -7
  219. package/dist/sql/SqlClient.d.ts.map +1 -1
  220. package/dist/sql/SqlClient.js +8 -7
  221. package/dist/sql/SqlClient.js.map +1 -1
  222. package/dist/testing/TestClock.d.ts +16 -3
  223. package/dist/testing/TestClock.d.ts.map +1 -1
  224. package/dist/testing/TestClock.js +112 -22
  225. package/dist/testing/TestClock.js.map +1 -1
  226. package/dist/workflow/Activity.js +1 -0
  227. package/dist/workflow/Activity.js.map +1 -1
  228. package/dist/workflow/Workflow.d.ts +22 -0
  229. package/dist/workflow/Workflow.d.ts.map +1 -1
  230. package/dist/workflow/Workflow.js +15 -3
  231. package/dist/workflow/Workflow.js.map +1 -1
  232. package/dist/workflow/WorkflowEngine.d.ts +20 -1
  233. package/dist/workflow/WorkflowEngine.d.ts.map +1 -1
  234. package/dist/workflow/WorkflowEngine.js +35 -21
  235. package/dist/workflow/WorkflowEngine.js.map +1 -1
  236. package/package.json +1 -1
  237. package/src/Array.ts +95 -6
  238. package/src/Channel.ts +24 -3
  239. package/src/ConfigProvider.ts +40 -44
  240. package/src/Duration.ts +4 -5
  241. package/src/Effect.ts +140 -53
  242. package/src/FileSystem.ts +23 -9
  243. package/src/Match.ts +65 -12
  244. package/src/Path.ts +52 -7
  245. package/src/Schema.ts +17 -4
  246. package/src/String.ts +17 -4
  247. package/src/TxRef.ts +4 -2
  248. package/src/ai/AiError.ts +3 -1
  249. package/src/ai/Decision.ts +70 -21
  250. package/src/ai/DecisionModel.ts +1 -1
  251. package/src/ai/LanguageModel.ts +57 -51
  252. package/src/ai/McpServer.ts +15 -55
  253. package/src/cli/Completions.ts +5 -0
  254. package/src/cli/Flag.ts +101 -0
  255. package/src/cli/Param.ts +96 -0
  256. package/src/cli/internal/command.ts +2 -1
  257. package/src/cli/internal/completions/bash.ts +4 -3
  258. package/src/cli/internal/completions/descriptor.ts +10 -1
  259. package/src/cli/internal/completions/fish.ts +4 -4
  260. package/src/cli/internal/completions/powershell.ts +5 -2
  261. package/src/cli/internal/completions/zsh.ts +7 -3
  262. package/src/cli/internal/parser.ts +12 -3
  263. package/src/cli/internal/wizard.ts +2 -1
  264. package/src/cluster/ClusterWorkflowEngine.ts +92 -24
  265. package/src/cluster/MessageStorage.ts +14 -3
  266. package/src/cluster/Runners.ts +88 -66
  267. package/src/cluster/internal/entityManager.ts +1 -1
  268. package/src/encoding/Sse.ts +1 -1
  269. package/src/graphql/GraphQLClient.ts +17 -5
  270. package/src/graphql/GraphQLProtocol.ts +274 -20
  271. package/src/graphql/GraphQLResolver.ts +45 -17
  272. package/src/http/FetchHttpClient.ts +6 -3
  273. package/src/http/FindMyWay/internal/router.ts +2 -1
  274. package/src/http/Headers.ts +6 -9
  275. package/src/http/HttpClient.ts +18 -1
  276. package/src/http/HttpClientError.ts +10 -6
  277. package/src/http/HttpClientRequest.ts +173 -7
  278. package/src/http/HttpMiddleware.ts +85 -5
  279. package/src/http/HttpRouter.ts +60 -13
  280. package/src/http/HttpServerError.ts +8 -4
  281. package/src/http/HttpServerRequest.ts +2 -2
  282. package/src/http/HttpStaticServer.ts +4 -10
  283. package/src/http/HttpTraceContext.ts +1 -1
  284. package/src/http/Url.ts +2 -1
  285. package/src/http/UrlParams.ts +225 -26
  286. package/src/http/internal/compression.ts +3 -17
  287. package/src/http/internal/compressionBody.ts +56 -0
  288. package/src/http/internal/decompression.ts +226 -0
  289. package/src/http/internal/tracing.ts +21 -4
  290. package/src/http/internal/urlParams.ts +5 -0
  291. package/src/http-api/HttpApiClient.ts +2 -1
  292. package/src/internal/core.ts +21 -1
  293. package/src/internal/effect.ts +5 -4
  294. package/src/internal/graphql.ts +30 -7
  295. package/src/internal/version.ts +1 -1
  296. package/src/observability/Otlp.ts +18 -0
  297. package/src/observability/OtlpExporter.ts +14 -3
  298. package/src/observability/OtlpLogger.ts +31 -2
  299. package/src/observability/OtlpMetrics.ts +30 -1
  300. package/src/observability/OtlpTracer.ts +32 -3
  301. package/src/reactivity/AsyncResult.ts +31 -0
  302. package/src/reactivity/AtomRegistry.ts +3 -1
  303. package/src/rpc/RpcClient.ts +6 -1
  304. package/src/sql/SqlClient.ts +9 -7
  305. package/src/testing/TestClock.ts +144 -29
  306. package/src/workflow/Activity.ts +1 -0
  307. package/src/workflow/Workflow.ts +31 -5
  308. package/src/workflow/WorkflowEngine.ts +60 -22
@@ -7,6 +7,7 @@
7
7
  * @internal
8
8
  */
9
9
  import type * as Completions from "../../Completions.ts"
10
+ import { takesSeparateValue } from "./descriptor.ts"
10
11
 
11
12
  // ---------------------------------------------------------------------------
12
13
  // Helpers
@@ -88,7 +89,7 @@ const flagCompletionArgs = (flag: Completions.FlagDescriptor): Array<string> =>
88
89
  if (flag.description) {
89
90
  args.push(`-d '${escapeFishString(flag.description)}'`)
90
91
  }
91
- const valueArgs = flagValueArgs(flag.type)
92
+ const valueArgs = takesSeparateValue(flag) ? flagValueArgs(flag.type) : undefined
92
93
  if (valueArgs) {
93
94
  args.push(valueArgs)
94
95
  }
@@ -159,8 +160,7 @@ const generateCompletions = (
159
160
 
160
161
  // Flag completions
161
162
  for (const flag of descriptor.flags) {
162
- const isBoolean = flag.type._tag === "Boolean"
163
- const dedup = isBoolean ? flagContainsOptCondition(flag) : valueFlagDedupCondition(flag)
163
+ const dedup = takesSeparateValue(flag) ? valueFlagDedupCondition(flag) : flagContainsOptCondition(flag)
164
164
  const flagCondition = condition ? `${condition}; and ${dedup}` : dedup
165
165
  const flagCondArg = flagCondition ? `-n '${flagCondition}'` : ``
166
166
 
@@ -170,7 +170,7 @@ const generateCompletions = (
170
170
  lines.push(parts.join(" "))
171
171
 
172
172
  // Boolean negation
173
- if (isBoolean) {
173
+ if (flag.type._tag === "Boolean") {
174
174
  const negParts = [`complete -c ${executableName}`]
175
175
  if (flagCondArg) negParts.push(flagCondArg)
176
176
  negParts.push(`-l no-${flag.name}`)
@@ -48,7 +48,9 @@ const valueFields = (type: Completions.FlagType | Completions.ArgumentType): str
48
48
 
49
49
  const flagEntry = (flag: Completions.FlagDescriptor): string =>
50
50
  `@{ name = ${quotePs(flag.name)}; forms = ${psArray(flagForms(flag))}; ` +
51
- `takesValue = $${flag.type._tag !== "Boolean"}; ${valueFields(flag.type)}; ` +
51
+ `takesValue = $${flag.type._tag !== "Boolean"}; ` +
52
+ (flag.optionalValue === true ? `optionalValue = $true; ` : "") +
53
+ `${valueFields(flag.type)}; ` +
52
54
  `description = ${psOptional(flag.description)} }`
53
55
 
54
56
  const argumentEntry = (argument: Completions.ArgumentDescriptor): string =>
@@ -235,7 +237,8 @@ const completer = (dataName: string): string =>
235
237
  continue
236
238
  }
237
239
  $used[$flag.name] = $true
238
- if (-not $hasValue) { $pending = $flag }
240
+ # A flag with an optional value only takes a value attached with '='.
241
+ if (-not $hasValue -and -not $flag.optionalValue) { $pending = $flag }
239
242
  }
240
243
  continue
241
244
  }
@@ -72,16 +72,20 @@ const argAction = (type: Completions.ArgumentType): string => {
72
72
  const flagSpecs = (flag: Completions.FlagDescriptor): Array<string> => {
73
73
  const specs: Array<string> = []
74
74
  const desc = flag.description ? `[${escapeZshExplanation(flag.description)}]` : ""
75
- const action = valueAction(flag.type)
75
+ // An optional value must follow `=` in the same word (`--name=-`), and `::`
76
+ // marks it optional.
77
+ const optional = flag.optionalValue === true && flag.type._tag !== "Boolean"
78
+ const valueSuffix = optional ? "=-" : ""
79
+ const action = optional ? `:${valueAction(flag.type)}` : valueAction(flag.type)
76
80
  const excl = `(${allForms(flag).join(" ")})`
77
81
 
78
82
  // Primary long name
79
- specs.push(`'${excl}--${flag.name}${desc}${action}'`)
83
+ specs.push(`'${excl}--${flag.name}${valueSuffix}${desc}${action}'`)
80
84
 
81
85
  // Aliases
82
86
  for (const alias of flag.aliases) {
83
87
  const prefix = alias.length === 1 ? "-" : "--"
84
- specs.push(`'${excl}${prefix}${alias}${desc}${action}'`)
88
+ specs.push(`'${excl}${prefix}${alias}${valueSuffix}${desc}${action}'`)
85
89
  }
86
90
 
87
91
  // Boolean negation
@@ -425,8 +425,9 @@ const asBooleanLiteral = (token: Token | undefined): string | undefined =>
425
425
  *
426
426
  * Value resolution order:
427
427
  * 1. Inline value: --flag=value or -f=value
428
- * 2. Boolean special case: implicit "true" or explicit boolean literal
429
- * 3. Next token: consume following Value token if present
428
+ * 2. Optional value: the flag's configured value, without consuming a token
429
+ * 3. Boolean special case: implicit "true" or explicit boolean literal
430
+ * 4. Next token: consume following Value token if present
430
431
  */
431
432
  const consumeFlagValue = (
432
433
  cursor: TokenCursor,
@@ -465,7 +466,7 @@ const consumeFlagValueWithTokens = (
465
466
  }
466
467
  }
467
468
 
468
- const literal = asBooleanLiteral(cursor.peek())
469
+ const literal = spec.optionalValue === undefined ? asBooleanLiteral(cursor.peek()) : undefined
469
470
  if (literal !== undefined) {
470
471
  const literalToken = cursor.take()
471
472
  return {
@@ -490,6 +491,14 @@ const consumeFlagValueWithTokens = (
490
491
  }
491
492
  }
492
493
 
494
+ if (spec.optionalValue !== undefined) {
495
+ return {
496
+ _tag: "Value",
497
+ value: spec.optionalValue,
498
+ tokens: []
499
+ }
500
+ }
501
+
493
502
  // Boolean flags: check for explicit literal or default to "true"
494
503
  if (Primitive.isBoolean(spec.primitiveType)) {
495
504
  const literal = asBooleanLiteral(cursor.peek())
@@ -171,8 +171,9 @@ const promptParam = Effect.fnUntraced(
171
171
  return values
172
172
  }
173
173
  // Inline option-looking values so the lexer cannot interpret them as flags or the -- delimiter.
174
+ // Flags with an optional value only accept an inline value.
174
175
  return values.flatMap((value) =>
175
- value.value.startsWith("-") && value.value.length > 1
176
+ single.optionalValue !== undefined || (value.value.startsWith("-") && value.value.length > 1)
176
177
  ? [commandLineArg(`--${single.name}=${value.value}`, `--${single.name}=${value.displayValue}`)]
177
178
  : [commandLineArg(`--${single.name}`), value]
178
179
  )
@@ -282,7 +282,7 @@ export const make = Effect.gen(function*() {
282
282
  yield* storage.clearAddress(clockAddress)
283
283
  })
284
284
 
285
- const resume = Effect.fnUntraced(function*(workflow: Workflow.Any, executionId: string) {
285
+ const resume = Effect.fnUntraced(function*(workflow: Workflow.Any, executionId: string, mode: WakeMode) {
286
286
  const maybeReply = yield* requestReply({
287
287
  workflow,
288
288
  entityType: `Workflow/${workflow._tag}`,
@@ -294,6 +294,8 @@ export const make = Effect.gen(function*() {
294
294
  const maybeSuspended = Option.filter(maybeReply, isSuspendedReply)
295
295
 
296
296
  if (Option.isNone(maybeSuspended)) return
297
+ // Replies without `failed` retain automatic wakes for compatibility.
298
+ if (mode === "automatic" && isFailedReply(maybeSuspended.value)) return
297
299
  if (
298
300
  !(yield* sharding.reset(
299
301
  Snowflake.Snowflake(maybeSuspended.value.requestId),
@@ -305,6 +307,15 @@ export const make = Effect.gen(function*() {
305
307
  yield* sharding.pollStorage
306
308
  })
307
309
 
310
+ // Activations load a persisted interrupt when they start, so the per-call
311
+ // check reads only memory.
312
+ const interruptPending = (instance: WorkflowEngine.WorkflowInstance["Service"]) =>
313
+ Effect.map(Effect.serviceOption(CurrentActivationScope), (activation) => {
314
+ const pending = Option.isSome(activation) ? pendingByActivation.get(activation.value) : undefined
315
+ return pending?.workflowName === instance.workflow._tag && pending.executionId === instance.executionId &&
316
+ pending.results.has(InterruptSignal.name)
317
+ })
318
+
308
319
  const sendResumeParent = Effect.fnUntraced(function*(options: {
309
320
  readonly workflowName: string
310
321
  readonly executionId: string
@@ -436,7 +447,25 @@ export const make = Effect.gen(function*() {
436
447
  const activation = yield* CurrentActivationScope
437
448
  let pending = pendingByActivation.get(activation)
438
449
  if (!pending) {
439
- pending = { workflowName: workflow._tag, executionId, results: new Map() }
450
+ const results = new Map<string, Exit.Exit<unknown, unknown>>()
451
+ // A new owner must see an interrupt persisted before it took over.
452
+ // Publish the cache only once loaded, so a retried build repeats
453
+ // a failed lookup.
454
+ const interrupt = yield* Effect.orDie(requestReply({
455
+ workflow,
456
+ entityType: `Workflow/${workflow._tag}`,
457
+ executionId,
458
+ tag: "deferred",
459
+ id: InterruptSignal.name
460
+ }))
461
+ if (Option.isSome(interrupt)) {
462
+ const decoded = decodeDeferredWithExit(interrupt.value as any)
463
+ results.set(
464
+ InterruptSignal.name,
465
+ (decoded.exit._tag === "Success" ? decoded.exit.value : decoded.exit) as Exit.Exit<unknown, unknown>
466
+ )
467
+ }
468
+ pending = { workflowName: workflow._tag, executionId, results }
440
469
  pendingByActivation.set(activation, pending)
441
470
  }
442
471
  // Retain awaited names through reply persistence, including suspension.
@@ -444,25 +473,30 @@ export const make = Effect.gen(function*() {
444
473
  let currentRun: {
445
474
  readonly request: Entity.Request<any>
446
475
  readonly instance: WorkflowEngine.WorkflowInstance["Service"]
476
+ // Activity awaits must wake the run after suspension without preempting it.
477
+ readonly activityAwaits: Map<string, number>
447
478
  } | undefined
448
479
  // Concurrent wakes share one wait for the current run to publish its reply.
480
+ // Explicit wakes cannot be dropped: operator resume bypasses the gate;
481
+ // InterruptSignal enters it only on a fresh entity, with no run to wait for.
449
482
  const resumeGate = Semaphore.makeUnsafe(1)
450
- const resumeCurrentRun = Effect.suspend(() =>
451
- resumeGate.withPermitsIfAvailable(1)(
452
- currentRun ? waitForRunReply(workflow, currentRun.request) : Effect.void
483
+ const resumeCurrentRun = (mode: WakeMode) =>
484
+ Effect.suspend(() =>
485
+ resumeGate.withPermitsIfAvailable(1)(
486
+ currentRun ? waitForRunReply(workflow, currentRun.request) : Effect.void
487
+ )
488
+ ).pipe(
489
+ // Release the gate before reset can start another run, so later
490
+ // completions can wake that replay.
491
+ Effect.flatMap((waited) => Option.isSome(waited) ? resume(workflow, executionId, mode) : Effect.void),
492
+ ensureSuccess
453
493
  )
454
- ).pipe(
455
- // Release the gate before reset can start another run, so later
456
- // completions can wake that replay.
457
- Effect.flatMap((waited) => Option.isSome(waited) ? resume(workflow, executionId) : Effect.void),
458
- ensureSuccess
459
- )
460
494
  return {
461
495
  run: (request: Entity.Request<any>) => {
462
496
  const instance = WorkflowEngine.WorkflowInstance.initial(workflow, executionId)
463
- currentRun = { request, instance }
497
+ currentRun = { request, instance, activityAwaits: new Map() }
464
498
  const parent = (request.payload as any)[payloadParentKey] as
465
- | { workflowName: string; executionId: string }
499
+ | { workflowName: string; executionId: string; detached?: boolean }
466
500
  | undefined
467
501
  return execute(workflow.payloadSchema.make(request.payload) as object, executionId).pipe(
468
502
  Effect.onExit((exit) => {
@@ -479,7 +513,9 @@ export const make = Effect.gen(function*() {
479
513
  !instance.abandoned &&
480
514
  !(suspendOnFailure && exit._tag === "Failure")
481
515
  ) {
482
- return parent ? ensureSuccess(sendResumeParent(parent, executionId)) : Effect.void
516
+ return parent && !parent.detached
517
+ ? ensureSuccess(sendResumeParent(parent, executionId))
518
+ : Effect.void
483
519
  }
484
520
  return engine.deferredResult(InterruptSignal).pipe(
485
521
  Effect.flatMap((maybeExit) => {
@@ -506,7 +542,9 @@ export const make = Effect.gen(function*() {
506
542
  activity(request: Entity.Request<any>) {
507
543
  const payload = request.payload as { name: string; attempt: number }
508
544
  const activityId = `${executionId}/${payload.name}`
509
- const instance = WorkflowEngine.WorkflowInstance.initial(workflow, executionId)
545
+ const instance = WorkflowEngine.WorkflowInstance.initial(workflow, executionId, {
546
+ awaitedDeferreds: currentRun?.activityAwaits
547
+ })
510
548
  interruptedActivities.delete(activityId)
511
549
  const entry = activities.get(activityId)
512
550
  if (!entry) {
@@ -546,18 +584,24 @@ export const make = Effect.gen(function*() {
546
584
  deferred: (request: Entity.Request<any>) => {
547
585
  const payload = request.payload as any
548
586
  pending.results.set(payload.name, payload.exit)
587
+ const mode: WakeMode = payload.name === InterruptSignal.name ? "explicit" : "automatic"
549
588
  // Await registers before reading the cache. If this run has not
550
589
  // registered yet, a later await will see the result just stored.
551
590
  // Check when the wake runs and skip preemption too: interrupting
552
591
  // a later await would require waiting for its reply before reset.
553
592
  // A run completing its own deferred also reads it from the cache
554
- // when it awaits later, so that completion never wakes the run.
593
+ // when it awaits later, unless an activity is already parked on it.
555
594
  const wake = Effect.suspend(() =>
556
595
  currentRun && !currentRun.instance.awaitedDeferreds.has(payload.name)
557
- ? currentRun.instance.completedDeferreds.has(payload.name)
596
+ ? currentRun.activityAwaits.has(payload.name)
597
+ // Hold an activity wake until the run has published its reply.
598
+ ? resumeCurrentRun(mode)
599
+ : currentRun.instance.completedDeferreds.has(payload.name)
558
600
  ? Effect.void
559
- : ensureSuccess(resume(workflow, executionId))
560
- : deferredState.deferredDone(executionId, payload.name).pipe(Effect.andThen(resumeCurrentRun))
601
+ : ensureSuccess(resume(workflow, executionId, mode))
602
+ : deferredState.deferredDone(executionId, payload.name).pipe(
603
+ Effect.andThen(resumeCurrentRun(mode))
604
+ )
561
605
  )
562
606
  // An asynchronous reply releases the RPC concurrency permit while
563
607
  // the wake waits; the pending request still occupies mailbox capacity.
@@ -572,7 +616,7 @@ export const make = Effect.gen(function*() {
572
616
  )
573
617
  },
574
618
 
575
- resume: () => resumeCurrentRun.pipe(Rpc.wrap({ fork: true, uninterruptible: true }))
619
+ resume: () => resumeCurrentRun("automatic").pipe(Rpc.wrap({ fork: true, uninterruptible: true }))
576
620
  }
577
621
  }),
578
622
  // Alongside the run, let deferred handlers briefly acquire a permit to
@@ -589,7 +633,7 @@ export const make = Effect.gen(function*() {
589
633
  ).pipe(Effect.andThen(registration))
590
634
  }),
591
635
 
592
- execute: (workflow, { discard, executionId, parent, payload }) => {
636
+ execute: (workflow, { detached, discard, executionId, parent, payload }) => {
593
637
  ensureEntity(workflow)
594
638
  return RcMap.get(clients, workflow._tag).pipe(
595
639
  Effect.flatMap((make) =>
@@ -597,7 +641,10 @@ export const make = Effect.gen(function*() {
597
641
  parent ?
598
642
  {
599
643
  ...payload,
600
- [payloadParentKey]: { workflowName: parent.workflow._tag, executionId: parent.executionId }
644
+ // A detached child keeps its parent for observability only.
645
+ [payloadParentKey]: detached
646
+ ? { workflowName: parent.workflow._tag, executionId: parent.executionId, detached: true }
647
+ : { workflowName: parent.workflow._tag, executionId: parent.executionId }
601
648
  } :
602
649
  payload,
603
650
  { discard }
@@ -649,7 +696,7 @@ export const make = Effect.gen(function*() {
649
696
  ))
650
697
  }),
651
698
 
652
- resume: (workflow, executionId) => ensureSuccess(resume(workflow, executionId)),
699
+ resume: (workflow, executionId) => ensureSuccess(resume(workflow, executionId, "explicit")),
653
700
 
654
701
  awaitComplete,
655
702
 
@@ -659,6 +706,21 @@ export const make = Effect.gen(function*() {
659
706
  const instance = Context.get(services, WorkflowEngine.WorkflowInstance)
660
707
  yield* Effect.annotateCurrentSpan("executionId", instance.executionId)
661
708
  const activityId = `${instance.executionId}/${activity.name}`
709
+ // A pending interrupt replays only to rebuild compensations, so it must
710
+ // not run new activities. Finalizers run once `interrupted` is set.
711
+ if (!instance.interrupted && (yield* interruptPending(instance))) {
712
+ const reply = yield* Effect.orDie(requestReply({
713
+ workflow: instance.workflow,
714
+ entityType: `Workflow/${instance.workflow._tag}`,
715
+ executionId: instance.executionId,
716
+ tag: "activity",
717
+ id: activityPrimaryKey(activity.name, attempt)
718
+ }))
719
+ // Suspend so `onExit` applies the interrupt to the run.
720
+ if (Option.isNone(reply) || isSuspendedReply(reply.value)) {
721
+ return new Workflow.Suspended({})
722
+ }
723
+ }
662
724
  const client = (yield* RcMap.get(clientsPartial, instance.workflow._tag))(instance.executionId)
663
725
  while (true) {
664
726
  if (!activities.has(activityId)) {
@@ -845,9 +907,14 @@ const DeferredRpc = Rpc.make("deferred", {
845
907
  .annotate(ClusterSchema.Persisted, true)
846
908
  .annotate(ClusterSchema.Uninterruptible, true)
847
909
 
910
+ type WakeMode = "explicit" | "automatic"
911
+
848
912
  const isSuspendedReply = (reply: WithExitEncoded<Workflow.ResultEncoded<any, any>>) =>
849
913
  reply.exit._tag === "Success" && reply.exit.value._tag === "Suspended"
850
914
 
915
+ const isFailedReply = (reply: WithExitEncoded<Workflow.ResultEncoded<any, any>>) =>
916
+ reply.exit._tag === "Success" && reply.exit.value._tag === "Suspended" && reply.exit.value.failed === true
917
+
851
918
  const decodeDeferredWithExit = Schema.decodeSync(Schema.toCodecJson(Reply.WithExit.schema(DeferredRpc)))
852
919
 
853
920
  const ResumeRpc = Rpc.make("resume", {
@@ -875,7 +942,8 @@ const makeWorkflowEntity = (workflow: Workflow.Any) =>
875
942
  ...workflow.payloadSchema.fields,
876
943
  [payloadParentKey]: Schema.optional(Schema.Struct({
877
944
  workflowName: Schema.String,
878
- executionId: Schema.String
945
+ executionId: Schema.String,
946
+ detached: Schema.optional(Schema.Boolean)
879
947
  }))
880
948
  },
881
949
  primaryKey: () => "",
@@ -1086,8 +1086,8 @@ export class MemoryDriver extends Context.Service<MemoryDriver>()("effect/cluste
1086
1086
  journal.push(envelope)
1087
1087
  return SaveResultEncoded.Success()
1088
1088
  }),
1089
- saveReply: (reply_) =>
1090
- Effect.sync(() => {
1089
+ saveReply: (reply_) => {
1090
+ const save = Effect.sync(() => {
1091
1091
  const reply = JSON.parse(JSON.stringify(reply_)) as Reply.Encoded
1092
1092
  const entry = requests.get(reply.requestId)
1093
1093
  if (!entry || replyIds.has(reply.id)) return
@@ -1098,7 +1098,18 @@ export class MemoryDriver extends Context.Service<MemoryDriver>()("effect/cluste
1098
1098
  entry.replies.push(reply)
1099
1099
  replyIds.add(reply.id)
1100
1100
  replyLatch.openUnsafe()
1101
- }),
1101
+ })
1102
+ // Memory has no transaction isolation. Publish terminal replies at the
1103
+ // outer commit boundary, before their queued handler notifications, so
1104
+ // reply readers cannot deliver a success that may still be rolled back.
1105
+ return TransactionNotifications.use((notifications) =>
1106
+ reply_._tag === "WithExit" && notifications
1107
+ ? Effect.sync(() => {
1108
+ notifications.push(save)
1109
+ })
1110
+ : save
1111
+ )
1112
+ },
1102
1113
  clearReplies: (id, options) =>
1103
1114
  Effect.sync(() => {
1104
1115
  const entry = requests.get(String(id))
@@ -14,6 +14,7 @@
14
14
  import * as Context from "../Context.ts"
15
15
  import * as Effect from "../Effect.ts"
16
16
  import * as Exit from "../Exit.ts"
17
+ import * as Fiber from "../Fiber.ts"
17
18
  import * as Latch from "../Latch.ts"
18
19
  import * as Layer from "../Layer.ts"
19
20
  import * as Option from "../Option.ts"
@@ -247,78 +248,93 @@ export const make: (
247
248
 
248
249
  type StorageRequestEntry = {
249
250
  readonly latch: Latch.Latch
250
- doneLatch: Latch.Latch | undefined
251
251
  readonly messages: Set<Message.OutgoingRequest<any>>
252
+ readonly pump: Fiber.Fiber<void>
252
253
  replies: Array<Reply.Reply<any>>
253
254
  }
254
255
  const storageRequests = new Map<Snowflake.Snowflake, StorageRequestEntry>()
255
256
  const waitingStorageRequests = new Map<Snowflake.Snowflake, Message.OutgoingRequest<any>>()
256
- const replyFromStorage = Effect.fnUntraced(
257
- function*(message: Message.OutgoingRequest<any>) {
258
- let entry = storageRequests.get(message.envelope.requestId)
259
- if (entry) {
260
- entry.messages.add(message)
261
- entry.doneLatch ??= Latch.makeUnsafe(false)
262
- return yield* entry.doneLatch.await
263
- } else {
264
- entry = {
265
- latch: Latch.makeUnsafe(false),
266
- doneLatch: undefined,
267
- replies: [],
268
- messages: new Set([message])
269
- }
270
- storageRequests.set(message.envelope.requestId, entry)
271
- }
272
257
 
273
- while (true) {
274
- // wait for the storage loop to notify us
275
- entry.latch.closeUnsafe()
276
- waitingStorageRequests.set(message.envelope.requestId, message)
277
- storageLatch.openUnsafe()
278
- yield* entry.latch.await
279
-
280
- // send the replies back
281
- for (let i = 0; i < entry.replies.length; i++) {
282
- const reply = entry.replies[i]
283
- // we have reached the end
284
- if (reply._tag === "WithExit") {
285
- // A reply can immediately reset and resend the same request. Do not
286
- // let that sender attach to this completed reply loop.
287
- storageRequests.delete(message.envelope.requestId)
288
- waitingStorageRequests.delete(message.envelope.requestId)
289
- for (const message of entry.messages) {
290
- yield* message.respond(reply)
291
- }
292
- entry.doneLatch?.openUnsafe()
293
- return
294
- }
258
+ // A reset request can register a replacement entry under the same id, which
259
+ // must survive the old entry's cleanup.
260
+ const removeStorageRequest = (requestId: Snowflake.Snowflake, entry: Omit<StorageRequestEntry, "pump">) => {
261
+ if (storageRequests.get(requestId) !== entry) return
262
+ storageRequests.delete(requestId)
263
+ waitingStorageRequests.delete(requestId)
264
+ }
295
265
 
296
- entry.latch.closeUnsafe()
266
+ // The pump is owned by the shared entry rather than the first caller, so any
267
+ // waiter can be interrupted without stopping delivery to the others.
268
+ const pumpReplies = Effect.fnUntraced(function*(
269
+ requestId: Snowflake.Snowflake,
270
+ entry: Omit<StorageRequestEntry, "pump">
271
+ ) {
272
+ while (true) {
273
+ // wait for the storage loop to notify us
274
+ entry.latch.closeUnsafe()
275
+ const message = entry.messages.values().next().value
276
+ if (!message) return
277
+ waitingStorageRequests.set(requestId, message)
278
+ storageLatch.openUnsafe()
279
+ yield* entry.latch.await
280
+
281
+ // send the replies back
282
+ for (let i = 0; i < entry.replies.length; i++) {
283
+ const reply = entry.replies[i]
284
+ // we have reached the end
285
+ if (reply._tag === "WithExit") {
286
+ // A reply can immediately reset and resend the same request. Do not
287
+ // let that sender attach to this completed entry.
288
+ removeStorageRequest(requestId, entry)
297
289
  for (const message of entry.messages) {
298
290
  yield* message.respond(reply)
299
291
  }
300
- // wait for ack
301
- yield* entry.latch.await
292
+ return
302
293
  }
303
- entry.replies = []
294
+
295
+ entry.latch.closeUnsafe()
296
+ for (const message of entry.messages) {
297
+ yield* message.respond(reply)
298
+ }
299
+ // wait for ack
300
+ yield* entry.latch.await
304
301
  }
305
- },
306
- (effect, message) =>
307
- Effect.ensuring(
308
- effect,
309
- Effect.sync(() => {
310
- const entry = storageRequests.get(message.envelope.requestId)
311
- // An old waiter must not clean up a reset request's new reply loop.
312
- if (!entry?.messages.has(message)) return
313
- if (entry.messages.size > 1) {
314
- entry.messages.delete(message)
315
- return
302
+ entry.replies = []
303
+ }
304
+ })
305
+
306
+ const replyFromStorage = (message: Message.OutgoingRequest<any>) =>
307
+ Effect.uninterruptibleMask((restore) =>
308
+ Effect.gen(function*() {
309
+ const requestId = message.envelope.requestId
310
+ let entry = storageRequests.get(requestId)
311
+ if (!entry) {
312
+ const state = {
313
+ latch: Latch.makeUnsafe(false),
314
+ messages: new Set<Message.OutgoingRequest<any>>(),
315
+ replies: [] as Array<Reply.Reply<any>>
316
316
  }
317
- storageRequests.delete(message.envelope.requestId)
318
- waitingStorageRequests.delete(message.envelope.requestId)
319
- })
320
- )
321
- )
317
+ entry = Object.assign(state, { pump: yield* Effect.forkIn(pumpReplies(requestId, state), runnersScope) })
318
+ storageRequests.set(requestId, entry)
319
+ }
320
+ const current = entry
321
+ current.messages.add(message)
322
+ return yield* restore(Fiber.join(current.pump)).pipe(
323
+ Effect.ensuring(Effect.suspend(() => {
324
+ current.messages.delete(message)
325
+ if (current.messages.size === 0) {
326
+ removeStorageRequest(requestId, current)
327
+ return Fiber.interrupt(current.pump)
328
+ }
329
+ // The storage loop decodes replies using the waiting message.
330
+ if (waitingStorageRequests.get(requestId) === message) {
331
+ waitingStorageRequests.set(requestId, current.messages.values().next().value!)
332
+ }
333
+ return Effect.void
334
+ }))
335
+ )
336
+ })
337
+ )
322
338
 
323
339
  const storageLatch = Latch.makeUnsafe(false)
324
340
  if (storage !== MessageStorage.noop) {
@@ -415,22 +431,28 @@ export const make: (
415
431
  })
416
432
  },
417
433
  notifyLocal(options) {
418
- return notifyWith(options.message, (message, duplicate) => {
434
+ return notifyWith(options.message, (message) => {
419
435
  if (options.discard || message._tag === "OutgoingEnvelope") {
420
436
  return Effect.catchTag(
421
437
  options.notify(Message.incomingLocalFromOutgoing(message)),
422
438
  "EntityNotAssignedToRunner",
423
439
  () => Effect.void
424
440
  )
425
- } else if (!duplicate && options.storageOnly !== true) {
426
- return options.notify(Message.incomingLocalFromOutgoing(message)).pipe(
427
- Effect.andThen(storage.registerReplyHandler(message)),
428
- Effect.catchTag("EntityNotAssignedToRunner", () => replyFromStorage(message))
429
- )
430
441
  }
442
+ // The request can be processed as soon as it is saved, so replies are
443
+ // always read back from storage. The local reply handler only wakes the
444
+ // storage loop, so streams do not have to wait for the next poll.
445
+ const readReplies = options.storageOnly
446
+ ? replyFromStorage(message)
447
+ : Effect.raceFirst(
448
+ replyFromStorage(message),
449
+ storage.registerReplyHandler(
450
+ new Message.OutgoingRequest({ ...message, respond: () => storageLatch.open })
451
+ ).pipe(Effect.ignore, Effect.andThen(Effect.never))
452
+ )
431
453
  return options.notify(Message.incomingLocalFromOutgoing(message)).pipe(
432
454
  Effect.catchTag("EntityNotAssignedToRunner", () => Effect.void),
433
- Effect.andThen(replyFromStorage(message))
455
+ Effect.andThen(readReplies)
434
456
  )
435
457
  })
436
458
  }
@@ -250,7 +250,7 @@ export const make = Effect.fnUntraced(function*<
250
250
  ) as Effect.Effect<Context.Context<Rpc.ToHandler<Rpcs>>>)
251
251
 
252
252
  const server = yield* RpcServer.makeNoSerialization(entity.protocol, {
253
- spanPrefix: `${entity.type}(${address.entityId})`,
253
+ spanPrefix: entity.type,
254
254
  spanAttributes: {
255
255
  ...options.spanAttributes,
256
256
  "entity.type": entity.type,
@@ -683,7 +683,7 @@ export const encoder: Encoder = {
683
683
  if (event.event !== "message") {
684
684
  data += `event: ${event.event}\n`
685
685
  }
686
- data += `data: ${event.data.replace(/\n/g, "\ndata: ")}\n`
686
+ data += `data: ${event.data.replace(/\r\n|\r|\n/g, "\ndata: ")}\n`
687
687
  return data + "\n"
688
688
  }
689
689
  case "Retry": {