|
| 1 | +// runtime:serialization — entry bundled into |
| 2 | +// crates/runtime/src/runtime_modules/serialization.js (via `bun run build`). |
| 3 | +// |
| 4 | +// XML/YAML/TOML/JSONL/MessagePack are thin wrappers over the Rust host ops; |
| 5 | +// Protobuf is a pure-JS reflective implementation (./protobuf). |
| 6 | +export { Protobuf } from "./protobuf/schema.js"; |
| 7 | + |
| 8 | +const ops = (globalThis as unknown as { __ops: Record<string, (...a: any[]) => any> }).__ops; |
| 9 | +const { |
| 10 | + xml_parse, xml_validate, xml_build, |
| 11 | + yaml_parse, yaml_validate, yaml_build, |
| 12 | + toml_parse, toml_validate, toml_build, |
| 13 | + msgpack_parse, msgpack_validate, msgpack_build, |
| 14 | + xml_stream_new, xml_stream_push, xml_stream_close, |
| 15 | +} = ops; |
| 16 | + |
| 17 | +interface ValidateOptions { detailed?: boolean; } |
| 18 | +function validateWith(fn: (s: any) => true | string, input: any, options: ValidateOptions = {}) { |
| 19 | + const result = fn(input); |
| 20 | + if (result === true) return options.detailed ? { valid: true } : true; |
| 21 | + return options.detailed ? { valid: false, error: result } : false; |
| 22 | +} |
| 23 | + |
| 24 | +export const TOML = { |
| 25 | + validate: (toml: string, options?: ValidateOptions) => validateWith(toml_validate, toml, options), |
| 26 | + parse: (toml: string) => toml_parse(toml), |
| 27 | + build: (obj: unknown) => toml_build(obj), |
| 28 | +}; |
| 29 | + |
| 30 | +export const YAML = { |
| 31 | + validate: (yaml: string, options?: ValidateOptions) => validateWith(yaml_validate, yaml, options), |
| 32 | + parse: (yaml: string) => yaml_parse(yaml), |
| 33 | + build: (obj: unknown) => yaml_build(obj), |
| 34 | +}; |
| 35 | + |
| 36 | +export const MessagePack = { |
| 37 | + validate: (msgpack: Uint8Array, options?: ValidateOptions) => validateWith(msgpack_validate, msgpack, options), |
| 38 | + decode: (msgpack: Uint8Array) => JSON.parse(msgpack_parse(msgpack)), |
| 39 | + encode: (obj: unknown) => msgpack_build(obj), |
| 40 | +}; |
| 41 | + |
| 42 | +class JSONLDecoderStream extends TransformStream { |
| 43 | + onError: (cb: (e: { line: number; raw: string; cause: Error }) => void) => void; |
| 44 | + constructor(options: { skipInvalid?: boolean } = {}) { |
| 45 | + let buffer = ""; |
| 46 | + const decoder = new TextDecoder(); |
| 47 | + const skipInvalid = !!options.skipInvalid; |
| 48 | + let lineNumber = 0; |
| 49 | + let errorCallback: ((e: { line: number; raw: string; cause: Error }) => void) | null = null; |
| 50 | + |
| 51 | + const emit = (controller: TransformStreamDefaultController, raw: string) => { |
| 52 | + const trimmed = raw.trim(); |
| 53 | + if (!trimmed) return; |
| 54 | + try { |
| 55 | + controller.enqueue(JSON.parse(trimmed)); |
| 56 | + } catch (err) { |
| 57 | + if (skipInvalid) errorCallback?.({ line: lineNumber, raw: trimmed, cause: err as Error }); |
| 58 | + else controller.error(new SyntaxError(`Invalid JSONL line ${lineNumber}: ${(err as Error).message}`)); |
| 59 | + } |
| 60 | + }; |
| 61 | + |
| 62 | + super({ |
| 63 | + transform(chunk, controller) { |
| 64 | + const text = typeof chunk === "string" ? chunk : decoder.decode(chunk, { stream: true }); |
| 65 | + buffer += text; |
| 66 | + const lines = buffer.split("\n"); |
| 67 | + buffer = lines.pop() ?? ""; |
| 68 | + for (const line of lines) { lineNumber++; emit(controller, line); } |
| 69 | + }, |
| 70 | + flush(controller) { |
| 71 | + if (buffer) { lineNumber++; emit(controller, buffer); } |
| 72 | + }, |
| 73 | + }); |
| 74 | + |
| 75 | + this.onError = (cb) => { errorCallback = cb; }; |
| 76 | + } |
| 77 | +} |
| 78 | + |
| 79 | +class JSONLEncoderStream extends TransformStream { |
| 80 | + private _writer: WritableStreamDefaultWriter | null = null; |
| 81 | + constructor() { |
| 82 | + super({ |
| 83 | + transform(chunk, controller) { |
| 84 | + try { |
| 85 | + controller.enqueue(JSON.stringify(chunk) + "\n"); |
| 86 | + } catch (err) { |
| 87 | + controller.error(new TypeError(`Cannot serialize to JSONL: ${(err as Error).message}`)); |
| 88 | + } |
| 89 | + }, |
| 90 | + }); |
| 91 | + } |
| 92 | + pipeTo(destination: WritableStream, options?: StreamPipeOptions) { |
| 93 | + return this.readable.pipeTo(destination, options); |
| 94 | + } |
| 95 | + write(chunk: unknown) { |
| 96 | + this._writer ??= this.writable.getWriter(); |
| 97 | + return this._writer.write(chunk); |
| 98 | + } |
| 99 | + close() { |
| 100 | + return (this._writer ?? this.writable.getWriter()).close(); |
| 101 | + } |
| 102 | +} |
| 103 | + |
| 104 | +export const JSONL = { DecoderStream: JSONLDecoderStream, EncoderStream: JSONLEncoderStream }; |
| 105 | + |
| 106 | +class XMLDecoderStream extends TransformStream { |
| 107 | + constructor() { |
| 108 | + let streamId: number | null = null; |
| 109 | + super({ |
| 110 | + start() { streamId = xml_stream_new(); }, |
| 111 | + transform(chunk, controller) { |
| 112 | + const text = typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk); |
| 113 | + for (const obj of xml_stream_push(streamId, text)) controller.enqueue(obj); |
| 114 | + }, |
| 115 | + flush() { xml_stream_close(streamId); }, |
| 116 | + }); |
| 117 | + } |
| 118 | +} |
| 119 | + |
| 120 | +export const XML = { |
| 121 | + validate: (xml: string, options?: ValidateOptions) => validateWith(xml_validate, xml, options), |
| 122 | + parse: (xml: string) => xml_parse(xml), |
| 123 | + build: (obj: unknown) => xml_build(obj), |
| 124 | + DecoderStream: XMLDecoderStream, |
| 125 | +}; |
0 commit comments