Skip to content

feat: compatible mode for molecule decode #209

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 23, 2025
Merged
Changes from 3 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
63 changes: 38 additions & 25 deletions packages/core/src/molecule/codec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,19 @@ import {

export type CodecLike<Encodable, Decoded = Encodable> = {
readonly encode: (encodable: Encodable) => Bytes;
readonly decode: (decodable: BytesLike) => Decoded;
readonly decode: (
decodable: BytesLike,
config?: { isExtraFieldIgnored?: boolean },

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use term compatible which is already used in Rust(https://github.com/search?q=repo%3Anervosnetwork%2Fmolecule%20compatible&type=code) instead of isExtraFieldIgnored

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought isCompatible should be a good enough name here. But after consideration, I suspect that compatible is an accurate and descriptive word here.

If we say compatible or isCompatible, it sounds more like the decodable is compatible, while it's not. It's more like we are telling "use the compatible mode", so isCompatibleMode might be more accurate, but still not descriptive enough.

As a result, I suggest describing exactly what we're doing, like shouldAcceptExtraField/isExtraFieldIgnored. And add a comment here to tell this is the same as compatible in Rust.

) => Decoded;
readonly byteLength?: number;
};
export class Codec<Encodable, Decoded = Encodable> {
constructor(
public readonly encode: (encodable: Encodable) => Bytes,
public readonly decode: (decodable: BytesLike) => Decoded,
public readonly decode: (
decodable: BytesLike,
config?: { isExtraFieldIgnored?: boolean }, // This is equivalent to "compatible" in the Rust implementation of Molecule.
) => Decoded,
public readonly byteLength?: number, // if provided, treat codec as fixed length
) {}

Expand All @@ -43,7 +49,7 @@ export class Codec<Encodable, Decoded = Encodable> {
}
return encoded;
},
(decodable) => {
(decodable, config = { isExtraFieldIgnored: false }) => {
const decodableBytes = bytesFrom(decodable);
if (
byteLength !== undefined &&
Expand All @@ -53,7 +59,7 @@ export class Codec<Encodable, Decoded = Encodable> {
`Codec.decode: expected byte length ${byteLength}, got ${decodableBytes.byteLength}`,
);
}
return decode(decodable);
return decode(decodable, config);
},
byteLength,
);
Expand All @@ -69,10 +75,10 @@ export class Codec<Encodable, Decoded = Encodable> {
return new Codec(
(encodable) =>
this.encode((inMap ? inMap(encodable) : encodable) as Encodable),
(buffer) =>
(buffer, config = { isExtraFieldIgnored: false }) =>
(outMap
? outMap(this.decode(buffer))
: this.decode(buffer)) as NewDecoded,
? outMap(this.decode(buffer, config))
: this.decode(buffer, config)) as NewDecoded,
this.byteLength,
);
}
Expand Down Expand Up @@ -128,7 +134,7 @@ export function fixedItemVec<Encodable, Decoded>(
throw new Error(`fixedItemVec(${e?.toString()})`);
}
},
decode(buffer) {
decode(buffer, config) {
const value = bytesFrom(buffer);
if (value.byteLength < 4) {
throw new Error(
Expand All @@ -147,7 +153,10 @@ export function fixedItemVec<Encodable, Decoded>(
const decodedArray: Array<Decoded> = [];
for (let offset = 4; offset < byteLength; offset += itemByteLength) {
decodedArray.push(
itemCodec.decode(value.slice(offset, offset + itemByteLength)),
itemCodec.decode(
value.slice(offset, offset + itemByteLength),
config,
),
);
}
return decodedArray;
Expand Down Expand Up @@ -185,7 +194,7 @@ export function dynItemVec<Encodable, Decoded>(
throw new Error(`dynItemVec(${e?.toString()})`);
}
},
decode(buffer) {
decode(buffer, config) {
const value = bytesFrom(buffer);
if (value.byteLength < 4) {
throw new Error(
Expand Down Expand Up @@ -215,7 +224,7 @@ export function dynItemVec<Encodable, Decoded>(
const start = offsets[index];
const end = offsets[index + 1];
const itemBuffer = value.slice(start, end);
decodedArray.push(itemCodec.decode(itemBuffer));
decodedArray.push(itemCodec.decode(itemBuffer, config));
}
return decodedArray;
} catch (e) {
Expand Down Expand Up @@ -259,13 +268,13 @@ export function option<Encodable, Decoded>(
throw new Error(`option(${e?.toString()})`);
}
},
decode(buffer) {
decode(buffer, config) {
const value = bytesFrom(buffer);
if (value.byteLength === 0) {
return undefined;
}
try {
return innerCodec.decode(buffer);
return innerCodec.decode(buffer, config);
} catch (e) {
throw new Error(`option(${e?.toString()})`);
}
Expand All @@ -290,7 +299,7 @@ export function byteVec<Encodable, Decoded>(
throw new Error(`byteVec(${e?.toString()})`);
}
},
decode(buffer) {
decode(buffer, config) {
const value = bytesFrom(buffer);
if (value.byteLength < 4) {
throw new Error(
Expand All @@ -304,7 +313,7 @@ export function byteVec<Encodable, Decoded>(
);
}
try {
return codec.decode(value.slice(4));
return codec.decode(value.slice(4), config);
} catch (e: unknown) {
throw new Error(`byteVec(${e?.toString()})`);
}
Expand Down Expand Up @@ -371,7 +380,7 @@ export function table<
const packedTotalSize = uint32To(header.length + body.length + 4);
return bytesConcat(packedTotalSize, header, body);
},
decode(buffer) {
decode(buffer, config) {
const value = bytesFrom(buffer);
if (value.byteLength < 4) {
throw new Error(
Expand All @@ -397,9 +406,13 @@ export function table<
const payload = value.slice(start, end);
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
Object.assign(object, { [field]: codec.decode(payload) });
} catch (e: unknown) {
throw new Error(`table.${field}(${e?.toString()})`);
Object.assign(object, { [field]: codec.decode(payload, config) });
} catch (_e: unknown) {
if (config?.isExtraFieldIgnored) {
Object.assign(object, { [field]: null });
} else {
throw new Error(`table.${field}(${_e?.toString()})`);
}
}
}
return object as Decoded;
Expand Down Expand Up @@ -466,7 +479,7 @@ export function union<T extends Record<string, CodecLike<any, any>>>(
throw new Error(`union.(${typeStr})(${e?.toString()})`);
}
},
decode(buffer) {
decode(buffer, config) {
const value = bytesFrom(buffer);
const fieldIndex = uint32From(value.slice(0, 4));
const keys = Object.keys(codecLayout);
Expand Down Expand Up @@ -496,7 +509,7 @@ export function union<T extends Record<string, CodecLike<any, any>>>(
return {
type: field,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
value: codecLayout[field].decode(value.slice(4)),
value: codecLayout[field].decode(value.slice(4), config),
} as UnionDecoded<T>;
},
});
Expand Down Expand Up @@ -535,15 +548,15 @@ export function struct<

return bytesFrom(bytes);
},
decode(buffer) {
decode(buffer, config) {
const value = bytesFrom(buffer);
const object = {};
let offset = 0;
Object.entries(codecLayout).forEach(([key, codec]) => {
const payload = value.slice(offset, offset + codec.byteLength!);
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
Object.assign(object, { [key]: codec.decode(payload) });
Object.assign(object, { [key]: codec.decode(payload, config) });
} catch (e: unknown) {
throw new Error(`struct.${key}(${(e as Error).toString()})`);
}
Expand Down Expand Up @@ -583,7 +596,7 @@ export function array<Encodable, Decoded>(
throw new Error(`array(${e?.toString()})`);
}
},
decode(buffer) {
decode(buffer, config) {
const value = bytesFrom(buffer);
if (value.byteLength != byteLength) {
throw new Error(
Expand All @@ -594,7 +607,7 @@ export function array<Encodable, Decoded>(
const result: Array<Decoded> = [];
for (let i = 0; i < value.byteLength; i += itemCodec.byteLength!) {
result.push(
itemCodec.decode(value.slice(i, i + itemCodec.byteLength!)),
itemCodec.decode(value.slice(i, i + itemCodec.byteLength!), config),
);
}
return result;
Expand Down