diff --git a/continue.md b/continue.md index eec60afb7..97cb22d8e 100644 --- a/continue.md +++ b/continue.md @@ -127,8 +127,11 @@ Actualizacion 2026-05-16: `createDomainRangeSchema(...)`. `colorValue`, `dateValue`, `timeValue`, `dateRange` y `timeRange` comparten ahora el patron identity codec + guard + widget + range order sin mover esa logica a `core`. - 2. Unificar paths sync/async de combinadores solo con tests amplios; es zona - sensible por `SiumAsyncSchemaError`. + 2. Cerrado: combinadores (`object`, `array`, `union`, `discriminated`) usan + la misma semantica por ruta que wrappers. Un schema marcado `async` puede + completar `decodeSync` si el input no necesita tocar trabajo async (por + ejemplo field optional async ausente, array vacio o union sync-first). Si + la ruta seleccionada necesita async, sigue lanzando `SiumAsyncSchemaError`. 3. Cerrado: `lazy()` documenta limites de datos runtime autociclicos y lanza `SiumLazyResolvingError` cuando el thunk usa el proxy mientras resuelve o devuelve el mismo proxy como schema final. No se anade `WeakSet` global @@ -137,6 +140,9 @@ Actualizacion 2026-05-16: 4. Validado tras `lazy()`: `npx vitest run src/arts/sium/test` -> 18 archivos, 370 tests OK; `npm run check` -> 0 errores, 0 warnings; `npm run test` -> 233 archivos, 2266 tests OK. + 5. Validado tras paths sync/async de combinadores: `npx vitest run +src/arts/sium/test` -> 18 archivos, 374 tests OK; `npm run check` -> 0 + errores, 0 warnings; `npm run test` -> 233 archivos, 2270 tests OK. - Validacion Sium domain factory: - `npx vitest run src/arts/sium/test/color.test.ts src/arts/sium/test/date.test.ts src/arts/sium/test/time.test.ts src/arts/sium/test/standard-schema.test.ts` -> 4 archivos, 122 tests OK. - `npm run check` -> 0 errores, 0 warnings. diff --git a/src/arts/sium/README.md b/src/arts/sium/README.md index f439e9b21..c27fe25b5 100644 --- a/src/arts/sium/README.md +++ b/src/arts/sium/README.md @@ -744,6 +744,9 @@ if (error instanceof SiumValidationError) { Lanzado por `decodeSync` o `validateSync` cuando la ruta de validacion necesita steps async. Los wrappers de sentinel pueden devolver sincronamente si no consultan el inner async (`optional(undefined)`, `nullable(null)`, `defaulted(undefined)`). +Los combinadores siguen la misma regla por ruta: por ejemplo, `object({ a: +optional(asyncSchema) }).decodeSync({ a: undefined })` puede completarse; `{ a: 'x' }` +lanza porque ya necesita validar el inner async. Solucion: usa `await schema.decode(...)` o `await schema.validate(...)`. diff --git a/src/arts/sium/core/array-combinator.ts b/src/arts/sium/core/array-combinator.ts index 84b7b8cc7..556b7d224 100644 --- a/src/arts/sium/core/array-combinator.ts +++ b/src/arts/sium/core/array-combinator.ts @@ -80,6 +80,7 @@ export function array(item: Schema): Schema { item }, decodeImpl: item['~sium'].async ? decodeAsyncArray : decodeSyncArray, + decodeSyncImpl: decodeSyncArray, encode: (value) => { if (!Array.isArray(value)) { throw new SiumEncodeExpectsArrayError(); diff --git a/src/arts/sium/core/object-combinator.ts b/src/arts/sium/core/object-combinator.ts index 62904aa1e..c404cd84e 100644 --- a/src/arts/sium/core/object-combinator.ts +++ b/src/arts/sium/core/object-combinator.ts @@ -118,6 +118,7 @@ export function object>( unknownKeys }, decodeImpl: isAsync ? decodeAsyncShape : decodeSyncShape, + decodeSyncImpl: decodeSyncShape, encode: (value) => { if (!isPlainObjectRecord(value)) { throw new SiumEncodeExpectsObjectError('object'); diff --git a/src/arts/sium/core/union-combinators.ts b/src/arts/sium/core/union-combinators.ts index e859410ed..58b146c07 100644 --- a/src/arts/sium/core/union-combinators.ts +++ b/src/arts/sium/core/union-combinators.ts @@ -68,6 +68,7 @@ export function union( members }, decodeImpl: isAsync ? decodeAsyncUnion : decodeSyncUnion, + decodeSyncImpl: decodeSyncUnion, encode: (value) => value as InferInput }); } @@ -160,6 +161,7 @@ export function discriminated { if (!isPlainObjectRecord(value)) { throw new SiumEncodeExpectsObjectError('discriminated'); diff --git a/src/arts/sium/test/combinators.test.ts b/src/arts/sium/test/combinators.test.ts index 32e43289b..d2d589e92 100644 --- a/src/arts/sium/test/combinators.test.ts +++ b/src/arts/sium/test/combinators.test.ts @@ -1,5 +1,6 @@ import { describe, expect, expectTypeOf, it, vi } from 'vitest'; import { array, discriminated, object, union } from '../core/combinators'; +import { optional } from '../core/modifiers'; import { pipe, refine } from '../core/pipe'; import { literal, number, string } from '../core/primitives'; import { createSchema } from '../core/schema'; @@ -297,6 +298,19 @@ describe('object()', () => { expect(() => schema.decodeSync({ a: 'x', b: 1 })).toThrow(SiumAsyncSchemaError); }); + + it('short-circuits async optional fields in sync object paths', () => { + const asyncField = createSchema({ + kind: 'string', + async: true, + decodeImpl: async (input: string) => input, + encode: (value: string) => value + }); + const schema = object({ a: optional(asyncField), b: number() }); + + expect(schema.decodeSync({ a: undefined, b: 1 })).toEqual({ a: undefined, b: 1 }); + expect(() => schema.decodeSync({ a: 'x', b: 1 })).toThrow(SiumAsyncSchemaError); + }); }); describe('array()', () => { @@ -429,6 +443,19 @@ describe('array()', () => { expect(() => schema.decodeSync(['a'])).toThrow(SiumAsyncSchemaError); }); + + it('short-circuits sync array paths that do not need async item work', () => { + const asyncItem = createSchema({ + kind: 'string', + async: true, + decodeImpl: async (input: string) => input, + encode: (value: string) => value + }); + + expect(array(asyncItem).decodeSync([])).toEqual([]); + expect(array(optional(asyncItem)).decodeSync([undefined])).toEqual([undefined]); + expect(() => array(asyncItem).decodeSync(['a'])).toThrow(SiumAsyncSchemaError); + }); }); describe('union()', () => { @@ -496,6 +523,18 @@ describe('union()', () => { expect(() => schema.decodeSync('x')).toThrow(SiumAsyncSchemaError); }); + + it('can decode sync union members before an async member is needed', () => { + const asyncMember = createSchema({ + kind: 'string', + async: true, + decodeImpl: async (input: string) => input, + encode: (value: string) => value + }); + + expect(union(number(), asyncMember).decodeSync(1)).toBe(1); + expect(() => union(asyncMember, number()).decodeSync(1 as never)).toThrow(SiumAsyncSchemaError); + }); }); describe('discriminated()', () => { @@ -640,4 +679,24 @@ describe('discriminated()', () => { expect(() => schema.decodeSync({ kind: 'a', val: 'x' })).toThrow(SiumAsyncSchemaError); }); + + it('short-circuits selected discriminated member paths when async work is not needed', () => { + const asyncValue = createSchema({ + kind: 'string', + async: true, + decodeImpl: async (input: string) => input, + encode: (value: string) => value + }); + const schema = discriminated('kind', [ + object({ kind: literal('a'), val: optional(asyncValue) }), + object({ kind: literal('b'), val: number() }) + ]); + + expect(schema.decodeSync({ kind: 'a', val: undefined })).toEqual({ + kind: 'a', + val: undefined + }); + expect(schema.decodeSync({ kind: 'b', val: 1 })).toEqual({ kind: 'b', val: 1 }); + expect(() => schema.decodeSync({ kind: 'a', val: 'x' })).toThrow(SiumAsyncSchemaError); + }); });