Allow sync sentinel short-circuits in Sium

active-uix
dev 5 months ago
parent fbb1e0f645
commit c4eafd02f2

@ -104,11 +104,13 @@ Actualizacion 2026-05-16:
5. Validado: `npx vitest run src/arts/sium/test` -> 18 archivos, 366 tests OK; 5. Validado: `npx vitest run src/arts/sium/test` -> 18 archivos, 366 tests OK;
`npm run check` -> 0 errores, 0 warnings; `npm run test` -> 233 archivos, `npm run check` -> 0 errores, 0 warnings; `npm run test` -> 233 archivos,
2262 tests OK. 2262 tests OK.
6. Pendiente deliberado: revisar `optional/nullable/defaulted` con schema interno async. El problema 6. Cerrado: `optional/nullable/defaulted` con inner async conservan el
es real, pero la solucion no es mover un `if`: `createSchema.decodeSync` short-circuit sync del sentinel (`undefined`, `null` o default). Si el
lanza por `spec.async === true` antes de entrar en `decodeImpl`. Hace falta valor definido necesita entrar al inner async, `decodeSync` /
una decision de runtime (`syncShortCircuit`, decodeSync custom por wrapper, `validateSync` siguen lanzando `SiumAsyncSchemaError`.
o semantica documentada de "schema async siempre async"). 7. Validado tras wrappers async: `npx vitest run src/arts/sium/test` -> 18
archivos, 368 tests OK; `npm run check` -> 0 errores, 0 warnings;
`npm run test` -> 233 archivos, 2264 tests OK.
- Sium P1 coordinado con consumidores: - Sium P1 coordinado con consumidores:
1. No cambiar `lazy()` a `kind: 'lazy'` hasta que 1. No cambiar `lazy()` a `kind: 'lazy'` hasta que
`src/uix/soma/components/form/components/form-auto-fields.svelte` sepa `src/uix/soma/components/form/components/form-auto-fields.svelte` sepa

