Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import type { CodecCallContext, CodecTrait } from './codec-types';
*
* Codec methods split into two groups:
*
* - **Query-time** methods (`encode`, `decode`) run per row/parameter at the IO boundary; they are required and Promise-returning. The per-family codec factory accepts sync or async author functions and lifts sync ones to Promise-shaped methods automatically.
* - **Query-time** methods (`encode`, `decode`) run per row/parameter at the IO boundary; encoding is Promise-returning, while decoding is strictly synchronous in this performance prototype.
* - **JSON** methods (`encodeJson`, `decodeJson`) run when the contract is serialized or loaded. Runtimes may also use `decodeJson` for values embedded in database-produced JSON results. They stay synchronous so contract validation and client construction are synchronous.
*
* Target-family codec interfaces extend this base; family-specific concerns (e.g. the SQL `column?` per-call context) layer on through the `CodecCallContext` extension pattern.
Expand All @@ -43,8 +43,8 @@ export interface Codec<
readonly __codecTraits?: TTraits;
/** Converts a JS value to the wire format expected by the database driver. Always Promise-returning at the boundary. The {@link CodecCallContext} is supplied by the runtime on every call (allocated once per runtime operation, including `query()`, `PreparedStatement.query()`, and `execute()`); family layers may narrow the ctx to extend it (e.g. SQL adds `column`). Author-side single-arg `(value) => …` functions remain legal via TypeScript's bivariance for trailing parameters. */
encode(value: TInput, ctx: CodecCallContext): Promise<TWire>;
/** Converts a wire value from the database driver into the JS application type. Always Promise-returning at the boundary. The {@link CodecCallContext} is supplied by the runtime on every call (allocated once per runtime operation, including `query()`, `PreparedStatement.query()`, and `execute()`); family layers may narrow the ctx to extend it (e.g. SQL adds `column`). Author-side single-arg `(wire) => …` functions remain legal via TypeScript's bivariance for trailing parameters. */
decode(wire: TWire, ctx: CodecCallContext): Promise<TInput>;
/** Converts a wire value from the database driver into the JS application type synchronously. The {@link CodecCallContext} is supplied by the runtime on every call (allocated once per runtime operation, including `query()`, `PreparedStatement.query()`, and `execute()`); family layers may narrow the ctx to extend it (e.g. SQL adds `column`). Author-side single-arg `(wire) => …` functions remain legal via TypeScript's bivariance for trailing parameters. */
decode(wire: TWire, ctx: CodecCallContext): TInput;
/** Converts a JS value to the target-defined JSON representation used for contract serialization. This must match the scalar shape produced by the target inside JSON values. Synchronous; called during contract emission. */
encodeJson(value: TInput): JsonValue;
/** Converts the target-defined JSON representation back to the JS input type. Synchronous; called during contract loading via `family.deserializeContract` and may be called by runtimes for embedded JSON values. */
Expand Down Expand Up @@ -74,7 +74,7 @@ export abstract class CodecImpl<
}

abstract encode(value: TInput, ctx: CodecCallContext): Promise<TWire>;
abstract decode(wire: TWire, ctx: CodecCallContext): Promise<TInput>;
abstract decode(wire: TWire, ctx: CodecCallContext): TInput;
abstract encodeJson(value: TInput): JsonValue;
abstract decodeJson(json: JsonValue): TInput;
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ test('encode/decode call sites accept an explicit ctx (signal optional inside th
type StringCodec = Codec<'demo/text@1', readonly [], string, string>;
const encodeWithCtx = (c: StringCodec, v: string, ctx: CodecCallContext): Promise<string> =>
c.encode(v, ctx);
const decodeWithCtx = (c: StringCodec, w: string, ctx: CodecCallContext): Promise<string> =>
const decodeWithCtx = (c: StringCodec, w: string, ctx: CodecCallContext): string =>
c.decode(w, ctx);
// An empty ctx is legal — `signal` is the only field today and is optional inside the context shape.
const encodeWithEmptyCtx = (c: StringCodec, v: string): Promise<string> => c.encode(v, {});
const decodeWithEmptyCtx = (c: StringCodec, w: string): Promise<string> => c.decode(w, {});
const decodeWithEmptyCtx = (c: StringCodec, w: string): string => c.decode(w, {});
void encodeWithCtx;
void decodeWithCtx;
void encodeWithEmptyCtx;
Expand Down Expand Up @@ -68,9 +68,9 @@ test('encode return type is unconditionally Promise<TWire> (no conditional types
expectTypeOf<ReturnType<CodecB['encode']>>().toEqualTypeOf<Promise<number>>();
});

