import { AssertState } from "./assert.js" import { DecodeBuffer, EncodeBuffer } from "./buffer.js" import { Metadata } from "./metadata.js" import { ScaleAssertError, ScaleEncodeError } from "./util.js" export type Input = T extends Codec ? I : never export type Output = T extends Codec ? O : never export function createCodec( _codec: & ThisType> & Pick, "_encode" | "_decode" | "_assert" | "_staticSize" | "_metadata">, ): Codec { const { _staticSize, _encode, _assert, _decode, _metadata } = _codec const codec: Codec = { // @ts-ignore https://gist.github.com/tjjfvi/ea194c4fce76dacdd60a0943256332aa __proto__: Codec.prototype, _staticSize, _encode, _decode, _assert, _metadata, } return codec } type NoInfer = T extends infer U ? U : never export function withMetadata(metadata: Metadata, NoInfer>, codec: Codec): Codec { const result: Codec = { // @ts-ignore https://gist.github.com/tjjfvi/ea194c4fce76dacdd60a0943256332aa __proto__: Codec.prototype, ...codec, _metadata: [...metadata as Metadata, ...codec._metadata], } return result } const codecInspectCtx = new Map() let codecInspectIdN = 0 const nodeCustomInspect = Symbol.for("nodejs.util.inspect.custom") const denoCustomInspect = Symbol.for("Deno.customInspect") abstract class _Codec { private [nodeCustomInspect](_0: unknown, _1: unknown, inspect: (value: unknown) => string) { return this._inspect(inspect) } private [denoCustomInspect](inspect: (value: unknown, opts: unknown) => string, opts: unknown) { return this._inspect((x) => inspect(x, opts)) } // Properly handles circular codecs in the case of $.deferred private _inspect(inspect: (value: unknown) => string): string private _inspect(this: AnyCodec, inspect: (value: unknown) => string): string { let id = codecInspectCtx.get(this) if (id !== undefined) { if (id === null) { codecInspectCtx.set(this, id = codecInspectIdN++) } return `$${id}` } try { codecInspectCtx.set(this, null) const metadata = this._metadata[0] const content = metadata ? metadata.type === "atomic" ? metadata.name : `${metadata.name}(${inspect(metadata.args).replace(/^\[(?: (.+) |(.+))\]$/s, "$1$2")})` : "?" id = codecInspectCtx.get(this) return id !== null ? `$${id} = ${content}` : content } finally { codecInspectCtx.delete(this) if (codecInspectCtx.size === 0) codecInspectIdN = 0 } } } export type AnyCodec = Codec export type Encodec = Codec export type Decodec = Codec export abstract class Codec extends _Codec implements AnyCodec { /** A static estimation of the size, which may be an under- or over-estimate */ abstract _staticSize: number /** Encodes the value into the supplied buffer, which should have at least `_staticSize` free byte. */ abstract _encode: (buffer: EncodeBuffer, value: I) => void /** Decodes the value from the supplied buffer */ abstract _decode: (buffer: DecodeBuffer) => O /** Asserts that the value is valid for this codec */ abstract _assert: (state: AssertState) => void /** An array with metadata representing the construction of this codec */ abstract _metadata: Metadata /** Encodes the value into a new Uint8Array (throws if async) */ encode(value: I) { const buf = new EncodeBuffer(this._staticSize) this._encode(buf, value) if (buf.asyncCount) throw new ScaleEncodeError(this, value, "Attempted to synchronously encode an async codec") return buf.finish() } /** Asynchronously encodes the value into a new Uint8Array */ async encodeAsync(value: I) { const buf = new EncodeBuffer(this._staticSize) this._encode(buf, value) return buf.finishAsync() } /** Decodes a value from the supplied Uint8Array */ decode(array: Uint8Array) { const buf = new DecodeBuffer(array) return this._decode(buf) } /** Requires the codec to have an explicit type annotation; if it doesn't, use `$.assert` instead. */ assert(value: unknown): asserts value is I { assert(this, value) } } /** Asserts that the value is valid for the specified codec */ export function assert(codec: Codec, value: unknown): asserts value is I { codec._assert(new AssertState(value)) } export function is(codec: Codec, value: unknown): value is T { try { codec._assert(new AssertState(value)) return true } catch (e) { if (e instanceof ScaleAssertError) { return false } else { throw e } } }