diff --git a/package.json b/package.json index cc88add..84d9d60 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "quickjs-emscripten-sync", + "name": "@open-social-protocol/quickjs-emscripten-sync", "author": "rot1024", - "version": "1.5.2", + "version": "1.5.4", "license": "MIT", "source": "./src/index.ts", "main": "./dist/quickjs-emscripten-sync.umd.js", @@ -27,18 +27,18 @@ "test": "vitest", "lint": "eslint ." }, - "peerDependencies": { - "quickjs-emscripten": "*" - }, + "peerDependencies": {}, "devDependencies": { "@vitest/coverage-c8": "^0.28.5", "eslint": "^8.22.0", "eslint-config-reearth": "^0.2.1", "prettier": "^2.7.1", - "quickjs-emscripten": "^0.21.0", "typescript": "^4.8.2", "vite": "^4.1.2", "vite-plugin-dts": "^2.0.0-beta.1", "vitest": "^0.28.5" + }, + "dependencies": { + "quickjs-emscripten": "^0.29.1" } } diff --git a/src/index.test.ts b/src/index.test.ts index f386eb0..4b49bda 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -209,12 +209,11 @@ describe("evalCode", () => { }); const res = vi.fn(); arena.evalCode(`(p, r) => { p.then(d => { r(d + "!"); }); }`)(promise, res); - deferred.resolve?.("hoge"); await promise; expect(arena.executePendingJobs()).toBe(1); expect(res).toBeCalledWith("hoge!"); - + await new Promise(resolve => setTimeout(resolve, 100)); arena.dispose(); ctx.dispose(); }); diff --git a/src/index.ts b/src/index.ts index b9ae857..2d6dfac 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ import type { QuickJSContext, SuccessOrFail, VmCallResult, + QuickJSAsyncContext, } from "quickjs-emscripten"; import { wrapContext, QuickJSContextEx } from "./contextex"; @@ -361,3 +362,19 @@ export class Arena { return unwrapHandle(this.context, target, this._symbolHandle); } } + +export class AsyncArena extends Arena { + asyncContext: QuickJSAsyncContext; + + constructor(ctx: QuickJSAsyncContext, options?: Options) { + super(ctx, options); + this.asyncContext = ctx; + } + /** + * Evaluate JS code in the VM and get the result as an object on the host side. It also converts and re-throws error objects when an error is thrown during evaluation. + */ + async evalCodeAsync(code: string): Promise { + const handle = await this.asyncContext.evalCodeAsync(code); + return this._unwrapResultAndUnmarshal(handle); + } +} diff --git a/src/marshal/index.test.ts b/src/marshal/index.test.ts index f5bbcf0..3799327 100644 --- a/src/marshal/index.test.ts +++ b/src/marshal/index.test.ts @@ -119,7 +119,7 @@ test("promise", async () => { deferred2.reject("bar"); await expect(deferred2.promise).rejects.toBe("bar"); - expect(ctx.unwrapResult(ctx.runtime.executePendingJobs())).toBe(1); + // expect(ctx.unwrapResult(ctx.runtime.executePendingJobs())).toBe(1); expect(notified).toEqual(["reject", "bar"]); register.dispose(); diff --git a/src/marshal/primitive.test.ts b/src/marshal/primitive.test.ts index 0ce0e9d..f65c5a2 100644 --- a/src/marshal/primitive.test.ts +++ b/src/marshal/primitive.test.ts @@ -1,7 +1,7 @@ import { getQuickJS } from "quickjs-emscripten"; import { expect, test } from "vitest"; -import { eq } from "../vmutil"; +import { arrayBufferEq, eq } from "../vmutil"; import marshalPrimitive from "./primitive"; @@ -15,13 +15,21 @@ test("works", async () => { expect(eq(ctx, marshalPrimitive(ctx, 1) ?? ctx.undefined, ctx.newNumber(1))).toBe(true); expect(eq(ctx, marshalPrimitive(ctx, -100) ?? ctx.undefined, ctx.newNumber(-100))).toBe(true); expect(eq(ctx, marshalPrimitive(ctx, "hoge") ?? ctx.undefined, ctx.newString("hoge"))).toBe(true); - // expect( - // eq( - // ctx, - // marshalPrimitive(ctx, BigInt(1)) ?? ctx.undefined, - // ctx.unwrapResult(ctx.evalCode("BigInt(1)")) - // ) - // ).toBe(true); + expect(eq(ctx, marshalPrimitive(ctx, BigInt(1)) ?? ctx.undefined, ctx.newBigInt(BigInt(1)))).toBe( + true, + ); + const arrayBuffer = new ArrayBuffer(10); + const uint8Array = new Uint8Array(arrayBuffer); + uint8Array[0] = 100; // Set the first byte to 100 (decimal) + uint8Array[1] = 255; // Set the second byte to 255 (decimal) + uint8Array[2] = 67; // Set the third byte to 67 (decimal) + expect( + arrayBufferEq( + ctx, + marshalPrimitive(ctx, arrayBuffer) ?? ctx.undefined, + ctx.newArrayBuffer(arrayBuffer), + ), + ).toBe(true); expect(marshalPrimitive(ctx, () => {})).toBe(undefined); expect(marshalPrimitive(ctx, [])).toBe(undefined); diff --git a/src/marshal/primitive.ts b/src/marshal/primitive.ts index 3f44853..083348b 100644 --- a/src/marshal/primitive.ts +++ b/src/marshal/primitive.ts @@ -6,6 +6,9 @@ export default function marshalPrimitive( ctx: QuickJSContext, target: unknown, ): QuickJSHandle | undefined { + if (target instanceof ArrayBuffer) { + return ctx.newArrayBuffer(target); + } switch (typeof target) { case "undefined": return ctx.undefined; @@ -17,15 +20,9 @@ export default function marshalPrimitive( return target ? ctx.true : ctx.false; case "object": return target === null ? ctx.null : undefined; - - // BigInt is not supported by quickjs-emscripten - // case "bigint": - // return call( - // ctx, - // `s => BigInt(s)`, - // undefined, - // ctx.newString(target.toString()) - // ); + // BigInt is now supported by quickjs-emscripten + case "bigint": + return ctx.newBigInt(target); } return undefined; diff --git a/src/marshal/promise.test.ts b/src/marshal/promise.test.ts index d3848ed..1d1e3cf 100644 --- a/src/marshal/promise.test.ts +++ b/src/marshal/promise.test.ts @@ -69,9 +69,10 @@ const testPromise = (reject: boolean) => async () => { } else { await expect(deferred.promise).resolves.toBe("hoge"); } - expect(ctx.runtime.hasPendingJob()).toBe(true); - const executed = ctx.unwrapResult(ctx.runtime.executePendingJobs()); - expect(executed).toBe(1); + // no need call executePendingJobs, it is called by promise.settled + // expect(ctx.runtime.hasPendingJob()).toBe(true); + // const executed = ctx.unwrapResult(ctx.runtime.executePendingJobs()); + // expect(executed).toBe(1); expect(mockNotify).toBeCalledTimes(1); expect(mockNotify).toBeCalledWith(reject ? "rejected" : "resolved", "hoge"); expect(marshal).toBeCalledTimes(1); diff --git a/src/marshal/promise.ts b/src/marshal/promise.ts index 572413e..250014b 100644 --- a/src/marshal/promise.ts +++ b/src/marshal/promise.ts @@ -10,6 +10,8 @@ export default function marshalPromise( const promise = ctx.newPromise(); + promise.settled.then(ctx.runtime.executePendingJobs); + target.then( d => promise.resolve(marshal(d)), d => promise.reject(marshal(d)), diff --git a/src/unmarshal/primitive.test.ts b/src/unmarshal/primitive.test.ts index d2375e5..7e96914 100644 --- a/src/unmarshal/primitive.test.ts +++ b/src/unmarshal/primitive.test.ts @@ -12,10 +12,9 @@ test("works", async () => { expect(unmarshalPrimitive(ctx, ctx.null)).toEqual([null, true]); expect(unmarshalPrimitive(ctx, ctx.newString("hoge"))).toEqual(["hoge", true]); expect(unmarshalPrimitive(ctx, ctx.newNumber(-10))).toEqual([-10, true]); - // expect( - // unmarshalPrimitive(ctx, ctx.unwrapResult(vm.evalCode(`BigInt(1)`))) - // ).toEqual([BigInt(1), true]); - + expect(unmarshalPrimitive(ctx, ctx.newBigInt(BigInt(1)))).toEqual([BigInt(1), true]); + const bufferHandle = ctx.newArrayBuffer(new ArrayBuffer(1)); + expect(unmarshalPrimitive(ctx, bufferHandle)).toEqual([new ArrayBuffer(1), true]); const obj = ctx.newObject(); expect(unmarshalPrimitive(ctx, obj)).toEqual([undefined, false]); const array = ctx.newArray(); @@ -23,6 +22,7 @@ test("works", async () => { const func = ctx.newFunction("", () => {}); expect(unmarshalPrimitive(ctx, func)).toEqual([undefined, false]); + bufferHandle.dispose(); obj.dispose(); array.dispose(); func.dispose(); diff --git a/src/unmarshal/primitive.ts b/src/unmarshal/primitive.ts index 1bb77e8..edbc121 100644 --- a/src/unmarshal/primitive.ts +++ b/src/unmarshal/primitive.ts @@ -1,11 +1,24 @@ import type { QuickJSContext, QuickJSHandle } from "quickjs-emscripten"; +import { isArrayBuffer } from "../vmutil"; + export default function unmarshalPrimitive( ctx: QuickJSContext, handle: QuickJSHandle, ): [any, boolean] { + if (isArrayBuffer(ctx, handle)) { + const value = ctx.getArrayBuffer(handle); + const buffer = value.value; + return [buffer.buffer, true]; + } const ty = ctx.typeof(handle); - if (ty === "undefined" || ty === "number" || ty === "string" || ty === "boolean") { + if ( + ty === "undefined" || + ty === "number" || + ty === "string" || + ty === "boolean" || + ty === "bigint" + ) { return [ctx.dump(handle), true]; } else if (ty === "object") { const isNull = ctx @@ -16,15 +29,5 @@ export default function unmarshalPrimitive( } } - // BigInt is not supported by quickjs-emscripten - // if (ty === "bigint") { - // const str = ctx - // .getProp(handle, "toString") - // .consume(toString => vm.unwrapResult(vm.callFunction(toString, handle))) - // .consume(str => ctx.getString(str)); - // const bi = BigInt(str); - // return [bi, true]; - // } - return [undefined, false]; } diff --git a/src/vmutil.ts b/src/vmutil.ts index 16be21a..ab821ca 100644 --- a/src/vmutil.ts +++ b/src/vmutil.ts @@ -35,6 +35,36 @@ export function call( } } +export function arrayBufferEq(ctx: QuickJSContext, a: QuickJSHandle, b: QuickJSHandle): boolean { + const val = ctx.dump( + call( + ctx, + `(buffer1, buffer2) => { + if (buffer1.byteLength !== buffer2.byteLength) { + return false; // Buffers must have the same size + } + + const view1 = new Uint8Array(buffer1); + const view2 = new Uint8Array(buffer2); + + for (let i = 0; i < buffer1.byteLength; i++) { + if (view1[i] !== view2[i]) { + return false; + } + } + return true; + } + `, + undefined, + a, + b, + ), + ); + a.dispose(); + b.dispose(); + return val; +} + export function eq(ctx: QuickJSContext, a: QuickJSHandle, b: QuickJSHandle): boolean { return ctx.dump(call(ctx, "Object.is", undefined, a, b)); } @@ -43,6 +73,10 @@ export function instanceOf(ctx: QuickJSContext, a: QuickJSHandle, b: QuickJSHand return ctx.dump(call(ctx, "(a, b) => a instanceof b", undefined, a, b)); } +export function isArrayBuffer(ctx: QuickJSContext, a: QuickJSHandle): boolean { + return ctx.dump(call(ctx, "(a, b) => a instanceof ArrayBuffer", undefined, a)); +} + export function isHandleObject(ctx: QuickJSContext, h: QuickJSHandle): boolean { return ctx.dump( call(ctx, `a => typeof a === "object" && a !== null || typeof a === "function"`, undefined, h), diff --git a/yarn.lock b/yarn.lock index 15e33e9..5fe1f13 100644 --- a/yarn.lock +++ b/yarn.lock @@ -161,6 +161,39 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== +"@jitl/quickjs-ffi-types@0.29.1": + version "0.29.1" + resolved "https://registry.yarnpkg.com/@jitl/quickjs-ffi-types/-/quickjs-ffi-types-0.29.1.tgz#445e1d94a411b166fd999d1880dd2152b5110531" + integrity sha512-dVt//PvFmwNJIwfyfPx7vR6cg7mLnAq6/THbhIeZ3NqXb66qXQATnW2gNqAnOcTh1D/f6HyrHFwRf3GdKUN3nw== + +"@jitl/quickjs-wasmfile-debug-asyncify@0.29.1": + version "0.29.1" + resolved "https://registry.yarnpkg.com/@jitl/quickjs-wasmfile-debug-asyncify/-/quickjs-wasmfile-debug-asyncify-0.29.1.tgz#e6e5540ec5bd40de3dddf5eaf88165aee603604b" + integrity sha512-jhxRDq+9ZWT8ddRt6JLcjLLF168EvFrOceiaoy+GuYYXJkWVrWk5p2Ray2GHzh+DL7byszRKEUAKqA9B86Rd9g== + dependencies: + "@jitl/quickjs-ffi-types" "0.29.1" + +"@jitl/quickjs-wasmfile-debug-sync@0.29.1": + version "0.29.1" + resolved "https://registry.yarnpkg.com/@jitl/quickjs-wasmfile-debug-sync/-/quickjs-wasmfile-debug-sync-0.29.1.tgz#a47cad4bef0015210c930e94ca833b86e27cf40a" + integrity sha512-qn6cxqg2fvobiJtQ/xHVSZH55djraIQ7g+9XGyJjf+ofsn1taMG8T5TIR2iZL7TMSdCRZkGiJeNOp9UaFCTN2w== + dependencies: + "@jitl/quickjs-ffi-types" "0.29.1" + +"@jitl/quickjs-wasmfile-release-asyncify@0.29.1": + version "0.29.1" + resolved "https://registry.yarnpkg.com/@jitl/quickjs-wasmfile-release-asyncify/-/quickjs-wasmfile-release-asyncify-0.29.1.tgz#97f817d3f2c67e893b468615d7fa1f627c12b2e1" + integrity sha512-1PD+TEw5raGk5s7G1N89gaT81J5A1N6PT79ixbm70gaU0xdUJyAUJoiLiS3YwLjcbpiiM0f7Vnf85M3Li914aQ== + dependencies: + "@jitl/quickjs-ffi-types" "0.29.1" + +"@jitl/quickjs-wasmfile-release-sync@0.29.1": + version "0.29.1" + resolved "https://registry.yarnpkg.com/@jitl/quickjs-wasmfile-release-sync/-/quickjs-wasmfile-release-sync-0.29.1.tgz#7066981f98a23f282d4a1f3aeed69a96166fff0e" + integrity sha512-iVeMmZXIDg3gD0elfj5FscwrqGLmcglvpvWDIzs5tmzJ4AKiDAHXunGwd8X4gifeW6S+f0j681M8+tZuUIqnJA== + dependencies: + "@jitl/quickjs-ffi-types" "0.29.1" + "@jridgewell/resolve-uri@^3.0.3": version "3.1.0" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" @@ -2161,10 +2194,23 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== -quickjs-emscripten@^0.21.0: - version "0.21.1" - resolved "https://registry.yarnpkg.com/quickjs-emscripten/-/quickjs-emscripten-0.21.1.tgz#5ebf67e4cc7ff5b4b7c0384b1db373b98670920e" - integrity sha512-/WJDTXtyL+lZ4D8bPCMsYWRYcbVUy6VFaHd6XSmz2wxRs6/j3apAjYTeQ5Q4vnO3ydsQ4CGl2hlE2kZ8Frb/Dw== +quickjs-emscripten-core@0.29.1: + version "0.29.1" + resolved "https://registry.yarnpkg.com/quickjs-emscripten-core/-/quickjs-emscripten-core-0.29.1.tgz#46bee69489343243a35f8be6df1cfd513ecee7ea" + integrity sha512-urFIWrPgKfzLFe/IwE8px3Oznb7UGsXpMjHLe15PTPbWongXmy6KAyxRpYFW8K/dDELilg0H/voysHbWHKj6uA== + dependencies: + "@jitl/quickjs-ffi-types" "0.29.1" + +quickjs-emscripten@^0.29.1: + version "0.29.1" + resolved "https://registry.yarnpkg.com/quickjs-emscripten/-/quickjs-emscripten-0.29.1.tgz#f272eb8947fa00e0c9b9c8cddf7584e6b4d314b1" + integrity sha512-9WX9cbbPW2/Wg/vedfAV6lL1kJB68xvE7kuILM24dkcIB/NCrIscZ94OdTGi9/77jYEJuajkzoQnHVn2BLzdaQ== + dependencies: + "@jitl/quickjs-wasmfile-debug-asyncify" "0.29.1" + "@jitl/quickjs-wasmfile-debug-sync" "0.29.1" + "@jitl/quickjs-wasmfile-release-asyncify" "0.29.1" + "@jitl/quickjs-wasmfile-release-sync" "0.29.1" + quickjs-emscripten-core "0.29.1" react-is@^16.13.1: version "16.13.1"