test('decode return type is unconditionally Promise<TInput> (no conditional types)', () => {
test('decode return type is unconditionally TInput (no conditional types)', () => {
type CodecA = Codec<'demo/a@1', readonly [], string, string>;
type CodecB = Codec<'demo/b@1', readonly [], number, number>;
expectTypeOf<ReturnType<CodecA['decode']>>().toEqualTypeOf<Promise<string>>();
expectTypeOf<ReturnType<CodecB['decode']>>().toEqualTypeOf<Promise<number>>();
expectTypeOf<ReturnType<CodecA['decode']>>().toEqualTypeOf<string>();
expectTypeOf<ReturnType<CodecB['decode']>>().toEqualTypeOf<number>();
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ test('encode is required and Promise-returning', () => {
expectTypeOf<EncodeReturn>().toExtend<Promise<unknown>>();
});

test('decode is required and Promise-returning', () => {
test('decode is required and synchronous', () => {
expectTypeOf<Codec>().toHaveProperty('decode');
expectTypeOf<Codec['decode']>().toBeFunction();
type DecodeReturn = ReturnType<Codec['decode']>;
expectTypeOf<DecodeReturn>().toExtend<Promise<unknown>>();
expectTypeOf<DecodeReturn>().toEqualTypeOf<unknown>();
});

test('encodeJson is required and synchronous', () => {
Expand Down Expand Up @@ -60,7 +60,7 @@ test('Codec carries four generics: encode TInput → TWire, decode TWire → TIn
expectTypeOf<Parameters<StringTextCodec['encode']>[0]>().toEqualTypeOf<string>();
expectTypeOf<ReturnType<StringTextCodec['encode']>>().toExtend<Promise<string>>();
expectTypeOf<Parameters<StringTextCodec['decode']>[0]>().toEqualTypeOf<string>();
expectTypeOf<ReturnType<StringTextCodec['decode']>>().toExtend<Promise<string>>();
expectTypeOf<ReturnType<StringTextCodec['decode']>>().toEqualTypeOf<string>();
expectTypeOf<Parameters<StringTextCodec['encodeJson']>[0]>().toEqualTypeOf<string>();
expectTypeOf<ReturnType<StringTextCodec['decodeJson']>>().toEqualTypeOf<string>();
});
Expand All @@ -70,5 +70,5 @@ test('TInput drives both write input and read output (no asymmetric output)', ()
expectTypeOf<Parameters<WireSeparateFromInput['encode']>[0]>().toEqualTypeOf<string>();
expectTypeOf<ReturnType<WireSeparateFromInput['encode']>>().toExtend<Promise<number>>();
expectTypeOf<Parameters<WireSeparateFromInput['decode']>[0]>().toEqualTypeOf<number>();
expectTypeOf<ReturnType<WireSeparateFromInput['decode']>>().toExtend<Promise<string>>();
expectTypeOf<ReturnType<WireSeparateFromInput['decode']>>().toEqualTypeOf<string>();
});
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class Int4FixtureCodec extends CodecImpl<'demo/int4@1', readonly ['equality'], n
async encode(value: number, _ctx: CodecCallContext): Promise<number> {
return value;
}
async decode(wire: number, _ctx: CodecCallContext): Promise<number> {
decode(wire: number, _ctx: CodecCallContext): number {
return wire;
}
encodeJson(value: number): JsonValue {
Expand Down Expand Up @@ -70,7 +70,7 @@ class VectorFixtureCodec<N extends number> extends CodecImpl<
async encode(value: number[], _ctx: CodecCallContext): Promise<string> {
return `[${value.join(',')}]`;
}
async decode(wire: string, _ctx: CodecCallContext): Promise<number[]> {
decode(wire: string, _ctx: CodecCallContext): number[] {
return wire.slice(1, -1).split(',').map(Number);
}
encodeJson(value: number[]): JsonValue {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class Int4FixtureCodec extends CodecImpl<'demo/int4@1', readonly ['equality'], n
async encode(value: number, _ctx: CodecCallContext): Promise<number> {
return value;
}
async decode(wire: number, _ctx: CodecCallContext): Promise<number> {
decode(wire: number, _ctx: CodecCallContext): number {
return wire;
}
encodeJson(value: number): JsonValue {
Expand Down Expand Up @@ -82,7 +82,7 @@ class VectorFixtureCodec<N extends number> extends CodecImpl<
async encode(value: number[], _ctx: CodecCallContext): Promise<string> {
return `[${value.join(',')}]`;
}
async decode(wire: string, _ctx: CodecCallContext): Promise<number[]> {
decode(wire: string, _ctx: CodecCallContext): number[] {
return wire.slice(1, -1).split(',').map(Number);
}
encodeJson(value: number[]): JsonValue {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1141,7 +1141,7 @@ describe('extractCodecLookup', () => {
({
id,
encode: async (v: unknown) => v,
decode: async (v: unknown) => v,
decode: (v: unknown) => v,
encodeJson: (v: unknown) => v,
decodeJson: (j: unknown) => j,
}) as unknown as Codec;
Expand Down Expand Up @@ -1487,7 +1487,7 @@ describe('validateScalarTypeCodecIds', () => {
? {
id,
encode: async (v: unknown) => v,
decode: async (v: unknown) => v,
decode: (v: unknown) => v,
encodeJson: (v: unknown) => v as JsonValue,
decodeJson: (v: JsonValue) => v,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class Int4FixtureCodec extends CodecImpl<'demo/int4@1', readonly ['equality'], n
async encode(value: number, _ctx: CodecCallContext): Promise<number> {
return value;
}
async decode(wire: number, _ctx: CodecCallContext): Promise<number> {
decode(wire: number, _ctx: CodecCallContext): number {
return wire;
}
encodeJson(value: number): JsonValue {
Expand Down Expand Up @@ -65,7 +65,7 @@ class VectorFixtureCodec<N extends number> extends CodecImpl<
async encode(value: number[], _ctx: CodecCallContext): Promise<string> {
return `[${value.join(',')}]`;
}
async decode(wire: string, _ctx: CodecCallContext): Promise<number[]> {
decode(wire: string, _ctx: CodecCallContext): number[] {
return wire.slice(1, -1).split(',').map(Number);
}
encodeJson(value: number[]): JsonValue {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ class StubStringCodec extends CodecImpl<'stub/string@1', readonly ['textual'], s
async encode(value: string, _ctx: CodecCallContext): Promise<string> {
return value;
}
async decode(wire: string, _ctx: CodecCallContext): Promise<string> {
decode(wire: string, _ctx: CodecCallContext): string {
return wire;
}
encodeJson(value: string): JsonValue {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class FixturePolicyTextCodec extends CodecImpl<
async encode(value: string, _ctx: CodecCallContext): Promise<string> {
return value;
}
async decode(wire: string, _ctx: CodecCallContext): Promise<string> {
decode(wire: string, _ctx: CodecCallContext): string {
return wire;
}
encodeJson(value: string): JsonValue {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class StubPolicyTextCodec extends CodecImpl<
async encode(value: string, _ctx: CodecCallContext): Promise<string> {
return value;
}
async decode(wire: string, _ctx: CodecCallContext): Promise<string> {
decode(wire: string, _ctx: CodecCallContext): string {
return wire;
}
encodeJson(value: string): JsonValue {
Expand Down Expand Up @@ -222,7 +222,7 @@ describe('generic extension-block printer (P2)', () => {
async encode(value: number, _ctx: CodecCallContext): Promise<number> {
return value;
}
async decode(wire: number, _ctx: CodecCallContext): Promise<number> {
decode(wire: number, _ctx: CodecCallContext): number {
return wire;
}
// A quoted "42" or a bare 42 both decode to the number 42 …
Expand Down
12 changes: 3 additions & 9 deletions packages/2-mongo-family/1-foundation/mongo-codec/src/codecs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ type JsonRoundTripConfig<TInput> = [TInput] extends [JsonValue]
/**
* Construct a Mongo codec from author functions.
*
* Author `encode` and `decode` as sync or async functions; the factory produces a {@link MongoCodec} whose query-time methods follow the boundary contract documented on the framework {@link BaseCodec}. Authors receive a second `ctx` options argument carrying the per-call context; ignore it if you don't need it.
* Author `encode` as sync or async and `decode` as synchronous; the factory produces a {@link MongoCodec} whose query-time methods follow the boundary contract documented on the framework {@link BaseCodec}. Authors receive a second `ctx` options argument carrying the per-call context; ignore it if you don't need it.
*
* Both `encode` and `decode` are required so `TInput` and `TWire` are always covered by an explicit author function — the factory installs no identity fallback. `encodeJson` and `decodeJson` default to identity **only when `TInput` is assignable to `JsonValue`**; otherwise both are required so the contract artifact stays JSON-safe.
*
Expand All @@ -50,7 +50,7 @@ export function mongoCodec<
config: {
typeId: Id;
encode: (value: TInput, ctx: CodecCallContext) => TWire | Promise<TWire>;
decode: (wire: TWire, ctx: CodecCallContext) => TInput | Promise<TInput>;
decode: (wire: TWire, ctx: CodecCallContext) => TInput;
} & JsonRoundTripConfig<TInput>,
): MongoCodec<Id, TTraits, TWire, TInput> {
const identity = (v: unknown) => v;
Expand All @@ -70,13 +70,7 @@ export function mongoCodec<
return Promise.reject(error);
}
},
decode: (wire, ctx) => {
try {
return Promise.resolve(userDecode(wire, ctx));
} catch (error) {
return Promise.reject(error);
}
},
decode: userDecode,
encodeJson: (widenedConfig.encodeJson ?? identity) as (value: TInput) => JsonValue,
decodeJson: (widenedConfig.decodeJson ?? identity) as (json: JsonValue) => TInput,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@ test('MongoCodecInput extracts the JS application type used for both write input

expectTypeOf<MongoCodecInput<typeof text>>().toEqualTypeOf<string>();
expectTypeOf<Parameters<typeof text.encode>[0]>().toEqualTypeOf<string>();
expectTypeOf<ReturnType<typeof text.decode>>().toExtend<Promise<string>>();
expectTypeOf<ReturnType<typeof text.decode>>().toEqualTypeOf<string>();
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ describe('mongoCodec()', () => {
});

expect(codec.id).toBe('test/string@1');
expect(await codec.decode('hello', {})).toBe('hello');
expect(codec.decode('hello', {})).toBe('hello');
expect(await codec.encode('hello', {})).toBe('hello');
});

Expand All @@ -24,11 +24,11 @@ describe('mongoCodec()', () => {
encode: (value: string) => value.toLowerCase(),
});

expect(await codec.decode('hello', {})).toBe('HELLO');
expect(codec.decode('hello', {})).toBe('HELLO');
expect(await codec.encode('HELLO', {})).toBe('hello');
});

it('lifts sync author functions to Promise-returning methods', () => {
it('keeps decode synchronous while lifting encode to a Promise', () => {
const codec = mongoCodec({
typeId: 'test/sync@1',
decode: (wire: string) => wire,
Expand All @@ -37,18 +37,18 @@ describe('mongoCodec()', () => {

const decoded = codec.decode('x', {});
const encoded = codec.encode('y', {});
expect(typeof (decoded as { then?: unknown }).then).toBe('function');
expect(decoded).toBe('x');
expect(typeof (encoded as { then?: unknown }).then).toBe('function');
});

it('accepts async author functions and uses them directly', async () => {
it('accepts async encode alongside synchronous decode', async () => {
const codec = mongoCodec({
typeId: 'test/async@1',
decode: async (wire: string) => `decoded:${wire}`,
decode: (wire: string) => `decoded:${wire}`,
encode: async (value: string) => `encoded:${value}`,
});

expect(await codec.decode('a', {})).toBe('decoded:a');
expect(codec.decode('a', {})).toBe('decoded:a');
expect(await codec.encode('b', {})).toBe('encoded:b');
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const mongoCodecLookup: CodecLookup = {
return {
id,
encode: async (v: unknown) => v,
decode: async (w: unknown) => w,
decode: (w: unknown) => w,
encodeJson: (v: unknown) => v,
decodeJson: (j: unknown) => j,
} as ReturnType<CodecLookup['get']>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ const mongoCodecLookup: CodecLookup = {
return {
id,
encode: async (v: unknown) => v,
decode: async (w: unknown) => w,
decode: (w: unknown) => w,
encodeJson: (v: unknown) => v,
decodeJson: (j: unknown) => j,
} as ReturnType<CodecLookup['get']>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const identityDescriptor = (id: string): AnyCodecDescriptor => ({
({
id,
encode: async (v: unknown) => v,
decode: async (v: unknown) => v,
decode: (v: unknown) => v,
encodeJson: (v: unknown) => v,
decodeJson: (j: unknown) => j,
}) as unknown as Codec,
Expand Down Expand Up @@ -273,7 +273,7 @@ describe('defineContract() — codec-encoded value set', () => {
({
id: 'test/upper@1',
encode: async (v: unknown) => v,
decode: async (v: unknown) => v,
decode: (v: unknown) => v,
encodeJson: (v: unknown) => (v as string).toUpperCase(),
decodeJson: (j: unknown) => j,
}) as unknown as Codec,
Expand Down
24 changes: 9 additions & 15 deletions packages/2-mongo-family/7-runtime/src/codecs/decoding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,13 @@ function wrapDecodeFailure(
throw wrapped;
}

export async function decodeMongoRow(
export function decodeMongoRow(
row: unknown,
shape: MongoResultShape,
registry: MongoCodecLookup,
collection: string,
ctx: CodecCallContext = {},
): Promise<unknown> {
): unknown {
if (shape.kind === 'unknown') {
return row;
}
Expand All @@ -60,9 +60,8 @@ export async function decodeMongoRow(
}
const rowObj = row as Record<string, unknown>;
const out: Record<string, unknown> = {};
const tasks: Array<Promise<void>> = [];

function scheduleLeaf(
function decodeLeaf(
path: string,
codecId: string,
wire: unknown,
Expand All @@ -73,15 +72,11 @@ export async function decodeMongoRow(
assign(wire);
return;
}
tasks.push(
(async () => {
try {
assign(await codec.decode(wire, ctx));
} catch (error) {
wrapDecodeFailure(error, collection, path, codecId, wire);
}
})(),
);
try {
assign(codec.decode(wire, ctx));
} catch (error) {
wrapDecodeFailure(error, collection, path, codecId, wire);
}
}

function walkField(
Expand All @@ -102,7 +97,7 @@ export async function decodeMongoRow(
assign(value);
return;
}
scheduleLeaf(path, fieldShape.codecId, value, assign);
decodeLeaf(path, fieldShape.codecId, value, assign);
return;
case 'document': {
if (value === null || value === undefined) {
Expand Down Expand Up @@ -174,6 +169,5 @@ export async function decodeMongoRow(
}
}

await Promise.all(tasks);
return out;
}
5 changes: 2 additions & 3 deletions packages/2-mongo-family/7-runtime/src/mongo-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,7 @@ export interface MongoRuntime {
*
* Mongo's read path decodes rows via `resultShape` (per ADR 209). The
* same `CodecCallContext` is forwarded into each `codec.decode(wire, ctx)`
* call, so async decoders that respect the signal get cancellation; the
* runtime itself does not currently emit a `phase: 'decode'` envelope.
* call synchronously; the runtime does not emit a `phase: 'decode'` envelope.
*/
query<Row>(plan: MongoQueryPlan<Row>, options?: RuntimeExecuteOptions): AsyncIterableResult<Row>;
execute(plan: MongoQueryPlan, options?: RuntimeExecuteOptions): Promise<RuntimeStatementStats>;
Expand Down Expand Up @@ -253,7 +252,7 @@ class MongoRuntimeImpl
rawRow,
);
} else {
const decoded = await decodeMongoRow(
const decoded = decodeMongoRow(
rawRow,
exec.resultShape,
self.#codecs,
Expand Down
Loading
Loading