effect 4.0.0-rc.118 → 4.0.0

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 (132) hide show
  1. package/README.md +17 -4
  2. package/dist/Array.d.ts +25 -25
  3. package/dist/Array.d.ts.map +1 -1
  4. package/dist/Array.js +16 -16
  5. package/dist/Array.js.map +1 -1
  6. package/dist/Chunk.d.ts +26 -26
  7. package/dist/Chunk.d.ts.map +1 -1
  8. package/dist/Chunk.js +15 -15
  9. package/dist/Chunk.js.map +1 -1
  10. package/dist/Effect.d.ts +17 -17
  11. package/dist/Effect.d.ts.map +1 -1
  12. package/dist/Effect.js +5 -5
  13. package/dist/MutableHashMap.d.ts.map +1 -1
  14. package/dist/MutableHashMap.js +4 -22
  15. package/dist/MutableHashMap.js.map +1 -1
  16. package/dist/Option.d.ts +17 -17
  17. package/dist/Option.d.ts.map +1 -1
  18. package/dist/Option.js +6 -6
  19. package/dist/Option.js.map +1 -1
  20. package/dist/Queue.d.ts +19 -2
  21. package/dist/Queue.d.ts.map +1 -1
  22. package/dist/Queue.js +9 -4
  23. package/dist/Queue.js.map +1 -1
  24. package/dist/Record.d.ts +14 -14
  25. package/dist/Record.d.ts.map +1 -1
  26. package/dist/Record.js +10 -10
  27. package/dist/Record.js.map +1 -1
  28. package/dist/Schema.d.ts +37 -21
  29. package/dist/Schema.d.ts.map +1 -1
  30. package/dist/Schema.js +29 -10
  31. package/dist/Schema.js.map +1 -1
  32. package/dist/SchemaAST.d.ts.map +1 -1
  33. package/dist/SchemaAST.js +116 -97
  34. package/dist/SchemaAST.js.map +1 -1
  35. package/dist/SchemaRepresentation.d.ts +3 -1
  36. package/dist/SchemaRepresentation.d.ts.map +1 -1
  37. package/dist/SchemaRepresentation.js +3 -1
  38. package/dist/SchemaRepresentation.js.map +1 -1
  39. package/dist/ai/McpSchema.d.ts +110 -110
  40. package/dist/ai/McpSchema.d.ts.map +1 -1
  41. package/dist/cluster/ClusterWorkflowEngine.d.ts.map +1 -1
  42. package/dist/cluster/ClusterWorkflowEngine.js +5 -1
  43. package/dist/cluster/ClusterWorkflowEngine.js.map +1 -1
  44. package/dist/cluster/MessageStorage.d.ts +32 -8
  45. package/dist/cluster/MessageStorage.d.ts.map +1 -1
  46. package/dist/cluster/MessageStorage.js +2 -1
  47. package/dist/cluster/MessageStorage.js.map +1 -1
  48. package/dist/cluster/Sharding.d.ts +5 -2
  49. package/dist/cluster/Sharding.d.ts.map +1 -1
  50. package/dist/cluster/Sharding.js +1 -1
  51. package/dist/cluster/Sharding.js.map +1 -1
  52. package/dist/cluster/SqlMessageStorage.d.ts.map +1 -1
  53. package/dist/cluster/SqlMessageStorage.js +15 -4
  54. package/dist/cluster/SqlMessageStorage.js.map +1 -1
  55. package/dist/http/HttpEffect.d.ts +1 -0
  56. package/dist/http/HttpEffect.d.ts.map +1 -1
  57. package/dist/http/HttpEffect.js +29 -22
  58. package/dist/http/HttpEffect.js.map +1 -1
  59. package/dist/internal/effect.js +75 -84
  60. package/dist/internal/effect.js.map +1 -1
  61. package/dist/internal/schema/annotations.js +1 -7
  62. package/dist/internal/schema/annotations.js.map +1 -1
  63. package/dist/internal/schema/codegen.js +56 -28
  64. package/dist/internal/schema/codegen.js.map +1 -1
  65. package/dist/internal/schema/toCodeDocument.js +10 -30
  66. package/dist/internal/schema/toCodeDocument.js.map +1 -1
  67. package/dist/internal/schema/toCodec.js +1 -15
  68. package/dist/internal/schema/toCodec.js.map +1 -1
  69. package/dist/internal/schema/toEquivalence.js +4 -3
  70. package/dist/internal/schema/toEquivalence.js.map +1 -1
  71. package/dist/internal/schema/toFormatter.js +4 -4
  72. package/dist/internal/schema/toFormatter.js.map +1 -1
  73. package/dist/reactivity/Atom.d.ts.map +1 -1
  74. package/dist/reactivity/Atom.js +6 -2
  75. package/dist/reactivity/Atom.js.map +1 -1
  76. package/dist/reactivity/AtomRpc.d.ts +3 -3
  77. package/dist/reactivity/AtomRpc.d.ts.map +1 -1
  78. package/dist/rpc/Rpc.d.ts +2 -2
  79. package/dist/rpc/Rpc.d.ts.map +1 -1
  80. package/dist/rpc/RpcClient.d.ts +7 -7
  81. package/dist/rpc/RpcClient.d.ts.map +1 -1
  82. package/dist/rpc/RpcClient.js +1 -2
  83. package/dist/rpc/RpcClient.js.map +1 -1
  84. package/dist/rpc/RpcMessage.d.ts +1 -1
  85. package/dist/rpc/RpcMessage.d.ts.map +1 -1
  86. package/dist/schema/SchemaCompiler/runtime.js +6 -1
  87. package/dist/schema/SchemaCompiler/runtime.js.map +1 -1
  88. package/dist/testing/TestSchema.d.ts +234 -6
  89. package/dist/testing/TestSchema.d.ts.map +1 -1
  90. package/dist/testing/TestSchema.js +244 -25
  91. package/dist/testing/TestSchema.js.map +1 -1
  92. package/dist/testing/index.d.ts +1 -0
  93. package/dist/testing/index.d.ts.map +1 -1
  94. package/dist/testing/index.js +1 -0
  95. package/dist/testing/index.js.map +1 -1
  96. package/dist/workflow/DurableDeferred.js +1 -1
  97. package/dist/workflow/DurableDeferred.js.map +1 -1
  98. package/dist/workflow/Workflow.js +1 -1
  99. package/dist/workflow/Workflow.js.map +1 -1
  100. package/package.json +1 -1
  101. package/src/Array.ts +31 -31
  102. package/src/Chunk.ts +29 -29
  103. package/src/Effect.ts +17 -17
  104. package/src/MutableHashMap.ts +4 -24
  105. package/src/Option.ts +19 -19
  106. package/src/Queue.ts +26 -6
  107. package/src/Record.ts +20 -20
  108. package/src/Schema.ts +48 -24
  109. package/src/SchemaAST.ts +135 -115
  110. package/src/SchemaRepresentation.ts +3 -1
  111. package/src/cluster/ClusterWorkflowEngine.ts +8 -1
  112. package/src/cluster/MessageStorage.ts +22 -5
  113. package/src/cluster/Sharding.ts +8 -4
  114. package/src/cluster/SqlMessageStorage.ts +19 -8
  115. package/src/http/HttpEffect.ts +15 -9
  116. package/src/internal/effect.ts +37 -62
  117. package/src/internal/schema/annotations.ts +0 -9
  118. package/src/internal/schema/codegen.ts +90 -43
  119. package/src/internal/schema/toCodeDocument.ts +10 -47
  120. package/src/internal/schema/toCodec.ts +3 -16
  121. package/src/internal/schema/toEquivalence.ts +4 -5
  122. package/src/internal/schema/toFormatter.ts +4 -6
  123. package/src/reactivity/Atom.ts +6 -2
  124. package/src/reactivity/AtomRpc.ts +5 -5
  125. package/src/rpc/Rpc.ts +2 -2
  126. package/src/rpc/RpcClient.ts +9 -10
  127. package/src/rpc/RpcMessage.ts +1 -1
  128. package/src/schema/SchemaCompiler/runtime.ts +17 -1
  129. package/src/testing/TestSchema.ts +315 -63
  130. package/src/testing/index.ts +1 -0
  131. package/src/workflow/DurableDeferred.ts +1 -1
  132. package/src/workflow/Workflow.ts +1 -1
