diff --git a/benchmarks/array.cjs b/benchmarks/array.cjs new file mode 100644 index 0000000..26ac275 --- /dev/null +++ b/benchmarks/array.cjs @@ -0,0 +1,38 @@ +const { Benchmark } = require('benchmark'); + +const n = 10000; + +function Person(name, age) { + this.name = name; + this.age = age; +} + +new Benchmark.Suite() +.add('init by constructor', () => { + const arr = new Array(n); + for (let i = 0; i < n; i++) { + arr[i] = new Person(i.toString(), i); + } +}) +.add('init by apply', () => { + const arr = Array(n); + for (let i = 0; i < n; i++) { + arr[i] = new Person(i.toString(), i); + } +}) +.add('init by literal + push', () => { + const arr = []; + for (let i = 0; i < n; i++) { + arr.push(new Person(i.toString(), i)); + } +}) +.add('init by literal + assign', () => { + const arr = []; + for (let i = 0; i < n; i++) { + arr[i] = new Person(i.toString(), i); + } +}) +.on('cycle', event => { + console.log(event.target.toString()); +}) +.run(); diff --git a/benchmarks/utf8-decode.js b/benchmarks/utf8-decode.cjs similarity index 77% rename from benchmarks/utf8-decode.js rename to benchmarks/utf8-decode.cjs index 95c3c22..9772877 100644 --- a/benchmarks/utf8-decode.js +++ b/benchmarks/utf8-decode.cjs @@ -2,31 +2,31 @@ const { Benchmark } = require('benchmark'); const textDecoder = new TextDecoder(); -const utf8DecodeJs = (function (bytes) { +const utf8DecodeJs = (bytes) => { let offset = 0; - const end = bytes.length; + let end = bytes.length; - const units = []; + let units = []; let result = ""; while (offset < end) { - const byte1 = bytes[offset++]; + let byte1 = bytes[offset++]; if ((byte1 & 0x80) === 0) { // 1 byte units.push(byte1); } else if ((byte1 & 0xe0) === 0xc0) { // 2 bytes - const byte2 = bytes[offset++] & 0x3f; + let byte2 = bytes[offset++] & 0x3f; units.push(((byte1 & 0x1f) << 6) | byte2); } else if ((byte1 & 0xf0) === 0xe0) { // 3 bytes - const byte2 = bytes[offset++] & 0x3f; - const byte3 = bytes[offset++] & 0x3f; + let byte2 = bytes[offset++] & 0x3f; + let byte3 = bytes[offset++] & 0x3f; units.push(((byte1 & 0x1f) << 12) | (byte2 << 6) | byte3); } else if ((byte1 & 0xf8) === 0xf0) { // 4 bytes - const byte2 = bytes[offset++] & 0x3f; - const byte3 = bytes[offset++] & 0x3f; - const byte4 = bytes[offset++] & 0x3f; + let byte2 = bytes[offset++] & 0x3f; + let byte3 = bytes[offset++] & 0x3f; + let byte4 = bytes[offset++] & 0x3f; let unit = ((byte1 & 0x07) << 0x12) | (byte2 << 0x0c) | (byte3 << 0x06) | byte4; if (unit > 0xffff) { unit -= 0x10000; @@ -49,7 +49,7 @@ const utf8DecodeJs = (function (bytes) { } return result; -}); +}; const textEncoded = new Uint8Array([0xac, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x21]); diff --git a/benchmarks/utf8-encode.cjs b/benchmarks/utf8-encode.cjs new file mode 100644 index 0000000..2e74719 --- /dev/null +++ b/benchmarks/utf8-encode.cjs @@ -0,0 +1,66 @@ +const { Benchmark } = require('benchmark'); + +const textEncoder = new TextEncoder(); + +const utf8EncodeJs = (str) => { + let strLength = str.length; + let output = []; + let offset = 0; + let pos = 0; + while (pos < strLength) { + let value = str.charCodeAt(pos++); + + if ((value & 0xffffff80) === 0) { + // 1-byte + output[offset++] = value; + continue; + } else if ((value & 0xfffff800) === 0) { + // 2-bytes + output[offset++] = ((value >> 6) & 0x1f) | 0xc0; + } else { + // handle surrogate pair + if (value >= 0xd800 && value <= 0xdbff) { + // high surrogate + if (pos < strLength) { + let extra = str.charCodeAt(pos); + if ((extra & 0xfc00) === 0xdc00) { + ++pos; + value = ((value & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000; + } + } + } + + if ((value & 0xffff0000) === 0) { + // 3-byte + output[offset++] = ((value >> 12) & 0x0f) | 0xe0; + output[offset++] = ((value >> 6) & 0x3f) | 0x80; + } else { + // 4-byte + output[offset++] = ((value >> 18) & 0x07) | 0xf0; + output[offset++] = ((value >> 12) & 0x3f) | 0x80; + output[offset++] = ((value >> 6) & 0x3f) | 0x80; + } + } + + output[offset++] = (value & 0x3f) | 0x80; + } + + return new Uint8Array(output); +} + +const text = 'μ•ˆλ…•, 세상!'; + +console.log(textEncoder.encode(text)); +console.log(utf8EncodeJs(text)); + +new Benchmark.Suite() +.add('TextEncoder', () => { + textEncoder.encode(text); +}) +.add('utf8EncodeJs', () => { + utf8EncodeJs(text); +}) +.on('cycle', event => { + console.log(event.target.toString()); +}) +.run(); diff --git a/packages/msgpack/.gitignore b/packages/msgpack/.gitignore index 12c18d4..6721fa0 100644 --- a/packages/msgpack/.gitignore +++ b/packages/msgpack/.gitignore @@ -1 +1,5 @@ +.merlin +.bsb.lock +*.bs.js + /lib/ diff --git a/packages/msgpack/bsconfig.json b/packages/msgpack/bsconfig.json new file mode 100644 index 0000000..2e158ac --- /dev/null +++ b/packages/msgpack/bsconfig.json @@ -0,0 +1,21 @@ +{ + "name": "@urlpack/msgpack", + "sources": [ + { + "dir": "src", + "subdirs": true + } + ], + "package-specs": [ + { + "module": "es6", + "in-source": true + } + ], + "suffix": ".bs.js", + "bs-dependencies": [ + "rescript-msgpack" + ], + "bs-dev-dependencies": [ + ] +} diff --git a/packages/msgpack/package.json b/packages/msgpack/package.json index b480d60..7fa047b 100644 --- a/packages/msgpack/package.json +++ b/packages/msgpack/package.json @@ -26,20 +26,25 @@ "main": "./lib/index.mjs" }, "files": [ - "src", - "lib" + "src/**/*.ts", + "src/*.res", + "lib/*.mjs", + "lib/*.cjs", + "lib/*.d.ts", + "lib/*.map" ], "scripts": { "prepack": "yarn build", - "build": "nanobundle build --standalone", + "build": "rescript build && nanobundle build --standalone", "test": "uvu -r tsm", "test:watch": "yarn test || true && watchlist src tests -- yarn test" }, "devDependencies": { - "@rescript/std": "^9.1.3", "concurrently": "^6.5.1", "esbuild": "^0.14.9", "nanobundle": "^0.0.21", + "rescript": "^9.1.4", + "rescript-msgpack": "workspace:^0.1.0", "tsm": "^2.2.1", "typescript": "^4.5.4", "uvu": "^0.5.2", diff --git a/packages/msgpack/src/decoder.bs.js b/packages/msgpack/src/decoder.bs.js deleted file mode 100644 index a859c83..0000000 --- a/packages/msgpack/src/decoder.bs.js +++ /dev/null @@ -1,564 +0,0 @@ -// Generated by ReScript, PLEASE EDIT WITH CARE - -import * as Js_exn from "@rescript/std/lib/es6/js_exn.js"; -import * as Js_dict from "@rescript/std/lib/es6/js_dict.js"; -import * as Pervasives from "@rescript/std/lib/es6/pervasives.js"; -import { timestampDecoder as timestampExt } from "./ext/TimestampDecoder"; - -var decode2 = (function (_t, bytes) { - let offset = 0; - const end = bytes.length; - - const units = []; - let result = ""; - while (offset < end) { - const byte1 = bytes[offset++]; - if ((byte1 & 0x80) === 0) { - // 1 byte - units.push(byte1); - } else if ((byte1 & 0xe0) === 0xc0) { - // 2 bytes - const byte2 = bytes[offset++] & 0x3f; - units.push(((byte1 & 0x1f) << 6) | byte2); - } else if ((byte1 & 0xf0) === 0xe0) { - // 3 bytes - const byte2 = bytes[offset++] & 0x3f; - const byte3 = bytes[offset++] & 0x3f; - units.push(((byte1 & 0x1f) << 12) | (byte2 << 6) | byte3); - } else if ((byte1 & 0xf8) === 0xf0) { - // 4 bytes - const byte2 = bytes[offset++] & 0x3f; - const byte3 = bytes[offset++] & 0x3f; - const byte4 = bytes[offset++] & 0x3f; - let unit = ((byte1 & 0x07) << 0x12) | (byte2 << 0x0c) | (byte3 << 0x06) | byte4; - if (unit > 0xffff) { - unit -= 0x10000; - units.push(((unit >>> 10) & 0x3ff) | 0xd800); - unit = 0xdc00 | (unit & 0x3ff); - } - units.push(unit); - } else { - units.push(byte1); - } - - if (units.length >= 0x1_000) { - result += String.fromCharCode(...units); - units.length = 0; - } - } - - if (units.length > 0) { - result += String.fromCharCode(...units); - } - - return result; - }); - -var $$TextDecoder = { - decode2: decode2 -}; - -var flip64 = (function(binary) { - let carry = 1; - for (let i = 7; i >= 0; i--) { - const v = (binary[i] ^ 0xff) + carry; - binary[i] = v & 0xff; - carry = v >> 8; - } -}); - -function make(extensions) { - var textDecoder = new TextDecoder(); - var extensions$1 = Js_dict.fromArray(extensions); - return { - textDecoder: textDecoder, - extensions: extensions$1 - }; -} - -function decode(t, binary) { - var extensions = t.extensions; - var textDecoder = t.textDecoder; - var binary$1 = binary.slice(); - var view = new DataView(binary$1.buffer); - var decode$1 = function (binary, _state, _cursor) { - while(true) { - var cursor = _cursor; - var state = _state; - if (typeof state === "number") { - var header = view.getUint8(cursor); - var cursor$1 = cursor + 1 | 0; - if (header < 128) { - _cursor = cursor$1; - _state = { - TAG: /* Done */5, - _0: header - }; - continue ; - } - if (header < 144) { - var len = header & 15; - _cursor = cursor$1; - _state = { - TAG: /* DecodeMap */2, - _0: len, - _1: {} - }; - continue ; - } - if (header < 160) { - var len$1 = header & 15; - _cursor = cursor$1; - _state = { - TAG: /* DecodeArray */1, - _0: len$1, - _1: new Array(len$1) - }; - continue ; - } - if (header < 192) { - var len$2 = header & 31; - _cursor = cursor$1; - _state = { - TAG: /* DecodeString */0, - _0: len$2 - }; - continue ; - } - switch (header) { - case 192 : - _cursor = cursor$1; - _state = { - TAG: /* Done */5, - _0: null - }; - continue ; - case 193 : - break; - case 194 : - _cursor = cursor$1; - _state = { - TAG: /* Done */5, - _0: false - }; - continue ; - case 195 : - _cursor = cursor$1; - _state = { - TAG: /* Done */5, - _0: true - }; - continue ; - case 196 : - var len$3 = view.getUint8(cursor$1); - _cursor = cursor$1 + 1 | 0; - _state = { - TAG: /* DecodeBinary */3, - _0: len$3 - }; - continue ; - case 197 : - var len$4 = view.getUint16(cursor$1); - _cursor = cursor$1 + 2 | 0; - _state = { - TAG: /* DecodeBinary */3, - _0: len$4 - }; - continue ; - case 198 : - var len$5 = view.getUint32(cursor$1); - _cursor = cursor$1 + 4 | 0; - _state = { - TAG: /* DecodeBinary */3, - _0: len$5 - }; - continue ; - case 199 : - var len$6 = view.getUint8(cursor$1); - var type_ = view.getInt8(cursor$1 + 1 | 0); - if (type_ === timestampExt.type) { - _cursor = cursor$1 + 2 | 0; - _state = { - TAG: /* DecodeExt */4, - _0: len$6, - _1: timestampExt - }; - continue ; - } - var ext = Js_dict.get(extensions, String(type_)); - if (ext === undefined) { - return Js_exn.raiseError("Unknown extension type " + String(type_)); - } - _cursor = cursor$1 + 2 | 0; - _state = { - TAG: /* DecodeExt */4, - _0: len$6, - _1: ext - }; - continue ; - case 200 : - var len$7 = view.getUint16(cursor$1); - var type_$1 = view.getInt8(cursor$1 + 2 | 0); - var ext$1 = Js_dict.get(extensions, String(type_$1)); - if (ext$1 === undefined) { - return Js_exn.raiseError("Unknown extension type " + String(type_$1)); - } - _cursor = cursor$1 + 2 | 0; - _state = { - TAG: /* DecodeExt */4, - _0: len$7, - _1: ext$1 - }; - continue ; - case 201 : - var len$8 = view.getUint32(cursor$1); - var type_$2 = view.getInt8(cursor$1 + 4 | 0); - var ext$2 = Js_dict.get(extensions, String(type_$2)); - if (ext$2 === undefined) { - return Js_exn.raiseError("Unknown extension type " + String(type_$2)); - } - _cursor = cursor$1 + 4 | 0; - _state = { - TAG: /* DecodeExt */4, - _0: len$8, - _1: ext$2 - }; - continue ; - case 202 : - var num = view.getFloat32(cursor$1); - _cursor = cursor$1 + 4 | 0; - _state = { - TAG: /* Done */5, - _0: num - }; - continue ; - case 203 : - var num$1 = view.getFloat64(cursor$1); - _cursor = cursor$1 + 8 | 0; - _state = { - TAG: /* Done */5, - _0: num$1 - }; - continue ; - case 204 : - var num$2 = view.getUint8(cursor$1); - _cursor = cursor$1 + 1 | 0; - _state = { - TAG: /* Done */5, - _0: num$2 - }; - continue ; - case 205 : - var num$3 = view.getUint16(cursor$1); - _cursor = cursor$1 + 2 | 0; - _state = { - TAG: /* Done */5, - _0: num$3 - }; - continue ; - case 206 : - var num$4 = view.getUint32(cursor$1); - _cursor = cursor$1 + 4 | 0; - _state = { - TAG: /* Done */5, - _0: num$4 - }; - continue ; - case 207 : - var hi = view.getUint32(cursor$1); - var lo = view.getUint32(cursor$1 + 4 | 0); - var num$5 = hi * Math.pow(256.0, 4.0) + lo; - _cursor = cursor$1 + 8 | 0; - _state = { - TAG: /* Done */5, - _0: num$5 - }; - continue ; - case 208 : - var num$6 = view.getInt8(cursor$1); - _cursor = cursor$1 + 1 | 0; - _state = { - TAG: /* Done */5, - _0: num$6 - }; - continue ; - case 209 : - var num$7 = view.getInt16(cursor$1); - _cursor = cursor$1 + 2 | 0; - _state = { - TAG: /* Done */5, - _0: num$7 - }; - continue ; - case 210 : - var num$8 = view.getInt32(cursor$1); - _cursor = cursor$1 + 4 | 0; - _state = { - TAG: /* Done */5, - _0: num$8 - }; - continue ; - case 211 : - flip64(binary.subarray(cursor$1, cursor$1 + 9 | 0)); - var hi$1 = view.getUint32(cursor$1); - var lo$1 = view.getUint32(cursor$1 + 4 | 0); - var num$9 = hi$1 * Math.pow(256.0, 4.0) + lo$1; - _cursor = cursor$1 + 8 | 0; - _state = { - TAG: /* Done */5, - _0: 0.0 - num$9 - }; - continue ; - case 212 : - var type_$3 = view.getInt8(cursor$1); - var ext$3 = Js_dict.get(extensions, String(type_$3)); - if (ext$3 === undefined) { - return Js_exn.raiseError("Unknown extension type " + String(type_$3)); - } - _cursor = cursor$1 + 1 | 0; - _state = { - TAG: /* DecodeExt */4, - _0: 1, - _1: ext$3 - }; - continue ; - case 213 : - var type_$4 = view.getInt8(cursor$1); - var ext$4 = Js_dict.get(extensions, String(type_$4)); - if (ext$4 === undefined) { - return Js_exn.raiseError("Unknown extension type " + String(type_$4)); - } - _cursor = cursor$1 + 1 | 0; - _state = { - TAG: /* DecodeExt */4, - _0: 2, - _1: ext$4 - }; - continue ; - case 214 : - var type_$5 = view.getInt8(cursor$1); - if (type_$5 === timestampExt.type) { - _cursor = cursor$1 + 1 | 0; - _state = { - TAG: /* DecodeExt */4, - _0: 4, - _1: timestampExt - }; - continue ; - } - var ext$5 = Js_dict.get(extensions, String(type_$5)); - if (ext$5 === undefined) { - return Js_exn.raiseError("Unknown extension type " + String(type_$5)); - } - _cursor = cursor$1 + 1 | 0; - _state = { - TAG: /* DecodeExt */4, - _0: 4, - _1: ext$5 - }; - continue ; - case 215 : - var type_$6 = view.getInt8(cursor$1); - if (type_$6 === timestampExt.type) { - _cursor = cursor$1 + 1 | 0; - _state = { - TAG: /* DecodeExt */4, - _0: 8, - _1: timestampExt - }; - continue ; - } - var ext$6 = Js_dict.get(extensions, String(type_$6)); - if (ext$6 === undefined) { - return Js_exn.raiseError("Unknown extension type " + String(type_$6)); - } - _cursor = cursor$1 + 1 | 0; - _state = { - TAG: /* DecodeExt */4, - _0: 8, - _1: ext$6 - }; - continue ; - case 216 : - var type_$7 = view.getInt8(cursor$1); - var ext$7 = Js_dict.get(extensions, String(type_$7)); - if (ext$7 === undefined) { - return Js_exn.raiseError("Unknown extension type " + String(type_$7)); - } - _cursor = cursor$1 + 1 | 0; - _state = { - TAG: /* DecodeExt */4, - _0: 16, - _1: ext$7 - }; - continue ; - case 217 : - var len$9 = view.getUint8(cursor$1); - _cursor = cursor$1 + 1 | 0; - _state = { - TAG: /* DecodeString */0, - _0: len$9 - }; - continue ; - case 218 : - var len$10 = view.getUint16(cursor$1); - _cursor = cursor$1 + 2 | 0; - _state = { - TAG: /* DecodeString */0, - _0: len$10 - }; - continue ; - case 219 : - var len$11 = view.getUint32(cursor$1); - _cursor = cursor$1 + 4 | 0; - _state = { - TAG: /* DecodeString */0, - _0: len$11 - }; - continue ; - case 220 : - var len$12 = view.getUint16(cursor$1); - _cursor = cursor$1 + 2 | 0; - _state = { - TAG: /* DecodeArray */1, - _0: len$12, - _1: new Array(len$12) - }; - continue ; - case 221 : - var len$13 = view.getUint32(cursor$1); - _cursor = cursor$1 + 4 | 0; - _state = { - TAG: /* DecodeArray */1, - _0: len$13, - _1: new Array(len$13) - }; - continue ; - case 222 : - var len$14 = view.getUint16(cursor$1); - _cursor = cursor$1 + 2 | 0; - _state = { - TAG: /* DecodeMap */2, - _0: len$14, - _1: {} - }; - continue ; - case 223 : - var len$15 = view.getUint32(cursor$1); - _cursor = cursor$1 + 4 | 0; - _state = { - TAG: /* DecodeMap */2, - _0: len$15, - _1: {} - }; - continue ; - default: - - } - if (header >= 256) { - return Js_exn.raiseError("Unknown header " + String(header)); - } - var num$10 = Pervasives.lnot(header ^ 255); - _cursor = cursor$1; - _state = { - TAG: /* Done */5, - _0: num$10 - }; - continue ; - } - switch (state.TAG | 0) { - case /* DecodeString */0 : - var len$16 = state._0; - var view$1 = binary.subarray(cursor, cursor + len$16 | 0); - var text = decode2(textDecoder, view$1); - _cursor = cursor + len$16 | 0; - _state = { - TAG: /* Done */5, - _0: text - }; - continue ; - case /* DecodeArray */1 : - var array = state._1; - var len$17 = state._0; - if (len$17 !== 0) { - var match = decode$1(binary, /* ExpectHeader */0, cursor); - var index = array.length - len$17 | 0; - array[index] = match[0]; - _cursor = match[1]; - _state = { - TAG: /* DecodeArray */1, - _0: len$17 - 1 | 0, - _1: array - }; - continue ; - } - _state = { - TAG: /* Done */5, - _0: array - }; - continue ; - case /* DecodeMap */2 : - var map = state._1; - var len$18 = state._0; - if (len$18 !== 0) { - var match$1 = decode$1(binary, /* ExpectHeader */0, cursor); - var key = match$1[0]; - if (typeof key !== "string") { - return Js_exn.raiseError("Unexpected key type. Expected string, but got " + typeof key); - } - var match$2 = decode$1(binary, /* ExpectHeader */0, match$1[1]); - map[key] = match$2[0]; - _cursor = match$2[1]; - _state = { - TAG: /* DecodeMap */2, - _0: len$18 - 1 | 0, - _1: map - }; - continue ; - } - _state = { - TAG: /* Done */5, - _0: map - }; - continue ; - case /* DecodeBinary */3 : - var len$19 = state._0; - var copy = binary.slice(cursor, cursor + len$19 | 0); - _cursor = cursor + len$19 | 0; - _state = { - TAG: /* Done */5, - _0: copy - }; - continue ; - case /* DecodeExt */4 : - var len$20 = state._0; - var copy$1 = binary.slice(cursor, cursor + len$20 | 0); - _cursor = cursor + len$20 | 0; - _state = { - TAG: /* Done */5, - _0: state._1.decode(copy$1, len$20) - }; - continue ; - case /* Done */5 : - return [ - state._0, - cursor - ]; - - } - }; - }; - var match = decode$1(binary$1, /* ExpectHeader */0, 0); - var readLength = match[1]; - var inputLength = binary$1.length; - if (inputLength !== readLength) { - Js_exn.raiseError("Invalid input length, expected " + String(inputLength) + ", but got " + String(readLength)); - } - return match[0]; -} - -export { - make , - decode , - -} diff --git a/packages/msgpack/src/decoder.res b/packages/msgpack/src/decoder.res index c8f993c..6112888 100644 --- a/packages/msgpack/src/decoder.res +++ b/packages/msgpack/src/decoder.res @@ -1,354 +1 @@ -open Js -open TypedArray2 - -module TextDecoder = { - type t - - @new external make: unit => t = "TextDecoder" - @send external decode: (t, Uint8Array.t) => string = "decode" - - /** - * Borrowing from https://github.com/msgpack/msgpack-javascript/blob/c58b7e2/src/utils/utf8.ts#L48-L89 - * - * Note: - * The Node.js builtin TextDecoder is seriously slow. - * This custom decoder is 10~12x faster than the builtin - * - * See benchmarks/utf8-decode.js - */ - let decode2: (t, Uint8Array.t) => string = %raw(`function (_t, bytes) { - let offset = 0; - const end = bytes.length; - - const units = []; - let result = ""; - while (offset < end) { - const byte1 = bytes[offset++]; - if ((byte1 & 0x80) === 0) { - // 1 byte - units.push(byte1); - } else if ((byte1 & 0xe0) === 0xc0) { - // 2 bytes - const byte2 = bytes[offset++] & 0x3f; - units.push(((byte1 & 0x1f) << 6) | byte2); - } else if ((byte1 & 0xf0) === 0xe0) { - // 3 bytes - const byte2 = bytes[offset++] & 0x3f; - const byte3 = bytes[offset++] & 0x3f; - units.push(((byte1 & 0x1f) << 12) | (byte2 << 6) | byte3); - } else if ((byte1 & 0xf8) === 0xf0) { - // 4 bytes - const byte2 = bytes[offset++] & 0x3f; - const byte3 = bytes[offset++] & 0x3f; - const byte4 = bytes[offset++] & 0x3f; - let unit = ((byte1 & 0x07) << 0x12) | (byte2 << 0x0c) | (byte3 << 0x06) | byte4; - if (unit > 0xffff) { - unit -= 0x10000; - units.push(((unit >>> 10) & 0x3ff) | 0xd800); - unit = 0xdc00 | (unit & 0x3ff); - } - units.push(unit); - } else { - units.push(byte1); - } - - if (units.length >= 0x1_000) { - result += String.fromCharCode(...units); - units.length = 0; - } - } - - if (units.length > 0) { - result += String.fromCharCode(...units); - } - - return result; - }`) -} - -type result -external result: 'a => result = "%identity" -external toString: result => string = "%identity" - -@new external makeSizedArray: int => array = "Array" - -let flip64: Uint8Array.t => unit = %raw(`function(binary) { - let carry = 1; - for (let i = 7; i >= 0; i--) { - const v = (binary[i] ^ 0xff) + carry; - binary[i] = v & 0xff; - carry = v >> 8; - } -}`) - -type ext = { - \"type": int, - decode: (. Uint8Array.t, int) => result, -} - -@module("./ext/TimestampDecoder") external timestampExt: ext = "timestamDecoder" - -type t = { - textDecoder: TextDecoder.t, - extensions: Dict.t, -} - -let make = (~extensions) => { - let textDecoder = TextDecoder.make() - let extensions = extensions->Js.Dict.fromArray - { - textDecoder: textDecoder, - extensions: extensions, - } -} - -type state = - | ExpectHeader - | DecodeString(int) - | DecodeArray(int, array) - | DecodeMap(int, Dict.t) - | DecodeBinary(int) - | DecodeExt(int, ext) - | Done(result) - -let decode = (t, binary) => { - let {textDecoder, extensions} = t - let binary = binary->Uint8Array.copy - let view = binary->Uint8Array.buffer->DataView.fromBuffer - let rec decode = (binary, ~state, ~cursor) => { - switch state { - | ExpectHeader => { - let header = view->DataView.getUint8(cursor) - let cursor = cursor + 1 - switch header { - | header if header < 0x80 => binary->decode(~state=Done(header->result), ~cursor) - | header if header < 0x90 => { - let len = header->land(0xf) - binary->decode(~state=DecodeMap(len, Dict.empty()), ~cursor) - } - | header if header < 0xa0 => { - let len = header->land(0xf) - binary->decode(~state=DecodeArray(len, makeSizedArray(len)), ~cursor) - } - | header if header < 0xc0 => { - let len = header->land(0x1f) - binary->decode(~state=DecodeString(len), ~cursor) - } - | 0xc0 => binary->decode(~state=Done(null->result), ~cursor) - | 0xc2 => binary->decode(~state=Done(false->result), ~cursor) - | 0xc3 => binary->decode(~state=Done(true->result), ~cursor) - | 0xc4 => { - let len = view->DataView.getUint8(cursor) - binary->decode(~state=DecodeBinary(len), ~cursor=cursor + 1) - } - | 0xc5 => { - let len = view->DataView.getUint16(cursor) - binary->decode(~state=DecodeBinary(len), ~cursor=cursor + 2) - } - | 0xc6 => { - let len = view->DataView.getUint32(cursor) - binary->decode(~state=DecodeBinary(len), ~cursor=cursor + 4) - } - | 0xc7 => { - let len = view->DataView.getUint8(cursor) - let type_ = view->DataView.getInt8(cursor + 1) - if type_ == timestampExt.\"type" { - binary->decode(~state=DecodeExt(len, timestampExt), ~cursor=cursor + 2) - } else { - switch extensions->Dict.get(type_->Belt.Int.toString) { - | Some(ext) => binary->decode(~state=DecodeExt(len, ext), ~cursor=cursor + 2) - | None => Exn.raiseError(`Unknown extension type ${type_->Belt.Int.toString}`) - } - } - } - | 0xc8 => { - let len = view->DataView.getUint16(cursor) - let type_ = view->DataView.getInt8(cursor + 2) - switch extensions->Dict.get(type_->Belt.Int.toString) { - | Some(ext) => binary->decode(~state=DecodeExt(len, ext), ~cursor=cursor + 2) - | None => Exn.raiseError(`Unknown extension type ${type_->Belt.Int.toString}`) - } - } - | 0xc9 => { - let len = view->DataView.getUint32(cursor) - let type_ = view->DataView.getInt8(cursor + 4) - switch extensions->Dict.get(type_->Belt.Int.toString) { - | Some(ext) => binary->decode(~state=DecodeExt(len, ext), ~cursor=cursor + 4) - | None => Exn.raiseError(`Unknown extension type ${type_->Belt.Int.toString}`) - } - } - | 0xca => { - let num = view->DataView.getFloat32(cursor) - binary->decode(~state=Done(num->result), ~cursor=cursor + 4) - } - | 0xcb => { - let num = view->DataView.getFloat64(cursor) - binary->decode(~state=Done(num->result), ~cursor=cursor + 8) - } - | 0xcc => { - let num = view->DataView.getUint8(cursor) - binary->decode(~state=Done(num->result), ~cursor=cursor + 1) - } - | 0xcd => { - let num = view->DataView.getUint16(cursor) - binary->decode(~state=Done(num->result), ~cursor=cursor + 2) - } - | 0xce => { - let num = view->DataView.getUint32(cursor) - binary->decode(~state=Done(num->result), ~cursor=cursor + 4) - } - | 0xcf => { - let hi = view->DataView.getUint32(cursor)->Belt.Int.toFloat - let lo = view->DataView.getUint32(cursor + 4)->Belt.Int.toFloat - let num = hi *. Math.pow_float(~base=256.0, ~exp=4.0) +. lo - binary->decode(~state=Done(num->result), ~cursor=cursor + 8) - } - | 0xd0 => { - let num = view->DataView.getInt8(cursor) - binary->decode(~state=Done(num->result), ~cursor=cursor + 1) - } - | 0xd1 => { - let num = view->DataView.getInt16(cursor) - binary->decode(~state=Done(num->result), ~cursor=cursor + 2) - } - | 0xd2 => { - let num = view->DataView.getInt32(cursor) - binary->decode(~state=Done(num->result), ~cursor=cursor + 4) - } - | 0xd3 => { - binary->Uint8Array.subarray(~start=cursor, ~end_=cursor + 9)->flip64 - let hi = view->DataView.getUint32(cursor)->Belt.Int.toFloat - let lo = view->DataView.getUint32(cursor + 4)->Belt.Int.toFloat - let num = hi *. Math.pow_float(~base=256.0, ~exp=4.0) +. lo - binary->decode(~state=Done((0.0 -. num)->result), ~cursor=cursor + 8) - } - | 0xd4 => { - let type_ = view->DataView.getInt8(cursor) - switch extensions->Dict.get(type_->Belt.Int.toString) { - | Some(ext) => binary->decode(~state=DecodeExt(1, ext), ~cursor=cursor + 1) - | None => Exn.raiseError(`Unknown extension type ${type_->Belt.Int.toString}`) - } - } - | 0xd5 => { - let type_ = view->DataView.getInt8(cursor) - switch extensions->Dict.get(type_->Belt.Int.toString) { - | Some(ext) => binary->decode(~state=DecodeExt(2, ext), ~cursor=cursor + 1) - | None => Exn.raiseError(`Unknown extension type ${type_->Belt.Int.toString}`) - } - } - | 0xd6 => { - let type_ = view->DataView.getInt8(cursor) - if type_ == timestampExt.\"type" { - binary->decode(~state=DecodeExt(4, timestampExt), ~cursor=cursor + 1) - } else { - switch extensions->Dict.get(type_->Belt.Int.toString) { - | Some(ext) => binary->decode(~state=DecodeExt(4, ext), ~cursor=cursor + 1) - | None => Exn.raiseError(`Unknown extension type ${type_->Belt.Int.toString}`) - } - } - } - | 0xd7 => { - let type_ = view->DataView.getInt8(cursor) - if type_ == timestampExt.\"type" { - binary->decode(~state=DecodeExt(8, timestampExt), ~cursor=cursor + 1) - } else { - switch extensions->Dict.get(type_->Belt.Int.toString) { - | Some(ext) => binary->decode(~state=DecodeExt(8, ext), ~cursor=cursor + 1) - | None => Exn.raiseError(`Unknown extension type ${type_->Belt.Int.toString}`) - } - } - } - | 0xd8 => { - let type_ = view->DataView.getInt8(cursor) - switch extensions->Dict.get(type_->Belt.Int.toString) { - | Some(ext) => binary->decode(~state=DecodeExt(16, ext), ~cursor=cursor + 1) - | None => Exn.raiseError(`Unknown extension type ${type_->Belt.Int.toString}`) - } - } - | 0xd9 => { - let len = view->DataView.getUint8(cursor) - binary->decode(~state=DecodeString(len), ~cursor=cursor + 1) - } - | 0xda => { - let len = view->DataView.getUint16(cursor) - binary->decode(~state=DecodeString(len), ~cursor=cursor + 2) - } - | 0xdb => { - let len = view->DataView.getUint32(cursor) - binary->decode(~state=DecodeString(len), ~cursor=cursor + 4) - } - | 0xdc => { - let len = view->DataView.getUint16(cursor) - binary->decode(~state=DecodeArray(len, makeSizedArray(len)), ~cursor=cursor + 2) - } - | 0xdd => { - let len = view->DataView.getUint32(cursor) - binary->decode(~state=DecodeArray(len, makeSizedArray(len)), ~cursor=cursor + 4) - } - | 0xde => { - let len = view->DataView.getUint16(cursor) - binary->decode(~state=DecodeMap(len, Dict.empty()), ~cursor=cursor + 2) - } - | 0xdf => { - let len = view->DataView.getUint32(cursor) - binary->decode(~state=DecodeMap(len, Dict.empty()), ~cursor=cursor + 4) - } - | header if header < 0x100 => { - let num = header->lxor(255)->lnot - binary->decode(~state=Done(num->result), ~cursor) - } - | header => Exn.raiseError(`Unknown header ${header->Belt.Int.toString}`) - } - } - | DecodeString(len) => { - let view = binary->Uint8Array.subarray(~start=cursor, ~end_=cursor + len) - let text = textDecoder->TextDecoder.decode2(view) - binary->decode(~state=Done(text->result), ~cursor=cursor + len) - } - | DecodeArray(len, array) => - switch len { - | 0 => binary->decode(~state=Done(array->result), ~cursor) - | len => { - let (item, cursor) = binary->decode(~state=ExpectHeader, ~cursor) - let index = array->Array2.length - len - array->Array2.unsafe_set(index, item) - binary->decode(~state=DecodeArray(len - 1, array), ~cursor) - } - } - | DecodeMap(len, map) => - switch len { - | 0 => binary->decode(~state=Done(map->result), ~cursor) - | len => { - let (key, cursor) = binary->decode(~state=ExpectHeader, ~cursor) - if typeof(key) == "string" { - let (value, cursor) = binary->decode(~state=ExpectHeader, ~cursor) - map->Dict.set(key->toString, value) - binary->decode(~state=DecodeMap(len - 1, map), ~cursor) - } else { - Exn.raiseError(`Unexpected key type. Expected string, but got ${typeof(key)}`) - } - } - } - | DecodeBinary(len) => { - let copy = binary->Uint8Array.slice(~start=cursor, ~end_=cursor + len) - binary->decode(~state=Done(copy->result), ~cursor=cursor + len) - } - | DecodeExt(len, ext) => { - let copy = binary->Uint8Array.slice(~start=cursor, ~end_=cursor + len) - binary->decode(~state=Done(ext.decode(. copy, len)), ~cursor=cursor + len) - } - | Done(result) => (result, cursor) - } - } - - let (result, readLength) = binary->decode(~state=ExpectHeader, ~cursor=0) - - let inputLength = binary->Uint8Array.length - if inputLength != readLength { - Exn.raiseError( - `Invalid input length, expected ${inputLength->Belt.Int.toString}, but got ${readLength->Belt.Int.toString}`, - ) - } - - result -} +include RescriptMsgpack.Decoder diff --git a/packages/msgpack/src/decoder.ts b/packages/msgpack/src/decoder.ts index d1cf81b..135890b 100644 --- a/packages/msgpack/src/decoder.ts +++ b/packages/msgpack/src/decoder.ts @@ -1,5 +1,4 @@ -import type { Input } from './types'; -import type { DecoderExtension } from './ext/types'; +import type { Input, DecoderExtension } from './types'; // @ts-ignore import { make as makeDecoder, decode } from './decoder.bs'; diff --git a/packages/msgpack/src/ext/TimestampDecoder.ts b/packages/msgpack/src/ext/TimestampDecoder.ts deleted file mode 100644 index e3efe8d..0000000 --- a/packages/msgpack/src/ext/TimestampDecoder.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { DecoderExtension } from './types'; - -export const timestampDecoder: DecoderExtension = { - type: -1, - // almost copy-pated from msgpack5's DateCodec implementation - // https://github.com/mcollina/msgpack5/blob/master/lib/codecs/DateCodec.js - decode(dataArray, byteLength) { - let view = new DataView(dataArray.buffer); - switch (byteLength) { - case 4: { - let seconds = view.getUint32(0, false); - let millis = seconds / 1000 | 0; - return new Date(millis); - } - case 8: { - let upper = view.getUint32(0); - let lower = view.getUint32(4); - let nanos = upper / 4; - let seconds = ((upper & 3) * (2 ** 32)) + lower; - let millis = seconds * 1000 + Math.round(nanos / 1e6); - return new Date(millis); - } - case 12: - throw new Error(`timestamp96 is not supported yet`); - default: - throw new Error(`invalid byteLength ${byteLength}`); - } - }, -}; diff --git a/packages/msgpack/src/ext/TimestampEncoder.ts b/packages/msgpack/src/ext/TimestampEncoder.ts index cc1e68d..4be80a8 100644 --- a/packages/msgpack/src/ext/TimestampEncoder.ts +++ b/packages/msgpack/src/ext/TimestampEncoder.ts @@ -1,4 +1,4 @@ -import type { EncoderExtension } from './types'; +import type { EncoderExtension } from '../types'; let uint64bound = 0x100000000; diff --git a/packages/msgpack/src/ext/types.ts b/packages/msgpack/src/ext/types.ts deleted file mode 100644 index 46546c4..0000000 --- a/packages/msgpack/src/ext/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -export interface EncoderExtension { - type: number; - check(input: object): input is T; - encode(input: T): Uint8Array; -} - -export interface DecoderExtension { - type: number; - decode(dataArray: Uint8Array, length: number): T; -} diff --git a/packages/msgpack/src/index.ts b/packages/msgpack/src/index.ts index b3d9c23..7f7621b 100644 --- a/packages/msgpack/src/index.ts +++ b/packages/msgpack/src/index.ts @@ -1,4 +1,3 @@ export * from './encoder'; export * from './decoder'; - -export type { EncoderExtension, DecoderExtension } from './ext/types'; +export * from './types'; diff --git a/packages/msgpack/src/types.ts b/packages/msgpack/src/types.ts index 662a669..43cb992 100644 --- a/packages/msgpack/src/types.ts +++ b/packages/msgpack/src/types.ts @@ -11,3 +11,14 @@ export type Input = ( | Date | ExtensionType ); + +export interface EncoderExtension { + type: number; + check(input: object): input is T; + encode(input: T): Uint8Array; +} + +export interface DecoderExtension { + type: number; + decode(dataArray: Uint8Array, length: number): T; +} diff --git a/packages/rescript-msgpack/.gitignore b/packages/rescript-msgpack/.gitignore new file mode 100644 index 0000000..6721fa0 --- /dev/null +++ b/packages/rescript-msgpack/.gitignore @@ -0,0 +1,5 @@ +.merlin +.bsb.lock +*.bs.js + +/lib/ diff --git a/packages/rescript-msgpack/README.md b/packages/rescript-msgpack/README.md new file mode 100644 index 0000000..47b2dd1 --- /dev/null +++ b/packages/rescript-msgpack/README.md @@ -0,0 +1 @@ +# rescript-msgpack diff --git a/packages/rescript-msgpack/bsconfig.json b/packages/rescript-msgpack/bsconfig.json new file mode 100644 index 0000000..42e73e0 --- /dev/null +++ b/packages/rescript-msgpack/bsconfig.json @@ -0,0 +1,21 @@ +{ + "name": "rescript-msgpack", + "namespace": true, + "sources": [ + { + "dir": "src", + "subdirs": true + } + ], + "package-specs": [ + { + "module": "es6", + "in-source": true + } + ], + "suffix": ".bs.js", + "bs-dependencies": [ + ], + "bs-dev-dependencies": [ + ] +} diff --git a/packages/rescript-msgpack/package.json b/packages/rescript-msgpack/package.json new file mode 100644 index 0000000..83130b3 --- /dev/null +++ b/packages/rescript-msgpack/package.json @@ -0,0 +1,20 @@ +{ + "name": "rescript-msgpack", + "version": "0.1.0", + "packageManager": "yarn@3.1.1", + "type": "module", + "files": [ + "src/**/*.res", + "src/**/*.resi", + "bsconfig.json" + ], + "scripts": { + "build": "rescript build -with-deps" + }, + "peerDependencies": { + "rescript": "^9.0.0" + }, + "devDependencies": { + "rescript": "^9.1.4" + } +} diff --git a/packages/rescript-msgpack/src/Decoder.res b/packages/rescript-msgpack/src/Decoder.res new file mode 100644 index 0000000..94930f1 --- /dev/null +++ b/packages/rescript-msgpack/src/Decoder.res @@ -0,0 +1,290 @@ +open Js.TypedArray2 +open Extension + +type t = { + textDecoder: TextDecoder.t, + extensions: Js.Dict.t, +} + +let make = (~extensions=[], ()) => { + let textDecoder = TextDecoder.make() + let extensions = + extensions + ->Js.Array2.map((ext: module(DecoderExtension)) => { + let module(DecoderExtension) = ext + (DecoderExtension.\"type"->Belt.Int.toString, ext) + }) + ->Js.Dict.fromArray + { + textDecoder: textDecoder, + extensions: extensions, + } +} + +let flip64: Uint8Array.t => unit = %raw(`binary => { + let carry = 1; + for (let i = 7; i >= 0; i--) { + let v = (binary[i] ^ 0xff) + carry; + binary[i] = v & 0xff; + carry = v >> 8; + } +}`) + +type state = + | ExpectHeader + | DecodeString(int) + | DecodeArray(int, array) + | DecodeMap(int, Js.Dict.t) + | DecodeBinary(int) + | DecodeExt(int, module(DecoderExtension)) + | Done(Message.t) + +let decode = (t, binary) => { + let {textDecoder, extensions} = t + let binary = binary->Uint8Array.copy + let view = binary->Uint8Array.buffer->DataView.fromBuffer + let rec decode = (binary, ~state, ~cursor) => { + switch state { + | ExpectHeader => { + let header = view->DataView.getUint8(cursor) + let cursor = cursor + 1 + switch header { + | header if header < 0x80 => binary->decode(~state=Done(header->Message.make), ~cursor) + | header if header < 0x90 => { + let len = header->land(0xf) + binary->decode(~state=DecodeMap(len, Js.Dict.empty()), ~cursor) + } + | header if header < 0xa0 => { + let len = header->land(0xf) + binary->decode(~state=DecodeArray(len, Message.makeArray(len)), ~cursor) + } + | header if header < 0xc0 => { + let len = header->land(0x1f) + binary->decode(~state=DecodeString(len), ~cursor) + } + | 0xc0 => binary->decode(~state=Done(Js.null->Message.make), ~cursor) + | 0xc2 => binary->decode(~state=Done(false->Message.make), ~cursor) + | 0xc3 => binary->decode(~state=Done(true->Message.make), ~cursor) + | 0xc4 => { + let len = view->DataView.getUint8(cursor) + binary->decode(~state=DecodeBinary(len), ~cursor=cursor + 1) + } + | 0xc5 => { + let len = view->DataView.getUint16(cursor) + binary->decode(~state=DecodeBinary(len), ~cursor=cursor + 2) + } + | 0xc6 => { + let len = view->DataView.getUint32(cursor) + binary->decode(~state=DecodeBinary(len), ~cursor=cursor + 4) + } + | 0xc7 => { + let len = view->DataView.getUint8(cursor) + let type_ = view->DataView.getInt8(cursor + 1) + if type_ == TimestampDecoder.\"type" { + binary->decode(~state=DecodeExt(len, module(TimestampDecoder)), ~cursor=cursor + 2) + } else { + switch extensions->Js.Dict.get(type_->Belt.Int.toString) { + | Some(ext) => binary->decode(~state=DecodeExt(len, ext), ~cursor=cursor + 2) + | None => Js.Exn.raiseError(`Unknown extension type ${type_->Belt.Int.toString}`) + } + } + } + | 0xc8 => { + let len = view->DataView.getUint16(cursor) + let type_ = view->DataView.getInt8(cursor + 2) + if type_ == TimestampDecoder.\"type" { + binary->decode(~state=DecodeExt(len, module(TimestampDecoder)), ~cursor=cursor + 2) + } else { + switch extensions->Js.Dict.get(type_->Belt.Int.toString) { + | Some(ext) => binary->decode(~state=DecodeExt(len, ext), ~cursor=cursor + 2) + | None => Js.Exn.raiseError(`Unknown extension type ${type_->Belt.Int.toString}`) + } + } + } + | 0xc9 => { + let len = view->DataView.getUint32(cursor) + let type_ = view->DataView.getInt8(cursor + 4) + if type_ == TimestampDecoder.\"type" { + binary->decode(~state=DecodeExt(len, module(TimestampDecoder)), ~cursor=cursor + 4) + } else { + switch extensions->Js.Dict.get(type_->Belt.Int.toString) { + | Some(ext) => binary->decode(~state=DecodeExt(len, ext), ~cursor=cursor + 4) + | None => Js.Exn.raiseError(`Unknown extension type ${type_->Belt.Int.toString}`) + } + } + } + | 0xca => { + let num = view->DataView.getFloat32(cursor) + binary->decode(~state=Done(num->Message.make), ~cursor=cursor + 4) + } + | 0xcb => { + let num = view->DataView.getFloat64(cursor) + binary->decode(~state=Done(num->Message.make), ~cursor=cursor + 8) + } + | 0xcc => { + let num = view->DataView.getUint8(cursor) + binary->decode(~state=Done(num->Message.make), ~cursor=cursor + 1) + } + | 0xcd => { + let num = view->DataView.getUint16(cursor) + binary->decode(~state=Done(num->Message.make), ~cursor=cursor + 2) + } + | 0xce => { + let num = view->DataView.getUint32(cursor) + binary->decode(~state=Done(num->Message.make), ~cursor=cursor + 4) + } + | 0xcf => { + let hi = view->DataView.getUint32(cursor)->Belt.Int.toFloat + let lo = view->DataView.getUint32(cursor + 4)->Belt.Int.toFloat + let num = hi *. Js.Math.pow_float(~base=256.0, ~exp=4.0) +. lo + binary->decode(~state=Done(num->Message.make), ~cursor=cursor + 8) + } + | 0xd0 => { + let num = view->DataView.getInt8(cursor) + binary->decode(~state=Done(num->Message.make), ~cursor=cursor + 1) + } + | 0xd1 => { + let num = view->DataView.getInt16(cursor) + binary->decode(~state=Done(num->Message.make), ~cursor=cursor + 2) + } + | 0xd2 => { + let num = view->DataView.getInt32(cursor) + binary->decode(~state=Done(num->Message.make), ~cursor=cursor + 4) + } + | 0xd3 => { + binary->Uint8Array.subarray(~start=cursor, ~end_=cursor + 9)->flip64 + let hi = view->DataView.getUint32(cursor)->Belt.Int.toFloat + let lo = view->DataView.getUint32(cursor + 4)->Belt.Int.toFloat + let num = hi *. Js.Math.pow_float(~base=256.0, ~exp=4.0) +. lo + binary->decode(~state=Done((0.0 -. num)->Message.make), ~cursor=cursor + 8) + } + | 0xd4 => { + let type_ = view->DataView.getInt8(cursor) + switch extensions->Js.Dict.get(type_->Belt.Int.toString) { + | Some(ext) => binary->decode(~state=DecodeExt(1, ext), ~cursor=cursor + 1) + | None => Js.Exn.raiseError(`Unknown extension type ${type_->Belt.Int.toString}`) + } + } + | 0xd5 => { + let type_ = view->DataView.getInt8(cursor) + switch extensions->Js.Dict.get(type_->Belt.Int.toString) { + | Some(ext) => binary->decode(~state=DecodeExt(2, ext), ~cursor=cursor + 1) + | None => Js.Exn.raiseError(`Unknown extension type ${type_->Belt.Int.toString}`) + } + } + | 0xd6 => { + let type_ = view->DataView.getInt8(cursor) + if type_ == TimestampDecoder.\"type" { + binary->decode(~state=DecodeExt(4, module(TimestampDecoder)), ~cursor=cursor + 1) + } else { + switch extensions->Js.Dict.get(type_->Belt.Int.toString) { + | Some(ext) => binary->decode(~state=DecodeExt(4, ext), ~cursor=cursor + 1) + | None => Js.Exn.raiseError(`Unknown extension type ${type_->Belt.Int.toString}`) + } + } + } + | 0xd7 => { + let type_ = view->DataView.getInt8(cursor) + if type_ == TimestampDecoder.\"type" { + binary->decode(~state=DecodeExt(8, module(TimestampDecoder)), ~cursor=cursor + 1) + } else { + switch extensions->Js.Dict.get(type_->Belt.Int.toString) { + | Some(ext) => binary->decode(~state=DecodeExt(8, ext), ~cursor=cursor + 1) + | None => Js.Exn.raiseError(`Unknown extension type ${type_->Belt.Int.toString}`) + } + } + } + | 0xd8 => { + let type_ = view->DataView.getInt8(cursor) + switch extensions->Js.Dict.get(type_->Belt.Int.toString) { + | Some(ext) => binary->decode(~state=DecodeExt(16, ext), ~cursor=cursor + 1) + | None => Js.Exn.raiseError(`Unknown extension type ${type_->Belt.Int.toString}`) + } + } + | 0xd9 => { + let len = view->DataView.getUint8(cursor) + binary->decode(~state=DecodeString(len), ~cursor=cursor + 1) + } + | 0xda => { + let len = view->DataView.getUint16(cursor) + binary->decode(~state=DecodeString(len), ~cursor=cursor + 2) + } + | 0xdb => { + let len = view->DataView.getUint32(cursor) + binary->decode(~state=DecodeString(len), ~cursor=cursor + 4) + } + | 0xdc => { + let len = view->DataView.getUint16(cursor) + binary->decode(~state=DecodeArray(len, Message.makeArray(len)), ~cursor=cursor + 2) + } + | 0xdd => { + let len = view->DataView.getUint32(cursor) + binary->decode(~state=DecodeArray(len, Message.makeArray(len)), ~cursor=cursor + 4) + } + | 0xde => { + let len = view->DataView.getUint16(cursor) + binary->decode(~state=DecodeMap(len, Js.Dict.empty()), ~cursor=cursor + 2) + } + | 0xdf => { + let len = view->DataView.getUint32(cursor) + binary->decode(~state=DecodeMap(len, Js.Dict.empty()), ~cursor=cursor + 4) + } + | header if header < 0x100 => { + let num = header->lxor(255)->lnot + binary->decode(~state=Done(num->Message.make), ~cursor) + } + | header => Js.Exn.raiseError(`Unknown header ${header->Belt.Int.toString}`) + } + } + | DecodeString(len) => { + let view = binary->Uint8Array.subarray(~start=cursor, ~end_=cursor + len) + let text = textDecoder->TextDecoder.decode2(view) + binary->decode(~state=Done(text->Message.make), ~cursor=cursor + len) + } + | DecodeArray(len, array) => + switch len { + | 0 => binary->decode(~state=Done(array->Message.make), ~cursor) + | len => { + let (item, cursor) = binary->decode(~state=ExpectHeader, ~cursor) + let index = array->Js.Array2.length - len + array->Js.Array2.unsafe_set(index, item) + binary->decode(~state=DecodeArray(len - 1, array), ~cursor) + } + } + | DecodeMap(len, map) => + switch len { + | 0 => binary->decode(~state=Done(map->Message.make), ~cursor) + | len => { + let (key, cursor) = binary->decode(~state=ExpectHeader, ~cursor) + if Js.typeof(key) == "string" { + let (value, cursor) = binary->decode(~state=ExpectHeader, ~cursor) + map->Js.Dict.set(key->Message.toString, value) + binary->decode(~state=DecodeMap(len - 1, map), ~cursor) + } else { + Js.Exn.raiseError(`Unexpected key type. Expected string, but got ${Js.typeof(key)}`) + } + } + } + | DecodeBinary(len) => { + let copy = binary->Uint8Array.slice(~start=cursor, ~end_=cursor + len) + binary->decode(~state=Done(copy->Message.make), ~cursor=cursor + len) + } + | DecodeExt(len, module(DecoderExtension)) => { + let copy = binary->Uint8Array.slice(~start=cursor, ~end_=cursor + len) + binary->decode(~state=Done(DecoderExtension.decode(copy, len)), ~cursor=cursor + len) + } + | Done(msg) => (msg, cursor) + } + } + + let (msg, readLength) = binary->decode(~state=ExpectHeader, ~cursor=0) + + let inputLength = binary->Uint8Array.length + if inputLength != readLength { + Js.Exn.raiseError( + `Invalid input length, expected ${inputLength->Belt.Int.toString}, but got ${readLength->Belt.Int.toString}`, + ) + } + + msg +} diff --git a/packages/rescript-msgpack/src/Decoder.resi b/packages/rescript-msgpack/src/Decoder.resi new file mode 100644 index 0000000..39990f9 --- /dev/null +++ b/packages/rescript-msgpack/src/Decoder.resi @@ -0,0 +1,8 @@ +type t = { + textDecoder: TextDecoder.t, + extensions: Js.Dict.t, +} + +let make: (~extensions: array=?, unit) => t + +let decode: (t, Js.TypedArray2.Uint8Array.t) => Message.t diff --git a/packages/rescript-msgpack/src/Encoder.res b/packages/rescript-msgpack/src/Encoder.res new file mode 100644 index 0000000..13b06da --- /dev/null +++ b/packages/rescript-msgpack/src/Encoder.res @@ -0,0 +1,187 @@ +open Extension + +type t = { + textEncoder: TextEncoder.t, + extensions: Js.Dict.t, +} + +let make = (~extensions=[], ()) => { + let textEncoder = TextEncoder.make() + let extensions = + extensions + ->Js.Array2.map((ext: module(EncoderExtension)) => { + let module(EncoderExtension) = ext + (EncoderExtension.\"type"->Belt.Int.toString, ext) + }) + ->Js.Dict.fromArray + { + textEncoder: textEncoder, + extensions: extensions, + } +} + +module Obj = { + type t = Js.Types.obj_val + + @scope("Array") @val external isArray: t => bool = "isArray" + + external toArray: t => array<'a> = "%identity" + + external toArrayBuffer: t => Js.TypedArray2.ArrayBuffer.t = "%identity" + + external toUint8Array: t => Js.TypedArray2.Uint8Array.t = "%identity" + + external toDict: t => Js.Dict.t<'a> = "%identity" +} + +module Uint8Array = { + include Js.TypedArray2.Uint8Array + + external toArrayLike: t => array = "%identity" + + @inline + let setOffset = (t, offset, elt) => { + t->unsafe_set(offset, elt) + t + } + + @inline + let setOffsetA = (t, offset, arr) => { + t->setArrayOffset(arr->toArrayLike, offset) + t + } +} + +type state = + | ExpectValue + | EncodeInt(int) + | EncodeFloat(float) + | EncodeString(string) + | EncodeBinary(Uint8Array.t) + | EncodeArray(int, array) + | EncodeMap(int, array<(string, Message.t)>) + | Done(Uint8Array.t) + +let encode = (t, msg) => { + let {textEncoder, extensions} = t + + let rec encode = (msg, ~state) => { + open Uint8Array + + switch state { + | ExpectValue => + switch msg->Message.toJSON->Js.Types.classify { + | JSNull => msg->encode(~state=Done(make([0xc0]))) + | JSFalse => msg->encode(~state=Done(make([0xc2]))) + | JSTrue => msg->encode(~state=Done(make([0xc3]))) + | JSNumber(number) if %raw(`number % 1 > 0`) => msg->encode(~state=EncodeFloat(number)) + | JSNumber(number) if -2147483648.0 <= number && number <= 2147483647.0 => + msg->encode(~state=EncodeInt(number->Belt.Float.toInt)) + | JSNumber(_) => Js.Exn.raiseError(`Only support int32 range yet`) + | JSString(string) => msg->encode(~state=EncodeString(string)) + | JSObject(object) if object->Obj.isArray => { + let array = object->Obj.toArray + let len = array->Belt.Array.length + msg->encode(~state=EncodeArray(len, array)) + } + | JSObject(object) if %raw(`object instanceof Uint8Array`) => { + let binary = object->Obj.toUint8Array + msg->encode(~state=EncodeBinary(binary)) + } + | JSObject(object) if %raw(`object instanceof ArrayBuffer`) => { + let binary = object->Obj.toArrayBuffer->Uint8Array.fromBuffer + msg->encode(~state=EncodeBinary(binary)) + } + | JSObject(object) if object->TimestampEncoder.check => msg->encode( + ~state=Done(object->TimestampEncoder.encode), + ) + | JSObject(object) => + switch extensions->Js.Dict.get(TimestampEncoder.\"type"->Belt.Int.toString) { + | Some(module(EncoderExtension)) => + msg->encode(~state=Done(object->EncoderExtension.encode)) + | None => { + let entries = object->Obj.toDict->Js.Dict.entries + let len = entries->Belt.Array.length + msg->encode(~state=EncodeMap(len, entries)) + } + } + | _ => msg->encode(~state=Done(fromLength(0))) + } + | EncodeInt(value) if value == 0 => msg->encode(~state=Done(make([0]))) + | EncodeInt(value) if 0 < value && value < 128 => msg->encode(~state=Done(make([0]))) + | EncodeInt(value) if 128 <= value && value < 256 => msg->encode(~state=Done(make([0]))) + | EncodeInt(value) if 256 <= value && value < 65536 => msg->encode(~state=Done(make([0]))) + | EncodeInt(value) if 65536 <= value => msg->encode(~state=Done(make([0]))) + | EncodeInt(value) if 0 > value && value >= -32 => msg->encode(~state=Done(make([0]))) + | EncodeInt(value) if -32 > value && value >= -128 => msg->encode(~state=Done(make([0]))) + | EncodeInt(value) if -128 > value && value >= -32768 => msg->encode(~state=Done(make([0]))) + | EncodeInt(value) if -32768 > value => msg->encode(~state=Done(make([0]))) + | EncodeFloat(value) => { + open Js.TypedArray2 + let is64 = value->Js.Math.fround != value + let array = fromLength(is64 ? 9 : 5) + let view = DataView.fromBuffer(array->buffer) + if is64 { + array->unsafe_set(0, 0xcb) + view->DataView.setFloat64(1, value) + } else { + array->unsafe_set(0, 0xca) + view->DataView.setFloat32(1, value) + } + msg->encode(~state=Done(array)) + } + | EncodeString(value) => { + let buffer = textEncoder->TextEncoder.encode2(value) + let len = buffer->length + let binary = switch len { + | len if len < 32 => + fromLength(len + 1)->setOffset(0, len->lor(0xa0))->setOffsetA(1, buffer) + | len if len < 256 => + fromLength(len + 2)->setOffset(0, 0xd9)->setOffset(1, len)->setOffsetA(2, buffer) + | len if len < 65536 => + fromLength(len + 3) + ->setOffset(0, 0xda) + ->setOffset(1, len->lsr(8)->land(0xff)) + ->setOffset(2, len->land(0xff)) + ->setOffsetA(3, buffer) + | len if Belt.Int.toFloat(len) < 4294967296.0 => + fromLength(len + 5) + ->setOffset(0, 0xdb) + ->setOffset(1, len->lsr(24)->land(0xff)) + ->setOffset(2, len->lsr(16)->land(0xff)) + ->setOffset(3, len->lsr(8)->land(0xff)) + ->setOffset(4, len->land(0xff)) + ->setOffsetA(5, buffer) + | _ => fromLength(0) + } + msg->encode(~state=Done(binary)) + } + | EncodeBinary(value) => { + let len = value->Uint8Array.length + let binary = switch len { + | len if len < 256 => + fromLength(len + 2)->setOffset(0, 0xc4)->setOffset(1, len)->setOffsetA(2, value) + | len if len < 65536 => + fromLength(len + 3) + ->setOffset(0, 0xc5) + ->setOffset(1, len->lsr(8)) + ->setOffset(2, len->land(0xff)) + ->setOffsetA(3, value) + | len if len->Belt.Int.toFloat < 4294967296.0 => + fromLength(len + 5) + ->setOffset(0, 0xc6) + ->setOffset(1, len->lsr(24)->land(0xff)) + ->setOffset(2, len->lsr(16)->land(0xff)) + ->setOffset(3, len->lsr(8)->land(0xff)) + ->setOffset(4, len->land(0xff)) + ->setOffsetA(5, value) + | _ => fromLength(0) + } + msg->encode(~state=Done(binary)) + } + | Done(binary) => binary + } + } + + msg->encode(~state=ExpectValue) +} diff --git a/packages/rescript-msgpack/src/Encoder.resi b/packages/rescript-msgpack/src/Encoder.resi new file mode 100644 index 0000000..6458b7f --- /dev/null +++ b/packages/rescript-msgpack/src/Encoder.resi @@ -0,0 +1,8 @@ +type t = { + textEncoder: TextEncoder.t, + extensions: Js.Dict.t, +} + +let make: (~extensions: array=?, unit) => t + +let encode: (t, Message.t) => Js.TypedArray2.Uint8Array.t diff --git a/packages/rescript-msgpack/src/Extension.res b/packages/rescript-msgpack/src/Extension.res new file mode 100644 index 0000000..476ffe9 --- /dev/null +++ b/packages/rescript-msgpack/src/Extension.res @@ -0,0 +1,13 @@ +module type EncoderExtension = { + let \"type": int + + let check: Js.Types.obj_val => bool + + let encode: Js.Types.obj_val => Js.TypedArray2.Uint8Array.t +} + +module type DecoderExtension = { + let \"type": int + + let decode: (Js.TypedArray2.Uint8Array.t, int) => Message.t +} diff --git a/packages/rescript-msgpack/src/Message.res b/packages/rescript-msgpack/src/Message.res new file mode 100644 index 0000000..578eae3 --- /dev/null +++ b/packages/rescript-msgpack/src/Message.res @@ -0,0 +1,13 @@ +type t + +external make: 'a => t = "%identity" + +@val external makeArray: int => array = "Array" + +external toString: t => string = "%identity" + +external toInt: t => int = "%identity" + +external toFloat: t => float = "%identity" + +external toJSON: t => Js.Json.t = "%identity" diff --git a/packages/rescript-msgpack/src/TextDecoder.res b/packages/rescript-msgpack/src/TextDecoder.res new file mode 100644 index 0000000..a2cbdaf --- /dev/null +++ b/packages/rescript-msgpack/src/TextDecoder.res @@ -0,0 +1,64 @@ +open Js.TypedArray2 + +type t + +@new external make: unit => t = "TextDecoder" +@send external decode: (t, Uint8Array.t) => string = "decode" + +/** + * Borrowing from https://github.com/msgpack/msgpack-javascript/blob/c58b7e2/src/utils/utf8.ts#L110-L157 + * + * Note: + * The Node.js builtin TextDecoder is seriously slow. + * This custom decoder is 10~12x faster than the builtin + * + * See benchmarks/utf8-decode.cjs + */ +let decode2: (t, Uint8Array.t) => string = %raw(`(_t, bytes) => { + let offset = 0; + let end = bytes.length; + + let units = []; + let result = ""; + while (offset < end) { + let byte1 = bytes[offset++]; + if ((byte1 & 0x80) === 0) { + // 1 byte + units.push(byte1); + } else if ((byte1 & 0xe0) === 0xc0) { + // 2 bytes + let byte2 = bytes[offset++] & 0x3f; + units.push(((byte1 & 0x1f) << 6) | byte2); + } else if ((byte1 & 0xf0) === 0xe0) { + // 3 bytes + let byte2 = bytes[offset++] & 0x3f; + let byte3 = bytes[offset++] & 0x3f; + units.push(((byte1 & 0x1f) << 12) | (byte2 << 6) | byte3); + } else if ((byte1 & 0xf8) === 0xf0) { + // 4 bytes + let byte2 = bytes[offset++] & 0x3f; + let byte3 = bytes[offset++] & 0x3f; + let byte4 = bytes[offset++] & 0x3f; + let unit = ((byte1 & 0x07) << 0x12) | (byte2 << 0x0c) | (byte3 << 0x06) | byte4; + if (unit > 0xffff) { + unit -= 0x10000; + units.push(((unit >>> 10) & 0x3ff) | 0xd800); + unit = 0xdc00 | (unit & 0x3ff); + } + units.push(unit); + } else { + units.push(byte1); + } + + if (units.length >= 0x1_000) { + result += String.fromCharCode(...units); + units.length = 0; + } + } + + if (units.length > 0) { + result += String.fromCharCode(...units); + } + + return result; +}`) diff --git a/packages/rescript-msgpack/src/TextEncoder.res b/packages/rescript-msgpack/src/TextEncoder.res new file mode 100644 index 0000000..08831e2 --- /dev/null +++ b/packages/rescript-msgpack/src/TextEncoder.res @@ -0,0 +1,61 @@ +open Js.TypedArray2 + +type t + +@new external make: unit => t = "TextEncoder" +@send external encode: (t, string) => Uint8Array.t = "encode" + +/** + * Borrowing from https://github.com/msgpack/msgpack-javascript/blob/c58b7e2/src/utils/utf8.ts#L48-L89 + * + * Note: + * The Node.js builtin TextEncoder is seriously slow. + * This custom decoder is ~5x faster than the builtin + * + * See benchmarks/utf8-encode.cjs + */ +let encode2: (t, string) => Uint8Array.t = %raw(`(_t, str) => { + let strLength = str.length; + let output = []; + let offset = 0; + let pos = 0; + while (pos < strLength) { + let value = str.charCodeAt(pos++); + + if ((value & 0xffffff80) === 0) { + // 1-byte + output[offset++] = value; + continue; + } else if ((value & 0xfffff800) === 0) { + // 2-bytes + output[offset++] = ((value >> 6) & 0x1f) | 0xc0; + } else { + // handle surrogate pair + if (value >= 0xd800 && value <= 0xdbff) { + // high surrogate + if (pos < strLength) { + let extra = str.charCodeAt(pos); + if ((extra & 0xfc00) === 0xdc00) { + ++pos; + value = ((value & 0x3ff) << 10) + (extra & 0x3ff) + 0x10000; + } + } + } + + if ((value & 0xffff0000) === 0) { + // 3-byte + output[offset++] = ((value >> 12) & 0x0f) | 0xe0; + output[offset++] = ((value >> 6) & 0x3f) | 0x80; + } else { + // 4-byte + output[offset++] = ((value >> 18) & 0x07) | 0xf0; + output[offset++] = ((value >> 12) & 0x3f) | 0x80; + output[offset++] = ((value >> 6) & 0x3f) | 0x80; + } + } + + output[offset++] = (value & 0x3f) | 0x80; + } + + return new Uint8Array(output); +}`) diff --git a/packages/rescript-msgpack/src/TimestampDecoder.res b/packages/rescript-msgpack/src/TimestampDecoder.res new file mode 100644 index 0000000..19cc2f4 --- /dev/null +++ b/packages/rescript-msgpack/src/TimestampDecoder.res @@ -0,0 +1,26 @@ +let \"type" = -1 + +// almost copy-pated from msgpack5's DateCodec implementation +// https://github.com/mcollina/msgpack5/blob/master/lib/codecs/DateCodec.js +let decode: (Js.TypedArray2.Uint8Array.t, int) => Message.t = %raw(`(array, byteLength) => { + let view = new DataView(array.buffer); + switch (byteLength) { + case 4: { + let seconds = view.getUint32(0, false); + let millis = seconds / 1000 | 0; + return new Date(millis); + } + case 8: { + let upper = view.getUint32(0); + let lower = view.getUint32(4); + let nanos = upper / 4; + let seconds = ((upper & 3) * (2 ** 32)) + lower; + let millis = seconds * 1000 + Math.round(nanos / 1e6); + return new Date(millis); + } + case 12: + throw new Error('timestamp96 is not supported yet'); + default: + throw new Error('invalid byteLength ' + byteLength); + } +}`) diff --git a/packages/rescript-msgpack/src/TimestampEncoder.res b/packages/rescript-msgpack/src/TimestampEncoder.res new file mode 100644 index 0000000..c2b08b2 --- /dev/null +++ b/packages/rescript-msgpack/src/TimestampEncoder.res @@ -0,0 +1,33 @@ +let \"type" = -1 + +let check: Js.Types.obj_val => bool = %raw(`x => x instanceof Date`) + +let encode: Js.Types.obj_val => Js.TypedArray2.Uint8Array.t = %raw(`obj => { + let uint64bound = 0x100000000; + + let millis = obj.getTime(); + let seconds = millis / 1000 | 0; + let nanos = (millis - (seconds * 1000)) * 1e6; + let bound = uint64bound - 1; + + if (nanos || seconds > bound) { + let array = new Uint8Array(10); + let view = new DataView(array.buffer); + let upperNanos = nanos * 4; + let upperSeconds = seconds / uint64bound; + let upper = (upperNanos + upperSeconds) & bound; + let lower = seconds & bound; + view.setUint8(0, 0xd7); + view.setInt8(1, -1); + view.setUint32(2, upper, false); + view.setUint32(6, lower, false); + return array; + } else { + let array = new Uint8Array(6); + let view = new DataView(array.buffer); + view.setUint8(0, 0xd6); + view.setInt8(1, -1); + view.setUint32(2, millis / 1000 | 0, false); + return array; + } +}`) diff --git a/yarn.lock b/yarn.lock index 81ebd96..9789f1b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -345,13 +345,6 @@ __metadata: languageName: node linkType: hard -"@rescript/std@npm:^9.1.3": - version: 9.1.3 - resolution: "@rescript/std@npm:9.1.3" - checksum: 100055c97210c4dcdef7606807f7d5f4e305f0ec6d2da74111d9993653d43fc5322318fec9a57be668f9770c42af139bf6347f870d95daa3a46cdf37b34aae8d - languageName: node - linkType: hard - "@tootallnate/once@npm:1": version: 1.1.2 resolution: "@tootallnate/once@npm:1.1.2" @@ -458,10 +451,11 @@ __metadata: version: 0.0.0-use.local resolution: "@urlpack/msgpack@workspace:packages/msgpack" dependencies: - "@rescript/std": ^9.1.3 concurrently: ^6.5.1 esbuild: ^0.14.9 nanobundle: ^0.0.21 + rescript: ^9.1.4 + rescript-msgpack: "workspace:^0.1.0" tsm: ^2.2.1 typescript: ^4.5.4 uvu: ^0.5.2 @@ -2823,6 +2817,28 @@ __metadata: languageName: node linkType: hard +"rescript-msgpack@workspace:^0.1.0, rescript-msgpack@workspace:packages/rescript-msgpack": + version: 0.0.0-use.local + resolution: "rescript-msgpack@workspace:packages/rescript-msgpack" + dependencies: + rescript: ^9.1.4 + peerDependencies: + rescript: ^9.0.0 + languageName: unknown + linkType: soft + +"rescript@npm:^9.1.4": + version: 9.1.4 + resolution: "rescript@npm:9.1.4" + bin: + bsc: bsc + bsrefmt: bsrefmt + bstracing: lib/bstracing + rescript: rescript + checksum: 3c503580f968b4d4409830093c57f85b5590f7372250345427b3a7c1f5ccb08009e1edd89b4a3dcbb3ee95ee4dcad9c337ece4d1f8fdaa10e22ed04b8a7495c0 + languageName: node + linkType: hard + "resolve-from@npm:^5.0.0": version: 5.0.0 resolution: "resolve-from@npm:5.0.0"