@ -252,6 +252,11 @@ defaulted(number(), () => 18);
`defaulted` aplica el valor por defecto en decode cuando el input es `undefined`. `defaulted` aplica el valor por defecto en decode cuando el input es `undefined`.
Si el schema interno es async, los wrappers conservan el short-circuit sincronico del sentinel:
`optional(asyncSchema).decodeSync(undefined)`, `nullable(asyncSchema).decodeSync(null)` y
`defaulted(asyncSchema).decodeSync(undefined)` no entran al schema interno. Cualquier valor que
si necesite validar el inner async lanza `SiumAsyncSchemaError` en APIs sync.
### Combinadores ### Combinadores
```ts ```ts
@ -730,7 +735,9 @@ if (error instanceof SiumValidationError) {
### `SiumAsyncSchemaError` ### `SiumAsyncSchemaError`
Lanzado por `decodeSync` o `validateSync` cuando el schema contiene steps async. 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)`).
Solucion: usa `await schema.decode(...)` o `await schema.validate(...)`. Solucion: usa `await schema.decode(...)` o `await schema.validate(...)`.

@ -8,9 +8,9 @@ import type { Schema } from './types';
* - `encode(undefined)` returns `undefined` directly. * - `encode(undefined)` returns `undefined` directly.
* - `encode(value)` delegates to the inner schema. * - `encode(value)` delegates to the inner schema.
* *
* Sync APIs inherit the async contract of the wrapped schema. If the inner * Sync APIs preserve the sentinel short-circuit even when the inner schema is
* schema is async, `decodeSync` / `validateSync` throw `SiumAsyncSchemaError` * async. `decodeSync(undefined)` returns `undefined`; defined values delegate
* even when the input is `undefined` and could be short-circuited. * to the inner schema and throw `SiumAsyncSchemaError` if that path is async.
*/ */
export function optional<I, O>(schema: Schema<I, O>): Schema<I | undefined, O | undefined> { export function optional<I, O>(schema: Schema<I, O>): Schema<I | undefined, O | undefined> {
const innerIsAsync = schema['~sium'].async; const innerIsAsync = schema['~sium'].async;
@ -32,6 +32,13 @@ export function optional<I, O>(schema: Schema<I, O>): Schema<I | undefined, O |
return innerIsAsync ? schema.decode(input, ctx) : schema.decodeSync(input, ctx); return innerIsAsync ? schema.decode(input, ctx) : schema.decodeSync(input, ctx);
}, },
decodeSyncImpl: (input, ctx) => {
if (input === undefined) {
return undefined;
}
return schema.decodeSync(input, ctx);
},
encode: (value) => { encode: (value) => {
if (value === undefined) { if (value === undefined) {
return undefined; return undefined;
@ -49,9 +56,9 @@ export function optional<I, O>(schema: Schema<I, O>): Schema<I | undefined, O |
* - `encode(null)` returns `null` directly. * - `encode(null)` returns `null` directly.
* - `encode(value)` delegates to the inner schema. * - `encode(value)` delegates to the inner schema.
* *
* Sync APIs inherit the async contract of the wrapped schema. If the inner * Sync APIs preserve the sentinel short-circuit even when the inner schema is
* schema is async, `decodeSync` / `validateSync` throw `SiumAsyncSchemaError` * async. `decodeSync(null)` returns `null`; non-null values delegate to the
* before any null short-circuit happens. * inner schema and throw `SiumAsyncSchemaError` if that path is async.
*/ */
export function nullable<I, O>(schema: Schema<I, O>): Schema<I | null, O | null> { export function nullable<I, O>(schema: Schema<I, O>): Schema<I | null, O | null> {
const innerIsAsync = schema['~sium'].async; const innerIsAsync = schema['~sium'].async;
@ -73,6 +80,13 @@ export function nullable<I, O>(schema: Schema<I, O>): Schema<I | null, O | null>
return innerIsAsync ? schema.decode(input, ctx) : schema.decodeSync(input, ctx); return innerIsAsync ? schema.decode(input, ctx) : schema.decodeSync(input, ctx);
}, },
decodeSyncImpl: (input, ctx) => {
if (input === null) {
return null;
}
return schema.decodeSync(input, ctx);
},
encode: (value) => { encode: (value) => {
if (value === null) { if (value === null) {
return null; return null;
@ -92,12 +106,14 @@ export function nullable<I, O>(schema: Schema<I, O>): Schema<I | null, O | null>
* - `encode(value)` delegates directly to the inner schema and never * - `encode(value)` delegates directly to the inner schema and never
* re-injects the default. * re-injects the default.
* *
* Sync APIs inherit the async contract of the wrapped schema. If the inner * Sync APIs preserve the default short-circuit even when the inner schema is
* schema is async, `decodeSync` / `validateSync` throw `SiumAsyncSchemaError` * async. `decodeSync(undefined)` returns `getDefault()`; defined values delegate
* even when the input is `undefined` and the default could be returned * to the inner schema and throw `SiumAsyncSchemaError` if that path is async.
* without consulting the inner decoder.
*/ */
export function defaulted<I, O>(schema: Schema<I, O>, getDefault: () => O): Schema<I | undefined, O> { export function defaulted<I, O>(
schema: Schema<I, O>,
getDefault: () => O
): Schema<I | undefined, O> {
const innerIsAsync = schema['~sium'].async; const innerIsAsync = schema['~sium'].async;
return createSchema({ return createSchema({
@ -118,6 +134,13 @@ export function defaulted<I, O>(schema: Schema<I, O>, getDefault: () => O): Sche
return innerIsAsync ? schema.decode(input, ctx) : schema.decodeSync(input, ctx); return innerIsAsync ? schema.decode(input, ctx) : schema.decodeSync(input, ctx);
}, },
decodeSyncImpl: (input, ctx) => {
if (input === undefined) {
return getDefault();
}
return schema.decodeSync(input, ctx);
},
encode: (value) => schema.encode(value) encode: (value) => schema.encode(value)
}); });
} }

@ -17,6 +17,7 @@ import type {
type CreateSchemaSpec<I, O> = { type CreateSchemaSpec<I, O> = {
kind: SchemaKind; kind: SchemaKind;
decodeImpl: (input: I, ctx: Ctx) => O | Promise<O>; decodeImpl: (input: I, ctx: Ctx) => O | Promise<O>;
decodeSyncImpl?: (input: I, ctx: Ctx) => O;
encode: (value: O) => I; encode: (value: O) => I;
wrappers?: ReadonlyArray<SchemaWrapper>; wrappers?: ReadonlyArray<SchemaWrapper>;
effects?: ReadonlyArray<SchemaEffect>; effects?: ReadonlyArray<SchemaEffect>;
@ -65,6 +66,10 @@ export function createSchema<I, O>(spec: CreateSchemaSpec<I, O>): Schema<I, O> {
}; };
const decodeSync = (input: I, ctx?: Partial<Ctx>): O => { const decodeSync = (input: I, ctx?: Partial<Ctx>): O => {
if (spec.decodeSyncImpl) {
return spec.decodeSyncImpl(input, normalizeCtx(input, ctx));
}
if (spec.async === true) { if (spec.async === true) {
throw new SiumAsyncSchemaError(spec.kind); throw new SiumAsyncSchemaError(spec.kind);
} }

@ -14,7 +14,9 @@
* reports `kind='color'`, `wrappers=['optional']`, `effects=[]` so UI * reports `kind='color'`, `wrappers=['optional']`, `effects=[]` so UI
* generators can peel the wrappers and resolve the semantic type. * generators can peel the wrappers and resolve the semantic type.
* 3. Async is the default API surface. `decodeSync` / `validateSync` throw * 3. Async is the default API surface. `decodeSync` / `validateSync` throw
* `SiumAsyncSchemaError` if the schema contains async refinements. * `SiumAsyncSchemaError` when the requested path needs async work. Wrapper
* sentinels (`optional(undefined)`, `nullable(null)`, `defaulted(undefined)`)
* may short-circuit synchronously even if their inner schema is async.
* 4. Errors are structured data: `{ path, code, message, params }`. * 4. Errors are structured data: `{ path, code, message, params }`.
* `message` uses the framework idlangref format * `message` uses the framework idlangref format
* (`#?sium.errors.X|fallback`) so the adapter can resolve or fall back * (`#?sium.errors.X|fallback`) so the adapter can resolve or fall back

