diff --git a/implementations/javascript/package.json b/implementations/javascript/package.json index c6f3aa6..ee61172 100644 --- a/implementations/javascript/package.json +++ b/implementations/javascript/package.json @@ -1,6 +1,6 @@ { "name": "preserves", - "version": "0.6.1", + "version": "0.6.2", "description": "Experimental data serialization format", "homepage": "https://gitlab.com/preserves/preserves", "license": "Apache-2.0", diff --git a/implementations/javascript/src/annotated.ts b/implementations/javascript/src/annotated.ts index 356caa3..d47ff44 100644 --- a/implementations/javascript/src/annotated.ts +++ b/implementations/javascript/src/annotated.ts @@ -1,4 +1,4 @@ -import { Encoder } from "./codec"; +import { Encoder } from "./encoder"; import { Tag } from "./constants"; import { AsPreserve, PreserveOn } from "./symbols"; import { DefaultPointer, Value } from "./values"; diff --git a/implementations/javascript/src/bytes.ts b/implementations/javascript/src/bytes.ts index d62ac8e..897ad11 100644 --- a/implementations/javascript/src/bytes.ts +++ b/implementations/javascript/src/bytes.ts @@ -1,6 +1,6 @@ import { Tag } from './constants'; import { AsPreserve, PreserveOn } from './symbols'; -import { Encoder, Preservable } from './codec'; +import { Encoder, Preservable } from './encoder'; import { DefaultPointer, Value } from './values'; const textEncoder = new TextEncoder(); diff --git a/implementations/javascript/src/codec.ts b/implementations/javascript/src/codec.ts index 8320459..d80951b 100644 --- a/implementations/javascript/src/codec.ts +++ b/implementations/javascript/src/codec.ts @@ -1,28 +1,8 @@ // Preserves Binary codec. -import { Value } from './values'; -import { Tag } from './constants'; -import { PreserveOn } from './symbols'; -import { Bytes, BytesLike, underlying } from './bytes'; -import { Annotated } from './annotated'; -import { Set, Dictionary } from './dictionary'; -import { DoubleFloat, SingleFloat } from './float'; -import { Record, Tuple } from './record'; - export type ErrorType = 'DecodeError' | 'EncodeError' | 'ShortPacket'; export const ErrorType = Symbol.for('ErrorType'); -export type Encodable = - Value | Preservable | Iterable> | ArrayBufferView; - -export interface Preservable { - [PreserveOn](encoder: Encoder): void; -} - -export function isPreservable(v: any): v is Preservable { - return typeof v === 'object' && v !== null && typeof v[PreserveOn] === 'function'; -} - export abstract class PreservesCodecError { abstract get [ErrorType](): ErrorType; @@ -61,375 +41,3 @@ export class ShortPacket extends DecodeError { return PreservesCodecError.isCodecError(e, 'ShortPacket'); } } - -export interface DecoderOptions { - includeAnnotations?: boolean; - decodePointer?: (v: Value) => T; -} - -export class Decoder { - packet: Uint8Array; - index: number; - options: DecoderOptions; - - constructor(packet: BytesLike = new Uint8Array(0), options: DecoderOptions = {}) { - this.packet = underlying(packet); - this.index = 0; - this.options = options; - } - - get includeAnnotations(): boolean { - return this.options.includeAnnotations ?? false; - } - - write(data: BytesLike) { - this.packet = Bytes.concat([this.packet.slice(this.index), data])._view; - this.index = 0; - } - - nextbyte(): number { - if (this.index >= this.packet.length) throw new ShortPacket("Short packet"); - // ^ NOTE: greater-than-or-equal-to, not greater-than. - return this.packet[this.index++]; - } - - nextbytes(n: number): DataView { - const start = this.index; - this.index += n; - if (this.index > this.packet.length) throw new ShortPacket("Short packet"); - // ^ NOTE: greater-than, not greater-than-or-equal-to. - return new DataView(this.packet.buffer, this.packet.byteOffset + start, n); - } - - varint(): number { - // TODO: Bignums :-/ - const v = this.nextbyte(); - if (v < 128) return v; - return (this.varint() << 7) + (v - 128); - } - - peekend(): boolean { - const matched = this.nextbyte() === Tag.End; - if (!matched) this.index--; - return matched; - } - - nextvalues(): Value[] { - const result = []; - while (!this.peekend()) result.push(this.next()); - return result; - } - - nextint(n: number): number { - // TODO: Bignums :-/ - if (n === 0) return 0; - let acc = this.nextbyte(); - if (acc & 0x80) acc -= 256; - for (let i = 1; i < n; i++) acc = (acc * 256) + this.nextbyte(); - return acc; - } - - wrap(v: Value): Value { - return this.includeAnnotations ? new Annotated(v) : v; - } - - static dictionaryFromArray(vs: Value[]): Dictionary, T> { - const d = new Dictionary, T>(); - if (vs.length % 2) throw new DecodeError("Missing dictionary value"); - for (let i = 0; i < vs.length; i += 2) { - d.set(vs[i], vs[i+1]); - } - return d; - } - - unshiftAnnotation(a: Value, v: Annotated) { - if (this.includeAnnotations) { - v.annotations.unshift(a); - } - return v; - } - - next(): Value { - const tag = this.nextbyte(); - switch (tag) { - case Tag.False: return this.wrap(false); - case Tag.True: return this.wrap(true); - case Tag.Float: return this.wrap(new SingleFloat(this.nextbytes(4).getFloat32(0, false))); - case Tag.Double: return this.wrap(new DoubleFloat(this.nextbytes(8).getFloat64(0, false))); - case Tag.End: throw new DecodeError("Unexpected Compound end marker"); - case Tag.Annotation: { - const a = this.next(); - const v = this.next() as Annotated; - return this.unshiftAnnotation(a, v); - } - case Tag.Pointer: { - const d = this.options.decodePointer; - if (d === void 0) { - throw new DecodeError("No decodePointer function supplied"); - } - return this.wrap(d(this.next())); - } - case Tag.SignedInteger: return this.wrap(this.nextint(this.varint())); - case Tag.String: return this.wrap(Bytes.from(this.nextbytes(this.varint())).fromUtf8()); - case Tag.ByteString: return this.wrap(Bytes.from(this.nextbytes(this.varint()))); - case Tag.Symbol: return this.wrap(Symbol.for(Bytes.from(this.nextbytes(this.varint())).fromUtf8())); - case Tag.Record: { - const vs = this.nextvalues(); - if (vs.length === 0) throw new DecodeError("Too few elements in encoded record"); - return this.wrap(Record(vs[0], vs.slice(1))); - } - case Tag.Sequence: return this.wrap(this.nextvalues()); - case Tag.Set: return this.wrap(new Set(this.nextvalues())); - case Tag.Dictionary: return this.wrap(Decoder.dictionaryFromArray(this.nextvalues())); - default: { - if (tag >= Tag.SmallInteger_lo && tag <= Tag.SmallInteger_lo + 15) { - const v = tag - Tag.SmallInteger_lo; - return this.wrap(v > 12 ? v - 16 : v); - } - if (tag >= Tag.MediumInteger_lo && tag <= Tag.MediumInteger_lo + 15) { - const n = tag - Tag.MediumInteger_lo; - return this.wrap(this.nextint(n + 1)); - } - throw new DecodeError("Unsupported Preserves tag: " + tag); - } - } - } - - try_next(): Value | undefined { - const start = this.index; - try { - return this.next(); - } catch (e) { - if (ShortPacket.isShortPacket(e)) { - this.index = start; - return void 0; - } - throw e; - } - } -} - -export function decode(bs: BytesLike, options?: DecoderOptions) { - return new Decoder(bs, options).next(); -} - -export function decodeWithAnnotations(bs: BytesLike, options: DecoderOptions = {}): Annotated { - return decode(bs, { ... options, includeAnnotations: true }) as Annotated; -} - -export interface EncoderOptions { - canonical?: boolean; - includeAnnotations?: boolean; - encodePointer?: (v: T) => Value; -} - -function chunkStr(bs: Uint8Array): string { - return String.fromCharCode.apply(null, bs as any as number[]); -} - -function isIterable(v: any): v is Iterable { - return typeof v === 'object' && v !== null && typeof v[Symbol.iterator] === 'function'; -} - -export class Encoder { - chunks: Array; - view: DataView; - index: number; - options: EncoderOptions; - - constructor(options: EncoderOptions = {}) { - this.chunks = []; - this.view = new DataView(new ArrayBuffer(256)); - this.index = 0; - this.options = options; - } - - get canonical(): boolean { - return this.options.canonical ?? true; - } - - get includeAnnotations(): boolean { - return this.options.includeAnnotations ?? !this.canonical; - } - - contents(): Bytes { - if (this.chunks.length === 0) { - const resultLength = this.index; - this.index = 0; - return new Bytes(this.view.buffer.slice(0, resultLength)); - } else { - this.rotatebuffer(4096); - return Bytes.concat(this.chunks); - } - } - - /* Like contents(), but hands back a string containing binary data "encoded" via latin-1 */ - contentsString(): string { - if (this.chunks.length === 0) { - const s = chunkStr(new Uint8Array(this.view.buffer, 0, this.index)); - this.index = 0; - return s; - } else { - this.rotatebuffer(4096); - return this.chunks.map(chunkStr).join(''); - } - } - - rotatebuffer(size: number) { - this.chunks.push(new Uint8Array(this.view.buffer, 0, this.index)); - this.view = new DataView(new ArrayBuffer(size)); - this.index = 0; - } - - makeroom(amount: number) { - if (this.index + amount > this.view.byteLength) { - this.rotatebuffer(amount + 4096); - } - } - - emitbyte(b: number) { - this.makeroom(1); - this.view.setUint8(this.index++, b); - } - - emitbytes(bs: Uint8Array) { - this.makeroom(bs.length); - (new Uint8Array(this.view.buffer)).set(bs, this.index); - this.index += bs.length; - } - - varint(v: number) { - while (v >= 128) { - this.emitbyte((v % 128) + 128); - v = Math.floor(v / 128); - } - this.emitbyte(v); - } - - encodeint(v: number) { - // TODO: Bignums :-/ - const plain_bitcount = Math.floor(Math.log2(v > 0 ? v : -(1 + v))) + 1; - const signed_bitcount = plain_bitcount + 1; - const bytecount = (signed_bitcount + 7) >> 3; - if (bytecount <= 16) { - this.emitbyte(Tag.MediumInteger_lo + bytecount - 1); - } else { - this.emitbyte(Tag.SignedInteger); - this.varint(bytecount); - } - const enc = (n: number, x: number) => { - if (n > 0) { - enc(n - 1, Math.floor(x / 256)); - this.emitbyte(x & 255); - } - }; - enc(bytecount, v); - } - - encodebytes(tag: Tag, bs: Uint8Array) { - this.emitbyte(tag); - this.varint(bs.length); - this.emitbytes(bs); - } - - encodevalues(tag: Tag, items: Iterable>) { - this.emitbyte(tag); - for (let i of items) { this.push(i); } - this.emitbyte(Tag.End); - } - - encoderawvalues(tag: Tag, items: BytesLike[]) { - this.emitbyte(tag); - items.forEach((i) => this.emitbytes(underlying(i))); - this.emitbyte(Tag.End); - } - - push(v: Encodable) { - if (isPreservable(v)) { - v[PreserveOn](this as unknown as Encoder); - } - else if (isPreservable(v)) { - v[PreserveOn](this); - } - else if (typeof v === 'boolean') { - this.emitbyte(v ? Tag.True : Tag.False); - } - else if (typeof v === 'number') { - if (v >= -3 && v <= 12) { - this.emitbyte(Tag.SmallInteger_lo + ((v + 16) & 0xf)); - } else { - this.encodeint(v); - } - } - else if (typeof v === 'string') { - this.encodebytes(Tag.String, new Bytes(v)._view); - } - else if (typeof v === 'symbol') { - const key = Symbol.keyFor(v); - if (key === void 0) throw new EncodeError("Cannot preserve non-global Symbol", v); - this.encodebytes(Tag.Symbol, new Bytes(key)._view); - } - else if (ArrayBuffer.isView(v)) { - if (v instanceof Uint8Array) { - this.encodebytes(Tag.ByteString, v); - } else { - const bs = new Uint8Array(v.buffer, v.byteOffset, v.byteLength); - this.encodebytes(Tag.ByteString, bs); - } - } - else if (Record.isRecord, Tuple>, T>(v)) { - this.emitbyte(Tag.Record); - this.push(v.label); - for (let i of v) { this.push(i); } - this.emitbyte(Tag.End); - } - else if (Array.isArray(v)) { - this.encodevalues(Tag.Sequence, v); - } - else if (isIterable>(v)) { - this.encodevalues(Tag.Sequence, v as Iterable>); - } - else { - const e = this.options.encodePointer ?? pointerId; - this.emitbyte(Tag.Pointer); - this.push(e(v)); - } - return this; // for chaining - } -} - -export function encode(v: Encodable, options?: EncoderOptions): Bytes { - return new Encoder(options).push(v).contents(); -} - -let _nextId = 0; -const _registry = new WeakMap(); -export function pointerId(v: object): number { - let id = _registry.get(v); - if (id === void 0) { - id = _nextId++; - _registry.set(v, id); - } - return id; -} - -const _canonicalEncoder = new Encoder({ canonical: true }); -let _usingCanonicalEncoder = false; -export function canonicalEncode(v: Encodable, options?: EncoderOptions): Bytes { - if (options === void 0 && !_usingCanonicalEncoder) { - _usingCanonicalEncoder = true; - const bs = _canonicalEncoder.push(v).contents(); - _usingCanonicalEncoder = false; - return bs; - } else { - return encode(v, { ... options, canonical: true }); - } -} - -export function canonicalString(v: Encodable): string { - return _canonicalEncoder.push(v).contentsString(); -} - -export function encodeWithAnnotations(v: Encodable, options: EncoderOptions = {}): Bytes { - return encode(v, { ... options, includeAnnotations: true }); -} diff --git a/implementations/javascript/src/decoder.ts b/implementations/javascript/src/decoder.ts new file mode 100644 index 0000000..a730c50 --- /dev/null +++ b/implementations/javascript/src/decoder.ts @@ -0,0 +1,163 @@ +import { Annotated } from "./annotated"; +import { DecodeError, ShortPacket } from "./codec"; +import { Tag } from "./constants"; +import { Set, Dictionary } from "./dictionary"; +import { DoubleFloat, SingleFloat } from "./float"; +import { Record } from "./record"; +import { Bytes, BytesLike, underlying } from "./bytes"; +import { Value } from "./values"; + +export interface DecoderOptions { + includeAnnotations?: boolean; + decodePointer?: (v: Value) => T; +} + +export class Decoder { + packet: Uint8Array; + index: number; + options: DecoderOptions; + + constructor(packet: BytesLike = new Uint8Array(0), options: DecoderOptions = {}) { + this.packet = underlying(packet); + this.index = 0; + this.options = options; + } + + get includeAnnotations(): boolean { + return this.options.includeAnnotations ?? false; + } + + write(data: BytesLike) { + this.packet = Bytes.concat([this.packet.slice(this.index), data])._view; + this.index = 0; + } + + nextbyte(): number { + if (this.index >= this.packet.length) throw new ShortPacket("Short packet"); + // ^ NOTE: greater-than-or-equal-to, not greater-than. + return this.packet[this.index++]; + } + + nextbytes(n: number): DataView { + const start = this.index; + this.index += n; + if (this.index > this.packet.length) throw new ShortPacket("Short packet"); + // ^ NOTE: greater-than, not greater-than-or-equal-to. + return new DataView(this.packet.buffer, this.packet.byteOffset + start, n); + } + + varint(): number { + // TODO: Bignums :-/ + const v = this.nextbyte(); + if (v < 128) return v; + return (this.varint() << 7) + (v - 128); + } + + peekend(): boolean { + const matched = this.nextbyte() === Tag.End; + if (!matched) this.index--; + return matched; + } + + nextvalues(): Value[] { + const result = []; + while (!this.peekend()) result.push(this.next()); + return result; + } + + nextint(n: number): number { + // TODO: Bignums :-/ + if (n === 0) return 0; + let acc = this.nextbyte(); + if (acc & 0x80) acc -= 256; + for (let i = 1; i < n; i++) acc = (acc * 256) + this.nextbyte(); + return acc; + } + + wrap(v: Value): Value { + return this.includeAnnotations ? new Annotated(v) : v; + } + + static dictionaryFromArray(vs: Value[]): Dictionary, T> { + const d = new Dictionary, T>(); + if (vs.length % 2) throw new DecodeError("Missing dictionary value"); + for (let i = 0; i < vs.length; i += 2) { + d.set(vs[i], vs[i+1]); + } + return d; + } + + unshiftAnnotation(a: Value, v: Annotated) { + if (this.includeAnnotations) { + v.annotations.unshift(a); + } + return v; + } + + next(): Value { + const tag = this.nextbyte(); + switch (tag) { + case Tag.False: return this.wrap(false); + case Tag.True: return this.wrap(true); + case Tag.Float: return this.wrap(new SingleFloat(this.nextbytes(4).getFloat32(0, false))); + case Tag.Double: return this.wrap(new DoubleFloat(this.nextbytes(8).getFloat64(0, false))); + case Tag.End: throw new DecodeError("Unexpected Compound end marker"); + case Tag.Annotation: { + const a = this.next(); + const v = this.next() as Annotated; + return this.unshiftAnnotation(a, v); + } + case Tag.Pointer: { + const d = this.options.decodePointer; + if (d === void 0) { + throw new DecodeError("No decodePointer function supplied"); + } + return this.wrap(d(this.next())); + } + case Tag.SignedInteger: return this.wrap(this.nextint(this.varint())); + case Tag.String: return this.wrap(Bytes.from(this.nextbytes(this.varint())).fromUtf8()); + case Tag.ByteString: return this.wrap(Bytes.from(this.nextbytes(this.varint()))); + case Tag.Symbol: return this.wrap(Symbol.for(Bytes.from(this.nextbytes(this.varint())).fromUtf8())); + case Tag.Record: { + const vs = this.nextvalues(); + if (vs.length === 0) throw new DecodeError("Too few elements in encoded record"); + return this.wrap(Record(vs[0], vs.slice(1))); + } + case Tag.Sequence: return this.wrap(this.nextvalues()); + case Tag.Set: return this.wrap(new Set(this.nextvalues())); + case Tag.Dictionary: return this.wrap(Decoder.dictionaryFromArray(this.nextvalues())); + default: { + if (tag >= Tag.SmallInteger_lo && tag <= Tag.SmallInteger_lo + 15) { + const v = tag - Tag.SmallInteger_lo; + return this.wrap(v > 12 ? v - 16 : v); + } + if (tag >= Tag.MediumInteger_lo && tag <= Tag.MediumInteger_lo + 15) { + const n = tag - Tag.MediumInteger_lo; + return this.wrap(this.nextint(n + 1)); + } + throw new DecodeError("Unsupported Preserves tag: " + tag); + } + } + } + + try_next(): Value | undefined { + const start = this.index; + try { + return this.next(); + } catch (e) { + if (ShortPacket.isShortPacket(e)) { + this.index = start; + return void 0; + } + throw e; + } + } +} + +export function decode(bs: BytesLike, options?: DecoderOptions) { + return new Decoder(bs, options).next(); +} + +export function decodeWithAnnotations(bs: BytesLike, options: DecoderOptions = {}): Annotated { + return decode(bs, { ... options, includeAnnotations: true }) as Annotated; +} diff --git a/implementations/javascript/src/dictionary.ts b/implementations/javascript/src/dictionary.ts index 3c1a2c1..d620a93 100644 --- a/implementations/javascript/src/dictionary.ts +++ b/implementations/javascript/src/dictionary.ts @@ -1,6 +1,4 @@ -import type { Encoder } from "./codec"; - -import { canonicalEncode, canonicalString } from "./codec"; +import { Encoder, canonicalEncode, canonicalString } from "./encoder"; import { Tag } from "./constants"; import { FlexMap, FlexSet, _iterMap } from "./flex"; import { PreserveOn } from "./symbols"; diff --git a/implementations/javascript/src/encoder.ts b/implementations/javascript/src/encoder.ts new file mode 100644 index 0000000..5ef4b8e --- /dev/null +++ b/implementations/javascript/src/encoder.ts @@ -0,0 +1,234 @@ +import { Tag } from "./constants"; +import { Bytes, BytesLike, underlying } from "./bytes"; +import { Value } from "./values"; +import { PreserveOn } from "./symbols"; +import { EncodeError } from "./codec"; +import { Record, Tuple } from "./record"; + +export type Encodable = + Value | Preservable | Iterable> | ArrayBufferView; + +export interface Preservable { + [PreserveOn](encoder: Encoder): void; +} + +export function isPreservable(v: any): v is Preservable { + return typeof v === 'object' && v !== null && typeof v[PreserveOn] === 'function'; +} + +export interface EncoderOptions { + canonical?: boolean; + includeAnnotations?: boolean; + encodePointer?: (v: T) => Value; +} + +function chunkStr(bs: Uint8Array): string { + return String.fromCharCode.apply(null, bs as any as number[]); +} + +function isIterable(v: any): v is Iterable { + return typeof v === 'object' && v !== null && typeof v[Symbol.iterator] === 'function'; +} + +export class Encoder { + chunks: Array; + view: DataView; + index: number; + options: EncoderOptions; + + constructor(options: EncoderOptions = {}) { + this.chunks = []; + this.view = new DataView(new ArrayBuffer(256)); + this.index = 0; + this.options = options; + } + + get canonical(): boolean { + return this.options.canonical ?? true; + } + + get includeAnnotations(): boolean { + return this.options.includeAnnotations ?? !this.canonical; + } + + contents(): Bytes { + if (this.chunks.length === 0) { + const resultLength = this.index; + this.index = 0; + return new Bytes(this.view.buffer.slice(0, resultLength)); + } else { + this.rotatebuffer(4096); + return Bytes.concat(this.chunks); + } + } + + /* Like contents(), but hands back a string containing binary data "encoded" via latin-1 */ + contentsString(): string { + if (this.chunks.length === 0) { + const s = chunkStr(new Uint8Array(this.view.buffer, 0, this.index)); + this.index = 0; + return s; + } else { + this.rotatebuffer(4096); + return this.chunks.map(chunkStr).join(''); + } + } + + rotatebuffer(size: number) { + this.chunks.push(new Uint8Array(this.view.buffer, 0, this.index)); + this.view = new DataView(new ArrayBuffer(size)); + this.index = 0; + } + + makeroom(amount: number) { + if (this.index + amount > this.view.byteLength) { + this.rotatebuffer(amount + 4096); + } + } + + emitbyte(b: number) { + this.makeroom(1); + this.view.setUint8(this.index++, b); + } + + emitbytes(bs: Uint8Array) { + this.makeroom(bs.length); + (new Uint8Array(this.view.buffer)).set(bs, this.index); + this.index += bs.length; + } + + varint(v: number) { + while (v >= 128) { + this.emitbyte((v % 128) + 128); + v = Math.floor(v / 128); + } + this.emitbyte(v); + } + + encodeint(v: number) { + // TODO: Bignums :-/ + const plain_bitcount = Math.floor(Math.log2(v > 0 ? v : -(1 + v))) + 1; + const signed_bitcount = plain_bitcount + 1; + const bytecount = (signed_bitcount + 7) >> 3; + if (bytecount <= 16) { + this.emitbyte(Tag.MediumInteger_lo + bytecount - 1); + } else { + this.emitbyte(Tag.SignedInteger); + this.varint(bytecount); + } + const enc = (n: number, x: number) => { + if (n > 0) { + enc(n - 1, Math.floor(x / 256)); + this.emitbyte(x & 255); + } + }; + enc(bytecount, v); + } + + encodebytes(tag: Tag, bs: Uint8Array) { + this.emitbyte(tag); + this.varint(bs.length); + this.emitbytes(bs); + } + + encodevalues(tag: Tag, items: Iterable>) { + this.emitbyte(tag); + for (let i of items) { this.push(i); } + this.emitbyte(Tag.End); + } + + encoderawvalues(tag: Tag, items: BytesLike[]) { + this.emitbyte(tag); + items.forEach((i) => this.emitbytes(underlying(i))); + this.emitbyte(Tag.End); + } + + push(v: Encodable) { + if (isPreservable(v)) { + v[PreserveOn](this as unknown as Encoder); + } + else if (isPreservable(v)) { + v[PreserveOn](this); + } + else if (typeof v === 'boolean') { + this.emitbyte(v ? Tag.True : Tag.False); + } + else if (typeof v === 'number') { + if (v >= -3 && v <= 12) { + this.emitbyte(Tag.SmallInteger_lo + ((v + 16) & 0xf)); + } else { + this.encodeint(v); + } + } + else if (typeof v === 'string') { + this.encodebytes(Tag.String, new Bytes(v)._view); + } + else if (typeof v === 'symbol') { + const key = Symbol.keyFor(v); + if (key === void 0) throw new EncodeError("Cannot preserve non-global Symbol", v); + this.encodebytes(Tag.Symbol, new Bytes(key)._view); + } + else if (ArrayBuffer.isView(v)) { + if (v instanceof Uint8Array) { + this.encodebytes(Tag.ByteString, v); + } else { + const bs = new Uint8Array(v.buffer, v.byteOffset, v.byteLength); + this.encodebytes(Tag.ByteString, bs); + } + } + else if (Record.isRecord, Tuple>, T>(v)) { + this.emitbyte(Tag.Record); + this.push(v.label); + for (let i of v) { this.push(i); } + this.emitbyte(Tag.End); + } + else if (Array.isArray(v)) { + this.encodevalues(Tag.Sequence, v); + } + else if (isIterable>(v)) { + this.encodevalues(Tag.Sequence, v as Iterable>); + } + else { + const e = this.options.encodePointer ?? pointerId; + this.emitbyte(Tag.Pointer); + this.push(e(v)); + } + return this; // for chaining + } +} + +export function encode(v: Encodable, options?: EncoderOptions): Bytes { + return new Encoder(options).push(v).contents(); +} + +let _nextId = 0; +const _registry = new WeakMap(); +export function pointerId(v: object): number { + let id = _registry.get(v); + if (id === void 0) { + id = _nextId++; + _registry.set(v, id); + } + return id; +} + +const _canonicalEncoder = new Encoder({ canonical: true }); +let _usingCanonicalEncoder = false; +export function canonicalEncode(v: Encodable, options?: EncoderOptions): Bytes { + if (options === void 0 && !_usingCanonicalEncoder) { + _usingCanonicalEncoder = true; + const bs = _canonicalEncoder.push(v).contents(); + _usingCanonicalEncoder = false; + return bs; + } else { + return encode(v, { ... options, canonical: true }); + } +} + +export function canonicalString(v: Encodable): string { + return _canonicalEncoder.push(v).contentsString(); +} + +export function encodeWithAnnotations(v: Encodable, options: EncoderOptions = {}): Bytes { + return encode(v, { ... options, includeAnnotations: true }); +} diff --git a/implementations/javascript/src/float.ts b/implementations/javascript/src/float.ts index 4aa759f..56eb563 100644 --- a/implementations/javascript/src/float.ts +++ b/implementations/javascript/src/float.ts @@ -1,7 +1,7 @@ +import { Encoder, Preservable } from "./encoder"; import { Tag } from "./constants"; import { AsPreserve, PreserveOn } from "./symbols"; import { DefaultPointer, Value } from "./values"; -import { Encoder, Preservable } from "./codec"; export type FloatType = 'Single' | 'Double'; export const FloatType = Symbol.for('FloatType'); diff --git a/implementations/javascript/src/index.ts b/implementations/javascript/src/index.ts index 8307f84..3781d03 100644 --- a/implementations/javascript/src/index.ts +++ b/implementations/javascript/src/index.ts @@ -1,7 +1,9 @@ export * from './annotated'; export * from './bytes'; export * from './codec'; +export * from './decoder'; export * from './dictionary'; +export * from './encoder'; export * from './flex'; export * from './float'; export * from './fold';