package/src/SchemaAST.ts CHANGED
@@ -3394,7 +3394,8 @@ export function collectSentinels(ast: AST): ReadonlyArray<Sentinel> {
3394
3394
  }
3395
3395
  }
3396
3396
 
3397
- type CandidateIndex = (input: any, isConstructor: boolean) => ReadonlyArray<AST>
3397
+ /** @internal */
3398
+ export type CandidateIndex = (input: any, isConstructor: boolean) => ReadonlyArray<number>
3398
3399
  type SentinelEntry = readonly [
3399
3400
  byValue: Map<LiteralValue | symbol, Set<number>>,
3400
3401
  all: Set<number>
@@ -3409,15 +3410,17 @@ const getRuntimeType = (input: unknown): Type => input === null ? "null" : Array
3409
3410
  const hasPropertySignature = (input: object, key: PropertyKey): boolean =>
3410
3411
  key === "__proto__" ? Object.hasOwn(input, key) : key in input
3411
3412
 
3412
- function getIndex(types: ReadonlyArray<AST>): CandidateIndex {
3413
+ /** @internal */
3414
+ export function getCandidateIndex(types: ReadonlyArray<AST>): CandidateIndex {
3413
3415
  let index = candidateIndexCache.get(types)
3414
3416
  if (index) return index
3415
3417
 
3416
3418
  let bySentinel: SentinelIndex | undefined
3417
3419
  let sentinelCandidateCount = 0
3418
3420
  let otherwise: { [K in Type]?: Array<number> } | undefined
3419
- let literalCandidates: Map<LiteralValue | symbol, Array<AST>> | undefined
3421
+ let literalCandidates: Map<LiteralValue | symbol, Array<number>> | undefined
3420
3422
  let onlyLiterals = true
3423
+ const literalOf: Array<LiteralValue | symbol | undefined> = []
3421
3424
  for (let i = 0; i < types.length; i++) {
3422
3425
  const a = types[i]
3423
3426
  const encoded = toCandidate(a)
@@ -3426,9 +3429,10 @@ function getIndex(types: ReadonlyArray<AST>): CandidateIndex {
3426
3429
  if (isLiteral(encoded) || isUniqueSymbol(encoded)) {
3427
3430
  literalCandidates ??= new Map()
3428
3431
  const literal = isLiteral(encoded) ? encoded.literal : encoded.symbol
3432
+ literalOf[i] = literal
3429
3433
  let arr = literalCandidates.get(literal)
3430
3434
  if (!arr) literalCandidates.set(literal, arr = [])
3431
- arr.push(a)
3435
+ arr.push(i)
3432
3436
  } else {
3433
3437
  onlyLiterals = false
3434
3438
  }
@@ -3454,24 +3458,25 @@ function getIndex(types: ReadonlyArray<AST>): CandidateIndex {
3454
3458
  }
3455
3459
 
3456
3460
  // Non-discriminated members are grouped by runtime type once and reused by every decode.
3457
- const fallbacks: { [K in Type]?: ReadonlyArray<AST> } = {}
3458
- const getFallback = (type: Type): ReadonlyArray<AST> =>
3459
- fallbacks[type] ??= Object.freeze((otherwise?.[type] ?? emptyCandidates).map((i) => types[i]))
3461
+ const fallbacks: { [K in Type]?: ReadonlyArray<number> } = {}
3462
+ const getFallback = (type: Type): ReadonlyArray<number> =>
3463
+ fallbacks[type] ??= Object.freeze(otherwise?.[type] ?? emptyCandidates)
3460
3464
 
3461
3465
  if (onlyLiterals && literalCandidates) {
3462
3466
  literalCandidates.forEach(Object.freeze)
3463
3467
  index = (input) => literalCandidates.get(input) ?? emptyCandidates
3464
3468
  } else if (bySentinel?.size === 1 && !otherwise) {
3465
3469
  const [key, [byValue]] = bySentinel.entries().next().value!
3466
- const candidates = byValue as unknown as Map<LiteralValue | symbol, ReadonlyArray<AST>>
3470
+ const candidates = new Map<LiteralValue | symbol, ReadonlyArray<number>>()
3467
3471
  for (const [literal, indexes] of byValue) {
3468
- candidates.set(literal, Object.freeze(Array.from(indexes, (index) => types[index])))
3472
+ candidates.set(literal, Object.freeze(Array.from(indexes)))
3469
3473
  }
3474
+ const all = Object.freeze(types.map((_, i) => i))
3470
3475
  index = (input, isConstructor) => {
3471
3476
  if (Predicate.isObjectKeyword(input)) {
3472
3477
  const value = hasPropertySignature(input, key) ? (input as any)[key] : undefined
3473
3478
  if (value !== undefined) return candidates.get(value) ?? emptyCandidates
3474
- if (isConstructor) return types
3479
+ if (isConstructor) return all
3475
3480
  }
3476
3481
  return emptyCandidates
3477
3482
  }
@@ -3538,12 +3543,12 @@ function getIndex(types: ReadonlyArray<AST>): CandidateIndex {
3538
3543
  }
3539
3544
  }
3540
3545
  }
3541
- return Array.from(selected).sort((a, b) => a - b).map((i) => types[i])
3546
+ return Array.from(selected).sort((a, b) => a - b)
3542
3547
  }
3543
3548
  } else {
3544
3549
  index = (input) => {
3545
3550
  const fallback = getFallback(getRuntimeType(input))
3546
- return literalCandidates ? fallback.filter(filterLiterals(input)) : fallback
3551
+ return literalCandidates ? fallback.filter((i) => literalOf[i] === undefined || literalOf[i] === input) : fallback
3547
3552
  }
3548
3553
  }
3549
3554
 
@@ -3551,31 +3556,6 @@ function getIndex(types: ReadonlyArray<AST>): CandidateIndex {
3551
3556
  return index
3552
3557
  }
3553
3558
 
3554
- function filterLiterals(input: any) {
3555
- return (ast: AST) => {
3556
- const encoded = toCandidate(ast)
3557
- return encoded._tag === "Literal" ?
3558
- encoded.literal === input
3559
- : encoded._tag === "UniqueSymbol" ?
3560
- encoded.symbol === input
3561
- : true
3562
- }
3563
- }
3564
-
3565
- /**
3566
- * The goal is to reduce the number of a union members that will be checked.
3567
- * This is useful to reduce the number of issues that will be returned.
3568
- *
3569
- * @internal
3570
- */
3571
- export function getCandidates(
3572
- input: any,
3573
- types: ReadonlyArray<AST>,
3574
- isConstructor = false
3575
- ): ReadonlyArray<AST> {
3576
- return getIndex(types)(input, isConstructor)
3577
- }
3578
-
3579
3559
  /**
3580
3560
  * AST node representing a union of schemas.
3581
3561
  *
@@ -3680,43 +3660,28 @@ export const Union: new<A extends AST = AST>(
3680
3660
  ): SchemaParser.Parser {
3681
3661
  // oxlint-disable-next-line @typescript-eslint/no-this-alias
3682
3662
  const ast = this
3663
+ const isConstructor = compileField !== undefined
3664
+ const parsers: Array<SchemaParser.Parser> = []
3665
+ const parser = (i: number): SchemaParser.Parser => parsers[i] ??= compile(ast.types[i])
3666
+ let index: CandidateIndex | undefined
3683
3667
 
3684
3668
  return (input, options) => {
3685
3669
  if (input === InternalParser.missing) {
3686
3670
  return InternalParser.missingExit
3687
3671
  }
3688
- const candidates = getCandidates(input, ast.types, compileField !== undefined)
3672
+ const candidates = (index ??= getCandidateIndex(ast.types))(input, isConstructor)
3689
3673
 
3690
3674
  if (candidates.length === 0) {
3691
3675
  return Effect.fail(new SchemaIssue.AnyOf(ast, [], input, options))
3692
3676
  }
3693
3677
  if (candidates.length === 1) {
3694
- const result = compile(candidates[0])(input, options)
3678
+ const result = parser(candidates[0])(input, options)
3695
3679
  if ((result as Exit.Exit<unknown, SchemaIssue.Issue>)._tag === "Success") return result
3696
3680
  return effectIsExit(result)
3697
3681
  ? failSingleUnionCandidate(ast, (result as Exit.Failure<unknown, SchemaIssue.Issue>).cause, input, options)
3698
- : Effect.catchCause(result, (cause) => failSingleUnionCandidate(ast, cause, input, options))
3699
- }
3700
-
3701
- const state = {
3702
- ast,
3703
- compile,
3704
- input,
3705
- out: undefined,
3706
- successes: ast.options?.mode === "oneOf" ? [] : undefined,
3707
- issues: undefined as Arr.NonEmptyArray<SchemaIssue.Issue> | undefined,
3708
- options
3709
- }
3710
- const eff = parseUnion(state, candidates)
3711
- if (!eff) {
3712
- if (state.out) return state.out
3713
- return Effect.fail(new SchemaIssue.AnyOf(ast, state.issues ?? [], input, options))
3682
+ : catchSingleUnionCandidate(ast, result, input, options)
3714
3683
  }
3715
- return Effect.flatMapEager(eff, (_) => {
3716
- if (state.out === InternalParser.sameExit) return Effect.succeed(input)
3717
- if (state.out) return state.out
3718
- return Effect.fail(new SchemaIssue.AnyOf(ast, state.issues ?? [], input, options))
3719
- })
3684
+ return parseUnionCandidates(ast, parser, candidates, input, options)
3720
3685
  }
3721
3686
  }
3722
3687
  private _rebuild(
@@ -3795,20 +3760,65 @@ function failSingleUnionCandidate(
3795
3760
  return Exit.fail(new SchemaIssue.AnyOf(ast, [issue], input, options))
3796
3761
  }
3797
3762
 
3798
- const parseUnion = iterateEager<{
3799
- readonly compile: (ast: AST) => SchemaParser.Parser
3763
+ function catchSingleUnionCandidate(
3764
+ ast: Union,
3765
+ result: Effect.Effect<unknown, SchemaIssue.Issue, unknown>,
3766
+ input: unknown,
3767
+ options: ParseOptions
3768
+ ) {
3769
+ return Effect.catchCause(result, (cause) => failSingleUnionCandidate(ast, cause, input, options))
3770
+ }
3771
+
3772
+ type UnionParserState = {
3773
+ readonly parser: (i: number) => SchemaParser.Parser
3800
3774
  readonly ast: Union
3801
3775
  readonly input: unknown
3802
3776
  readonly options: ParseOptions
3803
3777
  out: Exit.Success<unknown, SchemaIssue.Issue> | undefined
3804
3778
  readonly successes: Array<AST> | undefined
3805
3779
  issues: Array<SchemaIssue.Issue> | undefined
3806
- }, AST>()({
3807
- onItem(s, ast) {
3808
- const parser = s.compile(ast)
3809
- return parser(s.input, s.options)
3780
+ }
3781
+
3782
+ function parseUnionCandidates(
3783
+ ast: Union,
3784
+ parser: (i: number) => SchemaParser.Parser,
3785
+ candidates: ReadonlyArray<number>,
3786
+ input: unknown,
3787
+ options: ParseOptions
3788
+ ): Effect.Effect<unknown, SchemaIssue.Issue, any> {
3789
+ const state: UnionParserState = {
3790
+ ast,
3791
+ parser,
3792
+ input,
3793
+ out: undefined,
3794
+ successes: ast.options?.mode === "oneOf" ? [] : undefined,
3795
+ issues: undefined,
3796
+ options
3797
+ }
3798
+ const eff = parseUnion(state, candidates)
3799
+ if (!eff) {
3800
+ if (state.out) return state.out
3801
+ return Effect.fail(new SchemaIssue.AnyOf(ast, state.issues ?? [], input, options))
3802
+ }
3803
+ return resumeUnion(eff, state)
3804
+ }
3805
+
3806
+ function resumeUnion(
3807
+ eff: Effect.Effect<void, SchemaIssue.Issue, any>,
3808
+ state: UnionParserState
3809
+ ): Effect.Effect<unknown, SchemaIssue.Issue, any> {
3810
+ return Effect.flatMapEager(eff, (_) => {
3811
+ if (state.out === InternalParser.sameExit) return Effect.succeed(state.input)
3812
+ if (state.out) return state.out
3813
+ return Effect.fail(new SchemaIssue.AnyOf(state.ast, state.issues ?? [], state.input, state.options))
3814
+ })
3815
+ }
3816
+
3817
+ const parseUnion = iterateEager<UnionParserState, number>()({
3818
+ onItem(s, i) {
3819
+ return s.parser(i)(s.input, s.options)
3810
3820
  },
3811
- step(s, candidate, exit) {
3821
+ step(s, i, exit) {
3812
3822
  if (exit._tag === "Failure") {
3813
3823
  const issue = InternalSchemaCause.getSchemaIssue(exit.cause)
3814
3824
  if (issue === undefined) {
@@ -3818,12 +3828,12 @@ const parseUnion = iterateEager<{
3818
3828
  else s.issues = [issue]
3819
3829
  } else {
3820
3830
  if (s.out && s.successes) {
3821
- s.successes.push(candidate)
3831
+ s.successes.push(s.ast.types[i])
3822
3832
  return Exit.fail(new SchemaIssue.OneOf(s.ast, s.successes, s.input, s.options))
3823
3833
  }
3824
3834
  s.out = exit
3825
3835
  if (s.successes) {
3826
- s.successes.push(candidate)
3836
+ s.successes.push(s.ast.types[i])
3827
3837
  } else {
3828
3838
  return Exit.void
3829
3839
  }
@@ -4220,26 +4230,33 @@ export function isPattern(regExp: globalThis.RegExp, annotations?: Schema.Annota
4220
4230
  )
4221
4231
  }
4222
4232
 
4223
- function copy<A extends AST>(ast: A): Types.Mutable<A> {
4233
+ // Copies that update only context or encoding share a body.
4234
+ // All other fields describe the body itself, including child contexts.
4235
+ const bodyOwners = new WeakMap<AST, AST>()
4236
+
4237
+ function copy<A extends AST>(ast: A, changes: Partial<AST>): A {
4224
4238
  // AST copies preserve the prototype and enumerable values, not property descriptors.
4225
- return Object.assign(Object.create(Object.getPrototypeOf(ast)), ast)
4239
+ const out = Object.assign(Object.create(Object.getPrototypeOf(ast)), ast, changes) as A
4240
+ if (Reflect.ownKeys(changes).every((key) => key === "context" || key === "encoding")) {
4241
+ bodyOwners.set(out, getContextOwner(ast))
4242
+ }
4243
+ return out
4226
4244
  }
4227
4245
 
4228
- const contextOwners = new WeakMap<AST, AST>()
4229
-
4230
4246
  /** @internal */
4231
- export function getContextOwner(ast: AST): AST {
4232
- return contextOwners.get(ast) ?? ast
4247
+ export function getContextOwner<A extends AST>(ast: A): A {
4248
+ const existing = bodyOwners.get(ast)
4249
+ if (existing !== undefined) return existing as A
4250
+ if (ast.encoding === undefined) return ast
4251
+ // A body representative must not expose or retain the node's own codec.
4252
+ const owner = Object.assign(Object.create(Object.getPrototypeOf(ast)), ast, { encoding: undefined }) as A
4253
+ bodyOwners.set(ast, owner)
4254
+ return owner
4233
4255
  }
4234
4256
 
4235
4257
  /** @internal */
4236
4258
  export function replaceEncoding<A extends AST>(ast: A, encoding: Encoding | undefined): A {
4237
- if (ast.encoding === encoding) {
4238
- return ast
4239
- }
4240
- const out = copy(ast)
4241
- out.encoding = encoding
4242
- return out
4259
+ return ast.encoding === encoding ? ast : copy(ast, { encoding })
4243
4260
  }
4244
4261
 
4245
4262
  /** @internal */
@@ -4248,13 +4265,10 @@ export function replaceContext<A extends AST>(ast: A, context: Context | undefin
4248
4265
  return ast
4249
4266
  }
4250
4267
  const owner = getContextOwner(ast)
4251
- if (owner.context === context) {
4252
- return owner as A
4268
+ if (owner.context === context && owner.encoding === ast.encoding) {
4269
+ return owner
4253
4270
  }
4254
- const out = copy(ast)
4255
- out.context = context
4256
- contextOwners.set(out as A, owner)
4257
- return out
4271
+ return copy(ast, { context })
4258
4272
  }
4259
4273
 
4260
4274
  /** @internal */
@@ -4268,9 +4282,7 @@ export function annotate<A extends AST>(ast: A, annotations: Schema.Annotations.
4268
4282
  const last = ast.checks[ast.checks.length - 1]
4269
4283
  return replaceChecks(ast, Arr.append(ast.checks.slice(0, -1), last.annotate(annotations)))
4270
4284
  }
4271
- const out = copy(ast)
4272
- out.annotations = { ...ast.annotations, ...annotations }
4273
- return out
4285
+ return copy(ast, { annotations: { ...ast.annotations, ...annotations } })
4274
4286
  }
4275
4287
 
4276
4288
  /** @internal */
@@ -4281,9 +4293,7 @@ export function replaceChecks<A extends AST>(ast: A, checks: Checks | undefined)
4281
4293
  if (ast.checks === checks) {
4282
4294
  return ast
4283
4295
  }
4284
- const out = copy(ast)
4285
- out.checks = checks
4286
- return out
4296
+ return copy(ast, { checks })
4287
4297
  }
4288
4298
 
4289
4299
  /** @internal */
@@ -4364,13 +4374,6 @@ function appendTransformation<A extends AST>(
4364
4374
  return replaceEncoding(to, to.encoding ? [...to.encoding, link] : [link])
4365
4375
  }
4366
4376
 
4367
- /** @internal */
4368
- export function brand(ast: AST, brand: string): AST {
4369
- const existing = InternalAnnotations.resolveBrands(ast)
4370
- const brands = existing ? [...existing, brand] : [brand]
4371
- return annotate(ast, { brands })
4372
- }
4373
-
4374
4377
  /**
4375
4378
  * Maps over the array but will return the original array if no changes occur.
4376
4379
  * @internal
@@ -4555,6 +4558,17 @@ function extractStructuralChecks(checks: Checks): Checks | undefined {
4555
4558
  return Arr.isArrayNonEmpty(out) ? out : undefined
4556
4559
  }
4557
4560
 
4561
+ function canPreserveEncodingChecks(ast: AST): boolean {
4562
+ let preserve = true
4563
+ function visit(child: AST): AST {
4564
+ preserve = preserve && !child.encoding && !isSuspend(child)
4565
+ if (preserve && "recur" in child) child.recur(visit)
4566
+ return child
4567
+ }
4568
+ if ("recur" in ast) ast.recur(visit)
4569
+ return preserve
4570
+ }
4571
+
4558
4572
  /**
4559
4573
  * Strips all encoding transformations from an AST, returning the decoded
4560
4574
  * (type-level) representation.
@@ -4581,22 +4595,24 @@ function extractStructuralChecks(checks: Checks): Checks | undefined {
4581
4595
  * @since 4.0.0
4582
4596
  */
4583
4597
  export const toType = memoizeIdempotent(<A extends AST>(ast: A): A => {
4584
- const out: any = ast
4585
- const type = out.recur?.(toType) ?? out
4586
- const encodingChecks: Checks | undefined = type.encodingChecks
4587
- if (encodingChecks) {
4588
- const checks = type === ast
4589
- ? encodingChecks
4598
+ const owner = getContextOwner(ast)
4599
+ if (owner !== ast) {
4600
+ const type = toType(owner)
4601
+ return type === owner && ast.encoding === undefined ? ast : replaceContext(type, ast.context)
4602
+ }
4603
+ const type = ("recur" in ast ? ast.recur(toType) : ast) as A
4604
+ if ("encodingChecks" in type && type.encodingChecks) {
4605
+ const checks = canPreserveEncodingChecks(ast)
4606
+ ? type.encodingChecks
4590
4607
  : isArrays(type) || isObjects(type) || isDeclaration(type) && type.typeParameters.length > 0
4591
- ? extractStructuralChecks(encodingChecks)
4608
+ ? extractStructuralChecks(type.encodingChecks)
4592
4609
  : undefined
4593
- const copyOfType = copy(type)
4594
- copyOfType.encoding = undefined
4595
- copyOfType.encodingChecks = undefined
4596
- copyOfType.checks = combineChecks(type.checks, checks)
4597
- return copyOfType
4610
+ return copy(type, {
4611
+ encodingChecks: undefined,
4612
+ checks: combineChecks(type.checks, checks)
4613
+ })
4598
4614
  }
4599
- return type.encoding ? replaceEncoding(type, undefined) : type
4615
+ return type
4600
4616
  })
4601
4617
 
4602
4618
  /**
@@ -4668,8 +4684,12 @@ export const flip = memoize((ast: AST): AST => {
4668
4684
  if (ast.encoding) {
4669
4685
  return flipEncoding(ast, ast.encoding)
4670
4686
  }
4671
- const out: any = ast
4672
- return out.flip?.(flip) ?? out.recur?.(flip) ?? out
4687
+ const owner = getContextOwner(ast)
4688
+ if (owner !== ast) {
4689
+ const flipped = flip(owner)
4690
+ return flipped === owner ? ast : replaceContext(flipped, ast.context)
4691
+ }
4692
+ return "flip" in ast ? ast.flip(flip) : "recur" in ast ? ast.recur(flip) : ast
4673
4693
  })
4674
4694
 
4675
4695
  /** @internal */
@@ -2432,7 +2432,9 @@ export interface ToRepresentationOptions {
2432
2432
  * **Details**
2433
2433
  *
2434
2434
  * Apply `SchemaAST.toType` to the AST first to lower its type side instead. The optional reference policy controls which
2435
- * candidates are moved into the document's shared reference table.
2435
+ * candidates are moved into the document's shared reference table. TypeScript-only
2436
+ * distinctions such as those added by `Schema.brand` are absent from the AST
2437
+ * and cannot be reconstructed from the resulting document.
2436
2438
  *
2437
2439
  * @see {@link toRepresentations} for multiple roots sharing one reference table
2438
2440
  *
@@ -299,7 +299,14 @@ export const make = Effect.gen(function*() {
299
299
  )
300
300
 
301
301
  if (Option.isNone(maybeSuspended)) return
302
- yield* sharding.reset(Snowflake.Snowflake(maybeSuspended.value.requestId))
302
+ if (
303
+ !(yield* sharding.reset(
304
+ Snowflake.Snowflake(maybeSuspended.value.requestId),
305
+ { expectedReplyId: Snowflake.Snowflake(maybeSuspended.value.id) }
306
+ ))
307
+ ) {
308
+ return yield* Effect.fail(new Error("Failed to reset workflow run"))
309
+ }
303
310
  yield* sharding.pollStorage
304
311
  })
305
312
 
@@ -74,9 +74,16 @@ export class MessageStorage extends Context.Service<MessageStorage, {
74
74
  ) => Effect.Effect<void, PersistenceError | MalformedMessage>
75
75
 
76
76
  /**
77
- * Clear the `Reply`s for the given request id.
77
+ * Clear replies for the given request id. When options.expectedReplyId is
78
+ * provided, clear only if it is still the latest reply at the storage boundary.
79
+ * Without it, clear unconditionally.
80
+ * Custom storage implementations must honor this condition to protect
81
+ * workflows from stale concurrent resumes.
78
82
  */
79
- readonly clearReplies: (requestId: Snowflake.Snowflake) => Effect.Effect<void, PersistenceError>
83
+ readonly clearReplies: (
84
+ requestId: Snowflake.Snowflake,
85
+ options?: { readonly expectedReplyId?: Snowflake.Snowflake | undefined }
86
+ ) => Effect.Effect<void, PersistenceError>
80
87
 
81
88
  /**
82
89
  * Retrieves the replies for the specified requests.
@@ -350,9 +357,16 @@ export type Encoded = {
350
357
  readonly saveReply: (reply: Reply.Encoded) => Effect.Effect<void, PersistenceError>
351
358
 
352
359
  /**
353
- * Remove the replies for the specified request.
360
+ * Remove the replies for the specified request. If options.expectedReplyId
361
+ * is provided, compare it with the latest reply atomically before clearing.
362
+ * Without it, clear unconditionally.
363
+ * Custom storage implementations must honor this condition to protect
364
+ * workflows from stale concurrent resumes.
354
365
  */
355
- readonly clearReplies: (requestId: Snowflake.Snowflake) => Effect.Effect<void, PersistenceError>
366
+ readonly clearReplies: (
367
+ requestId: Snowflake.Snowflake,
368
+ options?: { readonly expectedReplyId?: Snowflake.Snowflake | undefined }
369
+ ) => Effect.Effect<void, PersistenceError>
356
370
 
357
371
  /**
358
372
  * Retrieves the request id for the specified primary key.
@@ -1054,10 +1068,13 @@ export class MemoryDriver extends Context.Service<MemoryDriver>()("effect/cluste
1054
1068
  replyIds.add(reply.id)
1055
1069
  replyLatch.openUnsafe()
1056
1070
  }),
1057
- clearReplies: (id) =>
1071
+ clearReplies: (id, options) =>
1058
1072
  Effect.sync(() => {
1059
1073
  const entry = requests.get(String(id))
1060
1074
  if (!entry) return
1075
+ if (
1076
+ options?.expectedReplyId !== undefined && entry.replies.at(-1)?.id !== String(options.expectedReplyId)
1077
+ ) return
1061
1078
  entry.replies = []
1062
1079
  entry.lastReceivedChunk = undefined
1063
1080
  unprocessed.add(entry.envelope)
@@ -195,9 +195,13 @@ export class Sharding extends Context.Service<Sharding, {
195
195
  >
196
196
 
197
197
  /**
198
- * Reset the state of a message
198
+ * Reset the state of a message. When options.expectedReplyId is provided,
199
+ * reset only if it is still the latest reply. Otherwise reset unconditionally.
199
200
  */
200
- readonly reset: (requestId: Snowflake.Snowflake) => Effect.Effect<boolean>
201
+ readonly reset: (
202
+ requestId: Snowflake.Snowflake,
203
+ options?: { readonly expectedReplyId?: Snowflake.Snowflake | undefined }
204
+ ) => Effect.Effect<boolean>
201
205
 
202
206
  /**
203
207
  * Trigger a storage read, which will read all unprocessed messages.
@@ -1208,8 +1212,8 @@ const make = Effect.gen(function*() {
1208
1212
  )
1209
1213
  }
1210
1214
 
1211
- const reset: Sharding["Service"]["reset"] = (requestId) =>
1212
- Effect.matchCause(storage.clearReplies(requestId), {
1215
+ const reset: Sharding["Service"]["reset"] = (requestId, options) =>
1216
+ Effect.matchCause(storage.clearReplies(requestId, options), {
1213
1217
  onSuccess: () => true,
1214
1218
  onFailure: () => false
1215
1219
  })
@@ -576,14 +576,25 @@ export const makeEncoded: (options?: {
576
576
  ),
577
577
 
578
578
  clearReplies: Effect.fnUntraced(
579
- function*(requestId) {
580
- yield* sql`DELETE FROM ${repliesTableSql} WHERE request_id = ${String(requestId)} AND kind = 0`
581
- yield* sql`DELETE FROM ${messagesTableSql} WHERE request_id = ${
582
- String(requestId)
583
- } AND kind = ${messageKindInterrupt}`
584
- yield* sql`UPDATE ${messagesTableSql} SET processed = ${sqlFalse}, last_reply_id = NULL, last_read = NULL WHERE request_id = ${
585
- String(requestId)
586
- }`
579
+ function*(requestId, options) {
580
+ const expectedReplyId = options?.expectedReplyId
581
+ if (expectedReplyId === undefined) {
582
+ yield* sql`DELETE FROM ${repliesTableSql} WHERE request_id = ${String(requestId)} AND kind = 0`
583
+ yield* sql`DELETE FROM ${messagesTableSql} WHERE request_id = ${
584
+ String(requestId)
585
+ } AND kind = ${messageKindInterrupt}`
586
+ yield* sql`UPDATE ${messagesTableSql} SET processed = ${sqlFalse}, last_reply_id = NULL, last_read = NULL WHERE request_id = ${
587
+ String(requestId)
588
+ }`
589
+ return
590
+ }
591
+ const id = String(requestId)
592
+ const expected = String(expectedReplyId)
593
+ // Check at the storage boundary, not at the earlier workflow read.
594
+ yield* sql`DELETE FROM ${repliesTableSql} WHERE request_id = ${id} AND kind = 0
595
+ AND EXISTS (SELECT 1 FROM ${messagesTableSql} WHERE id = ${id} AND last_reply_id = ${expected})`
596
+ yield* sql`UPDATE ${messagesTableSql} SET processed = ${sqlFalse}, last_reply_id = NULL, last_read = NULL
597
+ WHERE request_id = ${id} AND last_reply_id = ${expected}`
587
598
  },
588
599
  sql.withTransaction,
589
600
  PersistenceError.refail,
@@ -32,6 +32,7 @@ import * as preResponseHandler from "./internal/preResponseHandler.ts"
32
32
 
33
33
  /**
34
34
  * Runs an HTTP server effect, sends the produced response with the supplied handler, and converts failures into HTTP responses.
35
+ * On handler failure, the request scope closes with the original cause.
35
36
  *
36
37
  * @stability unstable
37
38
  * @category combinators
@@ -45,8 +46,10 @@ export const toHandled = <E, R, EH, RH>(
45
46
  ) => Effect.Effect<unknown, EH, RH>,
46
47
  middleware?: HttpMiddleware | undefined
47
48
  ): Effect.Effect<void, never, Exclude<R | RH | HttpServerRequest, Scope.Scope>> => {
48
- const handleCause = (request: HttpServerRequest, cause: Cause.Cause<E | EH | HttpServerError>) =>
49
- Effect.flatMapEager(causeResponse(cause), ([response, cause]) => {
49
+ const handleCause = (frame: RequestFrame, cause: Cause.Cause<E | EH | HttpServerError>) => {
50
+ frame.cause = cause
51
+ const request = frame.request
52
+ return Effect.flatMapEager(causeResponse(cause), ([response, cause]) => {
50
53
  const fiber = Fiber.getCurrent()!
51
54
  reportCauseUnsafe(fiber, cause)
52
55
  const handler = preResponseHandler.requestPreResponseHandlers.get(request.source)
@@ -67,6 +70,7 @@ export const toHandled = <E, R, EH, RH>(
67
70
  () => cont
68
71
  )
69
72
  })
73
+ }
70
74
 
71
75
  // Writes the response, applying any registered pre-response handler first.
72
76
  // Returns an `Exit` when the write completes synchronously.
@@ -85,14 +89,15 @@ export const toHandled = <E, R, EH, RH>(
85
89
  })
86
90
  }
87
91
 
88
- const withMiddleware = (request: HttpServerRequest): Effect.Effect<
92
+ const withMiddleware = (frame: RequestFrame): Effect.Effect<
89
93
  unknown,
90
94
  E | EH | HttpServerError,
91
95
  HttpServerRequest | R | RH
92
96
  > => {
97
+ const request = frame.request
93
98
  const responded = Effect.matchCauseEffect(self, {
94
99
  onSuccess: (response) => sendResponse(request, response),
95
- onFailure: (cause) => handleCause(request, cause)
100
+ onFailure: (cause) => handleCause(frame, cause)
96
101
  })
97
102
  return middleware === undefined ?
98
103
  responded :
@@ -116,9 +121,9 @@ export const toHandled = <E, R, EH, RH>(
116
121
  }
117
122
 
118
123
  // Apply tracing lazily so disabled requests can use the single-frame path.
119
- const traced = (request: HttpServerRequest) =>
124
+ const traced = (frame: RequestFrame) =>
120
125
  tracer(
121
- withMiddleware(request) as Effect.Effect<
126
+ withMiddleware(frame) as Effect.Effect<
122
127
  HttpServerResponse,
123
128
  E | EH | HttpServerError,
124
129
  HttpServerRequest | R | RH
@@ -135,13 +140,14 @@ export const toHandled = <E, R, EH, RH>(
135
140
  Effect.onExitPrimitive(effect, (exit) => {
136
141
  fiber.setContext(frame.prev)
137
142
  if (scopeEjected in frame.scope) return undefined
138
- return Scope.closeUnsafe(frame.scope, exit)
143
+ return Scope.closeUnsafe(frame.scope, frame.cause === undefined ? exit : Exit.failCause(frame.cause))
139
144
  }, true)
140
145
 
141
146
  class RequestFrame {
142
147
  readonly scope: Scope.Closeable
143
148
  readonly prev: Context.Context<never>
144
149
  readonly request: HttpServerRequest
150
+ cause: Cause.Cause<E | EH | HttpServerError> | undefined
145
151
  constructor(scope: Scope.Closeable, prev: Context.Context<never>, request: HttpServerRequest) {
146
152
  this.scope = scope
147
153
  this.prev = prev
@@ -159,7 +165,7 @@ export const toHandled = <E, R, EH, RH>(
159
165
  return finishAfter(this, fiber, sent)
160
166
  }
161
167
  [contE](cause: Cause.Cause<E | EH | HttpServerError>, fiber: Fiber.Fiber<unknown, unknown>) {
162
- return finishAfter(this, fiber, handleCause(this.request, cause))
168
+ return finishAfter(this, fiber, handleCause(this, cause))
163
169
  }
164
170
  }
165
171
 
@@ -177,7 +183,7 @@ export const toHandled = <E, R, EH, RH>(
177
183
  ;(fiber as any)._stack.push(frame)
178
184
  return self
179
185
  }
180
- return finishAfter(frame, fiber, middleware === undefined ? traced(frame.request) : withMiddleware(frame.request))
186
+ return finishAfter(frame, fiber, middleware === undefined ? traced(frame) : withMiddleware(frame))
181
187
  }) as any
182
188
  }
183
189