@ -171,7 +171,7 @@ describe('wrapper stacking and async inheritance', () => {
expect(schema['~sium'].wrappers).toEqual(['nullable', 'optional']); expect(schema['~sium'].wrappers).toEqual(['nullable', 'optional']);
}); });
it('throws SiumAsyncSchemaError for optional(asyncSchema).decodeSync(undefined)', () => { it('short-circuits optional(asyncSchema).decodeSync(undefined)', () => {
const decodeImpl = vi.fn(async (input: string) => input); const decodeImpl = vi.fn(async (input: string) => input);
const asyncSchema = createSchema({ const asyncSchema = createSchema({
kind: 'string', kind: 'string',
@ -180,11 +180,16 @@ describe('wrapper stacking and async inheritance', () => {
encode: (value: string) => value encode: (value: string) => value
}); });
expect(() => optional(asyncSchema).decodeSync(undefined)).toThrow(SiumAsyncSchemaError); expect(optional(asyncSchema).decodeSync(undefined)).toBeUndefined();
expect(optional(asyncSchema).validateSync(undefined)).toEqual({
ok: true,
value: undefined
});
expect(() => optional(asyncSchema).decodeSync('x')).toThrow(SiumAsyncSchemaError);
expect(decodeImpl).not.toHaveBeenCalled(); expect(decodeImpl).not.toHaveBeenCalled();
}); });
it('throws SiumAsyncSchemaError for defaulted(asyncSchema).decodeSync(undefined)', () => { it('short-circuits nullable(asyncSchema).decodeSync(null)', () => {
const decodeImpl = vi.fn(async (input: string) => input); const decodeImpl = vi.fn(async (input: string) => input);
const asyncSchema = createSchema({ const asyncSchema = createSchema({
kind: 'string', kind: 'string',
@ -193,9 +198,48 @@ describe('wrapper stacking and async inheritance', () => {
encode: (value: string) => value encode: (value: string) => value
}); });
expect(() => defaulted(asyncSchema, () => 'fallback').decodeSync(undefined)).toThrow( expect(nullable(asyncSchema).decodeSync(null)).toBeNull();
SiumAsyncSchemaError expect(nullable(asyncSchema).validateSync(null)).toEqual({
); ok: true,
value: null
});
expect(() => nullable(asyncSchema).decodeSync('x')).toThrow(SiumAsyncSchemaError);
expect(decodeImpl).not.toHaveBeenCalled();
});
it('short-circuits nested wrapper sentinels before async inner work', () => {
const decodeImpl = vi.fn(async (input: string) => input);
const asyncSchema = createSchema({
kind: 'string',
async: true,
decodeImpl,
encode: (value: string) => value
});
const schema = optional(nullable(asyncSchema));
expect(schema.decodeSync(undefined)).toBeUndefined();
expect(schema.decodeSync(null)).toBeNull();
expect(() => schema.decodeSync('x')).toThrow(SiumAsyncSchemaError);
expect(decodeImpl).not.toHaveBeenCalled();
});
it('short-circuits defaulted(asyncSchema).decodeSync(undefined)', () => {
const decodeImpl = vi.fn(async (input: string) => input);
const asyncSchema = createSchema({
kind: 'string',
async: true,
decodeImpl,
encode: (value: string) => value
});
const schema = defaulted(asyncSchema, () => 'fallback');
expect(schema.decodeSync(undefined)).toBe('fallback');
expect(schema.validateSync(undefined)).toEqual({
ok: true,
value: 'fallback'
});
expect(() => schema.decodeSync('x')).toThrow(SiumAsyncSchemaError);
expect(decodeImpl).not.toHaveBeenCalled(); expect(decodeImpl).not.toHaveBeenCalled();
}); });
@ -218,4 +262,3 @@ describe('wrapper stacking and async inheritance', () => {
expect(schema['~sium'].meta.widgetOptions).toBe(inner['~sium'].meta.widgetOptions); expect(schema['~sium'].meta.widgetOptions).toBe(inner['~sium'].meta.widgetOptions);
}); });
}); });

Loading…
Cancel
Save

Powered by TurnKey Linux.