UNPKG

@tai-kun/surrealdb

Version:

The SurrealDB SDK for JavaScript

1,011 lines (884 loc) 33 kB
import { CborMaxDepthReachedError, CborSyntaxError, CborTooLittleDataError, // CborTooMuchDataError, CborUnsafeMapKeyError, SurrealValueError, unreachable, } from "@tai-kun/surrealdb/errors"; import type { Uint8ArrayLike } from "@tai-kun/surrealdb/types"; import { utf8 } from "@tai-kun/surrealdb/utils"; import { getFloat16 } from "./_float"; import { ianaReviver } from "./_iana"; import { type AdditionalInfo, AI_EIGHT_BYTES, AI_FOUR_BYTES, AI_INDEFINITE_NUM_BYTES, AI_ONE_BYTE, AI_SIMPLE_FALSE, AI_SIMPLE_NULL, AI_SIMPLE_TRUE, AI_SIMPLE_UNDEFINED, AI_TWO_BYTES, BREAK, JS_MAX_SAFE_UNSIGNED_BIG_INTEGER, type MajorType, MT_ARRAY, MT_BYTE_STRING, MT_MAP, MT_NEGATIVE_INTEGER, MT_SIMPLE_FLOAT, MT_TAG, MT_UNSIGNED_INTEGER, MT_UTF8_STRING, Simple, } from "./spec"; import Tagged from "./tagged"; type InputValue = unknown; const PARENT_ROOT = 0; const PARENT_STR = 1; const PARENT_ARR = 2; const PARENT_OBJ = 3; const PARENT_MAP = 4; const PARENT_TAG = 5; namespace Parent { export type Root = { readonly $: typeof PARENT_ROOT; readonly put: (val: InputValue) => false; readonly out: () => unknown; val: unknown; }; export namespace Str { export type Byte = { readonly $: typeof PARENT_STR; readonly mt: typeof MT_BYTE_STRING; readonly put: (val: InputValue) => boolean; readonly out: () => Uint8Array; readonly acc: Uint8Array[]; size: number; }; export type Utf8 = { readonly $: typeof PARENT_STR; readonly mt: typeof MT_UTF8_STRING; readonly put: (val: InputValue) => boolean; readonly out: () => string; acc: string; }; } export type Str = | Str.Byte | Str.Utf8; export type Arr = { readonly $: typeof PARENT_ARR; readonly put: (val: InputValue) => boolean; readonly out: () => unknown[]; readonly acc: unknown[]; readonly len: number; idx: number; }; export namespace Obj { export type Obj = { readonly $: typeof PARENT_OBJ; readonly put: (val: InputValue) => boolean; readonly out: () => { [p: string]: unknown }; readonly acc: { [p: string]: unknown }; readonly v8n: IsSafeObjectKey; readonly len: number; idx: number; key: string | number | typeof NONE; }; export type Maq = { readonly $: typeof PARENT_MAP; readonly put: (val: InputValue) => boolean; readonly out: () => Map<unknown, unknown>; readonly acc: Map<unknown, unknown>; readonly v8n: IsSafeMapKey; readonly len: number; idx: number; key: unknown | typeof NONE; }; } export type Obj = | Obj.Obj | Obj.Maq; export type Tag = { readonly $: typeof PARENT_TAG; readonly put: (val: InputValue) => boolean; readonly out: () => unknown; readonly fns: readonly TaggedItemReviver[]; readonly tag: number | bigint; val: unknown; }; } type Parent = | Parent.Root | Parent.Str | Parent.Arr | Parent.Obj | Parent.Tag; const FRAGMENT_STR = 0; namespace Fragment { export type Str = { readonly $: typeof FRAGMENT_STR; readonly mt: typeof MT_BYTE_STRING | typeof MT_UTF8_STRING; readonly data: Uint8Array; readonly size: number; offset: number; }; } type Fragment = | Fragment.Str | never; // ここに追加 const HUMANIZED_DATA_ITEM = { [MT_UNSIGNED_INTEGER]: "unsigned integer", [MT_NEGATIVE_INTEGER]: "negative integer", [MT_BYTE_STRING]: "byte string", [MT_UTF8_STRING]: "utf8 string", [MT_ARRAY]: "array", [MT_MAP]: "map", [MT_TAG]: "tagged item", [MT_SIMPLE_FLOAT]: "simple/float", }; const NONE = Symbol.for("@tai-kun/surrealdb/cbor/none"); export const CONTINUE = Symbol.for("@tai-kun/surrealdb/cbor/continue"); export type TaggedItemReviver = (tagged: Tagged) => unknown | typeof CONTINUE; export type SimpleItemReviver = (simple: Simple) => unknown | typeof CONTINUE; export interface ReviverObject { readonly tagged?: TaggedItemReviver | undefined; readonly simple?: SimpleItemReviver | undefined; } export type Reviver = (value: Tagged | Simple) => unknown | typeof CONTINUE; export type IsSafeMapKey = ( key: unknown, map: Map<unknown, unknown>, ) => boolean; export type IsSafeObjectKey = ( key: string | number, obj: Record<string, unknown>, ) => boolean; export interface DecoderOptions { readonly isSafeMapKey?: IsSafeMapKey | undefined; readonly isSafeObjectKey?: IsSafeObjectKey | undefined; readonly mapType?: "Object" | "Map" | undefined; readonly maxDepth?: number | undefined; readonly reviver?: | Reviver | ReviverObject | readonly (Reviver | ReviverObject)[] | undefined; } export class _Decoder { protected readonly mapType: "Object" | "Map"; protected readonly maxDepth: number; protected readonly revivers: { tagged: TaggedItemReviver[]; simple: SimpleItemReviver[]; }; protected readonly isSafeMapKey: ( key: unknown, map: Map<unknown, unknown>, ) => boolean; protected readonly isSafeObjectKey: ( key: string | number, obj: Record<string, unknown>, ) => boolean; constructor(options: DecoderOptions | undefined = {}) { const { mapType = "Object", reviver, maxDepth = 64, isSafeMapKey = k => k !== "__proto__" && k !== "constructor", isSafeObjectKey = k => k !== "__proto__" && k !== "constructor", } = options; this.mapType = mapType; this.maxDepth = maxDepth; this.revivers = toRevivers(reviver); this.revivers.tagged.push(compatReviver, ianaReviver); this.isSafeMapKey = isSafeMapKey; this.isSafeObjectKey = isSafeObjectKey; this.depth = 0; this.parents = [ this.parent = { $: PARENT_ROOT, put(val) { if (val === BREAK) { // https://www.rfc-editor.org/rfc/rfc8949.html#section-appendix.f-4.4 throw new CborSyntaxError( `The "break" stop code was encountered outside of ` + "a indefinite-length data item.", ); } this.val = val; return false; }, out() { return this.val; }, val: undefined, }, ]; this.fragment = null; } protected depth: number; protected readonly parents: [Parent.Root, ...Exclude<Parent, Parent.Root>[]]; protected parent: Parent; protected fragment: Fragment | null; protected putValue(value: InputValue): void { // @ts-ignore (TS6133): 何故かエラーになる let done = false; while ((done = this.parent.put(value))) { this.end(); value = this.parents.pop()!.out(); this.parent = this.parents[this.parents.length - 1]!; } } protected begin(parent: Exclude<Parent, Parent.Root>): void { if (parent.$ !== PARENT_STR && ++this.depth >= this.maxDepth) { throw new CborMaxDepthReachedError(this.maxDepth); } this.parents.push(this.parent = parent); } private end(): void { if (this.parent.$ !== PARENT_STR) { this.depth -= 1; } } process(input: Uint8ArrayLike): void { let offset = 0; const fragment = this.fragment; if (fragment !== null) { switch (fragment.$) { case FRAGMENT_STR: { const remaining = fragment.size - fragment.offset; const chunk = input.subarray(offset, offset += remaining); fragment.data.set(chunk, fragment.offset); if (chunk.length < remaining) { fragment.offset += chunk.length; return; // input の最後に到達しているので早期リターン } let value: string | Uint8ArrayLike = fragment.data; if (fragment.mt === MT_UTF8_STRING) { value = utf8.decode(value); } this.putValue(value); this.fragment = null; break; } default: unreachable(fragment as never); } } const view = new DataView(input.buffer, input.byteOffset, input.byteLength); const viewLength = view.byteLength; while (offset < viewLength) { const ib = view.getUint8(offset++); let mt = (ib >> 5) as MajorType; let ai = (ib & 0x1f) as AdditionalInfo; let value; let isSimple = false; // Not a float let payloadLength = 0; switch (ai) { // # Short Data Item (24 <= ai <= 27) // +--------------+--------------------------------+---------------------------+-------------------------------------+ // | Byte count | 1 byte (CBOR data item header) | 1, 2, 4 or 8 Bytes | 0 ~ 2^64-1 Bytes | // +--------------+------------+-------------------+---------------------------+-------------------------------------+ // | Structure | Major type | Additional info | Additional info (Content) | Payload | // +--------------+------------+-------------------+---------------------------+----------+--------------------------+ // | Bit count | 3 Bits | 5 Bits | 8, 16, 32 or 64 Bits | 8 Bits | ... | // +--------------+------------+-------------------+---------------------------+----------+--------------------------+ // | Unsigned Int | 0 | 24, 25, 26, 27 | 0 ~ 2^64-1 (Value) | N/A | // +--------------+------------+-------------------+---------------------------+-------------------------------------+ // | Negative Int | 1 | 24, 25, 26, 27 | 0 ~ 2^64-1 | N/A | // +--------------+------------+-------------------+---------------------------+----------+---------+----------------+ // | Bytes / Text | 2, 3 | 24, 25, 26, 27 | count := 0 ~ 2^64-1 | bytes[0] | ... | bytes[<count>] | // +--------------+------------+-------------------+---------------------------+----------+---------+----------------+ // | Array / Map | 4, 5 | 24, 25, 26, 27 | 0 ~ 2^64-1 [items] | N/A (Subsequent data items / pairs) | // +--------------+------------+-------------------+---------------------------+-------------------------------------+ // | Tag | 6 | 24, 25, 26, 27 | 0 ~ 2^64-1 (Value) | N/A (A subsequent data item) | // +--------------+------------+-------------------+---------------------------+-------------------------------------+ // | Float | 7 | 25, 26, 27 | (IEEE 754 Float) | N/A | // +--------------+------------+-------------------+---------------------------+-------------------------------------+ case AI_ONE_BYTE: // 24 payloadLength = 1; value = view.getUint8(offset); if (mt === MT_SIMPLE_FLOAT) { if (value < 0x20) { // エンコーダーは、0xf8 (Major Type: 7, Additional Info: 24) で始まり、 // 0x20 未満のバイトで続く 2 バイトシーケンスを発行してはならない。 // https://www.rfc-editor.org/rfc/rfc8949.html#section-3.3-5 // https://www.rfc-editor.org/rfc/rfc8949.html#section-appendix.f-4.2 throw new CborSyntaxError( "2-byte sequence followed by a byte less than 0x20 is being" + " issued after the header 0xf8.", ); } isSimple = true; } break; case AI_TWO_BYTES: // 25 payloadLength = 2; value = mt === MT_SIMPLE_FLOAT ? getFloat16(view, offset) : view.getUint16(offset); break; case AI_FOUR_BYTES: // 26 payloadLength = 4; value = mt === MT_SIMPLE_FLOAT ? view.getFloat32(offset) : view.getUint32(offset); break; case AI_EIGHT_BYTES: // 27 payloadLength = 8; if (mt === MT_SIMPLE_FLOAT) { value = view.getFloat64(offset); } else { value = view.getBigUint64(offset); if (mt === MT_NEGATIVE_INTEGER) { // 負の値は -1 から value を引いた数なので、最大値より 1 以上小さくないと // Number にはなれない。 if (value <= JS_MAX_SAFE_UNSIGNED_BIG_INTEGER - 1n) { value = Number(value); } } else if (value <= JS_MAX_SAFE_UNSIGNED_BIG_INTEGER) { value = Number(value); } } break; // # Long Data Item (ai == 31) // +--------------+--------------------------------+--------------------------------------+ // | Byte count | 1 byte (CBOR data item header) | 0 ~ ? Bytes | // +--------------+------------+-------------------+--------------------------------------+ // | Structure | Major type | Additional info | Payload | // +--------------+------------+-------------------+----------+---------------------------+ // | Bit count | 3 Bits | 5 Bits | 8 Bits | ... | // +--------------+------------+-------------------+----------+-----+---------------------+ // | Bytes / Text | 2, 3 | 31 | bytes[0] | ... | Break (Non-payload) | // +--------------+------------+-------------------+----------+-----+---------------------+ // | Array / Map | 4, 5 | 31 | N/A (Subsequent data items / pairs) | // +--------------+------------+-------------------+--------------------------------------+ // | Break | 7 | 31 | N/A | // +--------------+------------+-------------------+--------------------------------------+ case AI_INDEFINITE_NUM_BYTES: // 31 switch (mt) { case MT_UNSIGNED_INTEGER: case MT_NEGATIVE_INTEGER: case MT_TAG: // https://www.rfc-editor.org/rfc/rfc8949.html#section-appendix.f-4.5 throw new CborSyntaxError( `The data item \`${HUMANIZED_DATA_ITEM[mt]}\` has ` + "indefinite-length bytes.", ); case MT_SIMPLE_FLOAT: if ( (this.parent.$ === PARENT_ARR || this.parent.$ === PARENT_OBJ || this.parent.$ === PARENT_MAP) && this.parent.len !== Infinity ) { throw new CborSyntaxError( `The definite-length data item \`${HUMANIZED_DATA_ITEM[mt]}\`` + `contains the "break" stop code.`, ); } value = BREAK; break; case MT_BYTE_STRING: case MT_UTF8_STRING: if (this.parent.$ === PARENT_STR && mt === this.parent.mt) { // https://www.rfc-editor.org/rfc/rfc8949.html#section-appendix.f-4.3 throw new CborSyntaxError( "The payload of the indefinite-length data item `" + HUMANIZED_DATA_ITEM[mt] + "` contains the same type.", ); } value = Infinity; break; // case MT_ARRAY: // case MT_MAP: default: value = Infinity; } break; // # Invalid Data Item (28 <= ai <= 30) // +--------------+--------------------------------+ // | Byte count | 1 byte (CBOR data item header) | // +--------------+------------+-------------------+ // | Structure | Major type | Additional info | // +--------------+------------+-------------------+ // | Bit count | 3 Bits | 5 Bits | // +--------------+------------+-------------------+ // | N/A | 0 ~ 7 | 28 ~ 30 | // +--------------+------------+-------------------+ case 28: case 29: case 30: // https://www.rfc-editor.org/rfc/rfc8949.html#section-3-3.6 throw new CborSyntaxError( `The additional info ${ai} is reserved for future additions to the ` + "CBOR format.", ); // # Tiny Data Item (0 <= ai <= 23) // +--------------+--------------------------------+-------------------------------------+ // | Byte count | 1 byte (CBOR data item header) | 0 ~ 23 Bytes | // +--------------+------------+-------------------+-------------------------------------+ // | Structure | Major type | Additional info | Payload | // +--------------+------------+-------------------+----------+--------------------------+ // | Bit count | 3 Bits | 5 Bits | 8 Bits | ... | // +--------------+------------+-------------------+----------+--------------------------+ // | Unsigned Int | 0 | 0 ~ 23 (Value) | N/A | // +--------------+------------+-------------------+-------------------------------------+ // | Negative Int | 1 | 0 ~ 23 | N/A | // +--------------+------------+-------------------+----------+---------+----------------+ // | Bytes / Text | 2, 3 | count := 0 ~ 23 | bytes[0] | ... | bytes[<count>] | // +--------------+------------+-------------------+----------+---------+----------------+ // | Array / Map | 4, 5 | 0 ~ 23 [items] | N/A (Subsequent data items / pairs) | // +--------------+------------+-------------------+-------------------------------------+ // | Tag | 6 | 0 ~ 23 (Value) | N/A (A subsequent data item) | // +--------------+------------+-------------------+-------------------------------------+ // | Simple | 7 | 0 ~ 19 (Value) | N/A | // +--------------+------------+-------------------+-------------------------------------+ // | Boolean | 7 | 20, 21 | N/A | // +--------------+------------+-------------------+-------------------------------------+ // | Nullable | 7 | 22, 23 | N/A | // +--------------+------------+-------------------+-------------------------------------+ // 0 ~ 23 default: value = ai; // if (mt === MT_SIMPLE_FLOAT) isSimple = true; } offset += payloadLength; // ai が 1,2,3,4 バイトを示していなければ 0 加算になる。 switch (mt) { case MT_UNSIGNED_INTEGER: // 0 break; case MT_NEGATIVE_INTEGER: // 1 // 値は -1 から引数を引いた値 // https://www.rfc-editor.org/rfc/rfc8949.html#section-3.1-2.4 value = typeof value === "number" ? -1 - value : -1n - (value as bigint); break; case MT_BYTE_STRING: // 2 if (ai === AI_INDEFINITE_NUM_BYTES) { this.begin({ $: PARENT_STR, mt, put(val) { if (val === BREAK) { return true; // done } if (val instanceof Uint8Array) { this.acc.push(val); this.size += val.length; return false; // continue } unreachable(val as never); }, out() { const data = new Uint8Array(this.size); for ( let offset = 0, chunk: Uint8Array | undefined; (chunk = this.acc.shift()); ) { data.set(chunk, offset); offset += chunk.length; } return data; }, size: 0, acc: [], }); continue; } else { // ここの処理は UTF8 文字列のと全く同じ payloadLength = value as number; value = input.subarray(offset, offset += payloadLength); if (value.length < payloadLength) { const data = new Uint8Array(payloadLength); data.set(value); this.fragment = { $: FRAGMENT_STR, mt, data, size: data.length, offset: value.length, }; return; // input の最後に到達しているので早期リターン } } break; case MT_UTF8_STRING: // 3 if (ai === AI_INDEFINITE_NUM_BYTES) { this.begin({ $: PARENT_STR, mt, put(val) { if (val === BREAK) { return true; // done } if (typeof val === "string") { this.acc += val; return false; // continue } unreachable(val as never); }, out() { return this.acc; }, acc: "", }); continue; } else { // ここの処理はバイト文字列のと全く同じ payloadLength = value as number; value = input.subarray(offset, offset += payloadLength); if (value.length < payloadLength) { const data = new Uint8Array(payloadLength); data.set(value); this.fragment = { $: FRAGMENT_STR, mt, data, size: data.length, offset: value.length, }; return; // input の最後に到達しているので早期リターン } // ここだけ違う。デコードはする。 value = utf8.decode(value); } break; case MT_ARRAY: // 4 if (ai === AI_INDEFINITE_NUM_BYTES) { this.begin({ $: PARENT_ARR, put(val) { if (val === BREAK) { return true; // done } this.acc.push(val); return false; // continue }, out() { return this.acc; }, len: Infinity, idx: NaN, acc: [], }); continue; } else if (value !== 0) { this.begin({ $: PARENT_ARR, put(val) { // 固定長配列 に対する BREAK 違反は検証済み。 this.acc.push(val); return ++this.idx === this.len; }, out() { return this.acc; }, len: value as number, idx: 0, acc: [], }); continue; } else { value = []; } break; case MT_MAP: // 5 if (this.mapType === "Object") { if (ai === AI_INDEFINITE_NUM_BYTES) { this.begin({ $: PARENT_OBJ, put(val) { if (val === BREAK) { return true; // done } if (this.key === NONE) { if ( (typeof val !== "string" && typeof val !== "number") || !this.v8n(val, this.acc) ) { throw new CborUnsafeMapKeyError(val); } this.key = val; } else { this.acc[this.key] = val; this.key = NONE; } return false; // continue }, out() { if (this.key === NONE) { return this.acc; } throw new CborTooLittleDataError({ cause: { key: this.key, }, }); }, v8n: this.isSafeObjectKey, len: Infinity, idx: Infinity, key: NONE, acc: {}, }); continue; } else if (value !== 0) { this.begin({ $: PARENT_OBJ, put(val) { if (this.key === NONE) { if ( (typeof val !== "string" && typeof val !== "number") || !this.v8n(val, this.acc) ) { throw new CborUnsafeMapKeyError(val); } this.key = val; } else { // 固定長オブジェクト に対する BREAK 違反は検証済み。 this.acc[this.key] = val; this.key = NONE; this.idx += 1; } return this.idx === this.len; }, out() { if (this.key === NONE) { return this.acc; } throw new CborTooLittleDataError({ cause: { key: this.key, }, }); }, v8n: this.isSafeObjectKey, len: value as number, idx: 0, key: NONE, acc: {}, }); continue; } else { value = {}; } } else if (this.mapType === "Map") { if (ai === AI_INDEFINITE_NUM_BYTES) { this.begin({ $: PARENT_MAP, put(val) { if (val === BREAK) { return true; // done } if (this.key === NONE) { if (!this.v8n(val, this.acc)) { throw new CborUnsafeMapKeyError(val); } this.key = val; } else { this.acc.set(this.key, val); this.key = NONE; } return false; // continue }, out() { if (this.key === NONE) { return this.acc; } throw new CborTooLittleDataError({ cause: { key: this.key, }, }); }, v8n: this.isSafeMapKey, len: Infinity, idx: Infinity, key: NONE, acc: new Map(), }); continue; } else if (value !== 0) { this.begin({ $: PARENT_MAP, put(val) { if (this.key === NONE) { if (!this.v8n(val, this.acc)) { throw new CborUnsafeMapKeyError(val); } // 固定長マップ に対する BREAK 違反は検証済み。 this.key = val; } else { // 固定長マップ に対する BREAK 違反は検証済み。 this.acc.set(this.key, val); this.key = NONE; this.idx += 1; } return this.idx === this.len; }, out() { if (this.key === NONE) { return this.acc; } throw new CborTooLittleDataError({ cause: { key: this.key, }, }); }, v8n: this.isSafeMapKey, len: value as number, idx: 0, key: NONE, acc: new Map(), }); continue; } else { value = new Map(); } } else { // 将来的にはカスタムオブジェクトを指定できるようにしたい。 throw new SurrealValueError( ["\"Object\"", "\"Object\""], this.mapType, ); } break; case MT_TAG: this.begin({ $: PARENT_TAG, put(val) { // タグ付きデータアイテム に対する BREAK 違反は検証済み。 this.val = val; return true; // done }, out() { return reviveValue(this.fns, new Tagged(this.tag, this.val)); }, fns: this.revivers.tagged, tag: value as number | bigint, val: undefined, }); continue; case MT_SIMPLE_FLOAT: // isSimple が true になるのは ai が 0~24 のときだけ。 if (isSimple) { switch (value) { case AI_SIMPLE_FALSE: // 20 value = false; break; case AI_SIMPLE_TRUE: // 21 value = true; break; case AI_SIMPLE_NULL: // 22 value = null; break; case AI_SIMPLE_UNDEFINED: // 23 value = undefined; break; default: // isSimple が true になるときは、少なくとも float ではないので、ここで // value を Simple オブジェクトにしても問題ない。 value = reviveValue( this.revivers.simple, new Simple(value as number), ); } } break; default: unreachable(mt); } if ( this.parent.$ === PARENT_STR && mt !== this.parent.mt && value !== BREAK ) { // https://www.rfc-editor.org/rfc/rfc8949.html#section-appendix.f-4.3 throw new CborSyntaxError( "The payload of the data item `" + HUMANIZED_DATA_ITEM[this.parent.mt] + "` contains a mismatched " + `data item \`${HUMANIZED_DATA_ITEM[mt]}\`.`, ); } this.putValue(value); } } output(): unknown { if (this.parent.$ !== PARENT_ROOT || this.fragment) { throw new CborTooLittleDataError({ cause: { parents: this.parents, fragment: this.fragment, }, }); } // TODO(tai-kun): クリーンアップ必要?一度出力して使いまわす機会ないと思う。 return this.parent.out(); } } function reviveValue<T>( revivers: readonly ((value: T) => unknown)[], value: T, ): unknown { for (let i = 0, len = revivers.length, ret; i < len; i++) { if ((ret = revivers[i]!(value)) !== CONTINUE) { return ret; } } return value; } function toRevivers(reviver: DecoderOptions["reviver"]): { tagged: TaggedItemReviver[]; simple: SimpleItemReviver[]; } { if (!reviver) { return { tagged: [], simple: [], }; } if (typeof reviver === "function") { return { tagged: [reviver], simple: [reviver], }; } if (Array.isArray(reviver)) { const tagged: TaggedItemReviver[] = []; const simple: SimpleItemReviver[] = []; for (const r of reviver) { if (typeof r === "function") { tagged.push(r); simple.push(r); } else { r.tagged && tagged.push(r.tagged); r.simple && simple.push(r.simple); } } return { tagged, simple, }; } return { tagged: reviver.tagged ? [reviver.tagged] : [], simple: reviver.simple ? [reviver.simple] : [], }; } function compatReviver(t: Tagged) { // https://github.com/surrealdb/surrealdb/blob/v2.0.1/core/src/rpc/format/cbor/convert.rs#L30 // https://github.com/surrealdb/surrealdb/blob/v2.0.1/core/src/rpc/format/cbor/convert.rs#L324 // より、NONE のタグは 6 で、その値は null if (t.tag === 6 && t.value === null) { return; // undefined } return CONTINUE; }