Skip to content

Commit 6d264b8

Browse files
committed
DisposableResult
1 parent b2a0af5 commit 6d264b8

File tree

5 files changed

+140
-44
lines changed

5 files changed

+140
-44
lines changed

packages/quickjs-emscripten-core/src/context-asyncify.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
JSRuntimePointer,
77
JSValuePointer,
88
} from "@jitl/quickjs-ffi-types"
9+
import type { ContextResult } from "./context"
910
import { QuickJSContext } from "./context"
1011
import { debugLog } from "./debug"
1112
import type { Lifetime } from "./lifetime"
@@ -46,7 +47,7 @@ export class QuickJSAsyncContext extends QuickJSContext {
4647
filename: string = "eval.js",
4748
/** See {@link EvalFlags} for number semantics */
4849
options?: number | ContextEvalOptions,
49-
): Promise<VmCallResult<QuickJSHandle>> {
50+
): Promise<ContextResult<QuickJSHandle>> {
5051
const detectModule = (options === undefined ? 1 : 0) as EvalDetectModule
5152
const flags = evalOptionsToFlags(options) as EvalFlags
5253
let resultPtr = 0 as JSValuePointer
@@ -70,9 +71,9 @@ export class QuickJSAsyncContext extends QuickJSContext {
7071
const errorPtr = this.ffi.QTS_ResolveException(this.ctx.value, resultPtr)
7172
if (errorPtr) {
7273
this.ffi.QTS_FreeValuePointer(this.ctx.value, resultPtr)
73-
return { error: this.memory.heapValueHandle(errorPtr) }
74+
return this.fail(this.memory.heapValueHandle(errorPtr))
7475
}
75-
return { value: this.memory.heapValueHandle(resultPtr) }
76+
return this.success(this.memory.heapValueHandle(resultPtr))
7677
}
7778

7879
/**

packages/quickjs-emscripten-core/src/context.ts

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ import { QuickJSDeferredPromise } from "./deferred-promise"
1818
// eslint-disable-next-line @typescript-eslint/no-unused-vars
1919
import type { shouldInterruptAfterDeadline } from "./interrupt-helpers"
2020
import { QuickJSPromisePending, QuickJSUnwrapError } from "./errors"
21-
import type { Disposable, DisposableArray } from "./lifetime"
21+
import type { Disposable, DisposableArray, DisposableFail, DisposableSuccess } from "./lifetime"
2222
import {
23+
DisposableResult,
2324
Lifetime,
2425
Scope,
2526
StaticLifetime,
@@ -35,25 +36,25 @@ import type {
3536
// eslint-disable-next-line @typescript-eslint/no-unused-vars
3637
ExecutePendingJobsResult,
3738
} from "./runtime"
38-
import {
39+
import type {
3940
ContextEvalOptions,
40-
IsEqualOp,
4141
GetOwnPropertyNamesOptions,
4242
JSValue,
4343
PromiseExecutor,
4444
QuickJSHandle,
4545
StaticJSValue,
4646
} from "./types"
47-
import { evalOptionsToFlags, getOwnPropertyNamesOptionsToFlags } from "./types"
47+
import { IsEqualOp, evalOptionsToFlags, getOwnPropertyNamesOptionsToFlags } from "./types"
4848
import type {
4949
LowLevelJavascriptVm,
5050
SuccessOrFail,
51-
VmCallResult,
5251
VmFunctionImplementation,
5352
VmPropertyDescriptor,
5453
} from "./vm-interface"
5554
import { QuickJSIterator } from "./QuickJSIterator"
5655

56+
export type ContextResult<S> = DisposableResult<S, QuickJSHandle>
57+
5758
/**
5859
* Property key for getting or setting a property on a handle with
5960
* {@link QuickJSContext#getProp}, {@link QuickJSContext#setProp}, or {@link QuickJSContext#defineProp}.
@@ -744,7 +745,7 @@ export class QuickJSContext
744745
*
745746
* @param promiseLikeHandle - A handle to a Promise-like value with a `.then(onSuccess, onError)` method.
746747
*/
747-
resolvePromise(promiseLikeHandle: QuickJSHandle): Promise<VmCallResult<QuickJSHandle>> {
748+
resolvePromise(promiseLikeHandle: QuickJSHandle): Promise<ContextResult<QuickJSHandle>> {
748749
this.runtime.assertOwned(promiseLikeHandle)
749750
const vmResolveResult = Scope.withScope((scope) => {
750751
const vmPromise = scope.manage(this.getProp(this.global, "Promise"))
@@ -755,25 +756,25 @@ export class QuickJSContext
755756
return Promise.resolve(vmResolveResult)
756757
}
757758

758-
return new Promise<VmCallResult<QuickJSHandle>>((resolve) => {
759+
return new Promise<ContextResult<QuickJSHandle>>((resolve) => {
759760
Scope.withScope((scope) => {
760761
const resolveHandle = scope.manage(
761762
this.newFunction("resolve", (value) => {
762-
resolve({ value: value && value.dup() })
763+
resolve(this.success(value && value.dup()))
763764
}),
764765
)
765766

766767
const rejectHandle = scope.manage(
767768
this.newFunction("reject", (error) => {
768-
resolve({ error: error && error.dup() })
769+
resolve(this.fail(error && error.dup()))
769770
}),
770771
)
771772

772773
const promiseHandle = scope.manage(vmResolveResult.value)
773774
const promiseThenHandle = scope.manage(this.getProp(promiseHandle, "then"))
774-
this.unwrapResult(
775-
this.callFunction(promiseThenHandle, promiseHandle, resolveHandle, rejectHandle),
776-
).dispose()
775+
this.callFunction(promiseThenHandle, promiseHandle, resolveHandle, rejectHandle)
776+
.unwrap()
777+
.dispose()
777778
})
778779
})
779780
}
@@ -861,7 +862,7 @@ export class QuickJSContext
861862
getPropNames(
862863
handle: QuickJSHandle,
863864
options: GetOwnPropertyNamesOptions,
864-
): SuccessOrFail<DisposableArray<JSValue>, QuickJSHandle> {
865+
): ContextResult<DisposableArray<JSValue>> {
865866
this.runtime.assertOwned(handle)
866867
handle.value // assert alive
867868
const flags = getOwnPropertyNamesOptionsToFlags(options)
@@ -875,7 +876,7 @@ export class QuickJSContext
875876
flags,
876877
)
877878
if (errorPtr) {
878-
return { error: this.memory.heapValueHandle(errorPtr) }
879+
return this.fail(this.memory.heapValueHandle(errorPtr))
879880
}
880881
const len = this.uint32Out.value.typedArray[0]
881882
const ptr = outPtr.value.typedArray[0]
@@ -884,7 +885,7 @@ export class QuickJSContext
884885
this.memory.heapValueHandle(ptr as JSValuePointer),
885886
)
886887
this.module._free(ptr)
887-
return { value: createDisposableArray(handles) }
888+
return this.success(createDisposableArray(handles))
888889
})
889890
}
890891

@@ -907,17 +908,17 @@ export class QuickJSContext
907908
* }
908909
* ```
909910
*/
910-
getIterator(handle: QuickJSHandle): SuccessOrFail<QuickJSIterator, QuickJSHandle> {
911+
getIterator(handle: QuickJSHandle): ContextResult<QuickJSIterator> {
911912
const SymbolIterator = (this._SymbolIterator ??= this.memory.manage(
912913
this.getWellKnownSymbol("iterator"),
913914
))
914915
return Scope.withScope((scope) => {
915916
const methodHandle = scope.manage(this.getProp(handle, SymbolIterator))
916917
const iteratorCallResult = this.callFunction(methodHandle, handle)
917918
if (iteratorCallResult.error) {
918-
return iteratorCallResult
919+
return iteratorCallResult.cast()
919920
}
920-
return { value: new QuickJSIterator(iteratorCallResult.value, this) }
921+
return this.success(new QuickJSIterator(iteratorCallResult.value, this))
921922
})
922923
}
923924

@@ -1000,7 +1001,7 @@ export class QuickJSContext
10001001
func: QuickJSHandle,
10011002
thisVal: QuickJSHandle,
10021003
...args: QuickJSHandle[]
1003-
): VmCallResult<QuickJSHandle> {
1004+
): ContextResult<QuickJSHandle> {
10041005
this.runtime.assertOwned(func)
10051006
const resultPtr = this.memory
10061007
.toPointerArray(args)
@@ -1017,10 +1018,10 @@ export class QuickJSContext
10171018
const errorPtr = this.ffi.QTS_ResolveException(this.ctx.value, resultPtr)
10181019
if (errorPtr) {
10191020
this.ffi.QTS_FreeValuePointer(this.ctx.value, resultPtr)
1020-
return { error: this.memory.heapValueHandle(errorPtr) }
1021+
return this.fail(this.memory.heapValueHandle(errorPtr))
10211022
}
10221023

1023-
return { value: this.memory.heapValueHandle(resultPtr) }
1024+
return this.success(this.memory.heapValueHandle(resultPtr))
10241025
}
10251026

10261027
/**
@@ -1066,7 +1067,7 @@ export class QuickJSContext
10661067
* See {@link EvalFlags} for number semantics.
10671068
*/
10681069
options?: number | ContextEvalOptions,
1069-
): VmCallResult<QuickJSHandle> {
1070+
): ContextResult<QuickJSHandle> {
10701071
const detectModule = (options === undefined ? 1 : 0) as EvalDetectModule
10711072
const flags = evalOptionsToFlags(options) as EvalFlags
10721073
const resultPtr = this.memory
@@ -1084,9 +1085,9 @@ export class QuickJSContext
10841085
const errorPtr = this.ffi.QTS_ResolveException(this.ctx.value, resultPtr)
10851086
if (errorPtr) {
10861087
this.ffi.QTS_FreeValuePointer(this.ctx.value, resultPtr)
1087-
return { error: this.memory.heapValueHandle(errorPtr) }
1088+
return this.fail(this.memory.heapValueHandle(errorPtr))
10881089
}
1089-
return { value: this.memory.heapValueHandle(resultPtr) }
1090+
return this.success(this.memory.heapValueHandle(resultPtr))
10901091
}
10911092

10921093
/**
@@ -1326,4 +1327,12 @@ export class QuickJSContext
13261327
const ptr = this.ffi.QTS_bjson_decode(this.ctx.value, handle.value)
13271328
return this.memory.heapValueHandle(ptr)
13281329
}
1330+
1331+
protected success<S>(value: S): DisposableSuccess<S, QuickJSHandle> {
1332+
return DisposableResult.success(value)
1333+
}
1334+
1335+
protected fail<S>(error: QuickJSHandle): DisposableFail<S, QuickJSHandle> {
1336+
return DisposableResult.fail(error, (error) => this.unwrapResult(error))
1337+
}
13291338
}

packages/quickjs-emscripten-core/src/lifetime.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { SuccessOrFail } from "./vm-interface"
12
import type { MaybeAsyncBlock } from "./asyncify-helpers"
23
import { maybeAsync } from "./asyncify-helpers"
34
import { QTS_DEBUG } from "./debug"
@@ -363,3 +364,95 @@ export function createDisposableArray<T extends Disposable>(
363364

364365
return array as T[] & Disposable
365366
}
367+
368+
function isDisposable(value: unknown): value is { alive: boolean; dispose(): unknown } {
369+
return Boolean(
370+
value &&
371+
(typeof value === "object" || typeof value === "function") &&
372+
"alive" in value &&
373+
typeof value.alive === "boolean" &&
374+
"dispose" in value &&
375+
typeof value.dispose === "function",
376+
)
377+
}
378+
379+
abstract class AbstractDisposableResult<S, F> extends UsingDisposable {
380+
static success<S, F>(value: S): DisposableSuccess<S, F> {
381+
return new DisposableSuccess(value) satisfies SuccessOrFail<S, F>
382+
}
383+
384+
static fail<S, F>(
385+
error: F,
386+
onUnwrap: (status: SuccessOrFail<S, F>) => void,
387+
): DisposableFail<S, F> {
388+
return new DisposableFail(error, onUnwrap) satisfies SuccessOrFail<S, F>
389+
}
390+
391+
static is<S, F>(result: SuccessOrFail<S, F>): result is DisposableResult<S, F> {
392+
return result instanceof AbstractDisposableResult
393+
}
394+
395+
abstract get alive(): boolean
396+
abstract dispose(): void
397+
abstract unwrap(): S
398+
abstract unwrapOr<T>(fallback: T): S | T
399+
}
400+
401+
export class DisposableSuccess<S, F> extends AbstractDisposableResult<S, F> {
402+
declare error?: undefined
403+
404+
constructor(readonly value: S) {
405+
super()
406+
}
407+
408+
override get alive() {
409+
return isDisposable(this.value) ? this.value.alive : true
410+
}
411+
412+
override dispose(): void {
413+
if (isDisposable(this.value)) {
414+
this.value.dispose()
415+
}
416+
}
417+
418+
override unwrap(): S {
419+
return this.value
420+
}
421+
422+
override unwrapOr<T>(_fallback: T): S | T {
423+
return this.value
424+
}
425+
}
426+
427+
export class DisposableFail<S, F> extends AbstractDisposableResult<S, F> {
428+
constructor(
429+
readonly error: F,
430+
private readonly onUnwrap: (status: SuccessOrFail<S, F>) => void,
431+
) {
432+
super()
433+
}
434+
435+
override get alive(): boolean {
436+
return isDisposable(this.error) ? this.error.alive : true
437+
}
438+
439+
override dispose(): void {
440+
throw new Error("Method not implemented.")
441+
}
442+
443+
override unwrap(): S {
444+
this.onUnwrap(this)
445+
throw this.error
446+
}
447+
448+
override unwrapOr<T>(fallback: T): S | T {
449+
return fallback
450+
}
451+
452+
cast<S2 = never>(): DisposableFail<S2, F> {
453+
return this as unknown as DisposableFail<S2, F>
454+
}
455+
}
456+
457+
export type DisposableResult<S, F> = DisposableSuccess<S, F> | DisposableFail<S, F>
458+
export const DisposableResult = AbstractDisposableResult

packages/quickjs-emscripten-core/src/runtime.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,11 @@ import { QuickJSContext } from "./context"
1111
import { debugLog } from "./debug"
1212
import { QuickJSWrongOwner } from "./errors"
1313
import type { Disposable } from "./lifetime"
14-
import { Lifetime, Scope, UsingDisposable } from "./lifetime"
14+
import { DisposableResult, Lifetime, Scope, UsingDisposable } from "./lifetime"
1515
import { ModuleMemory } from "./memory"
1616
import type { QuickJSModuleCallbacks, RuntimeCallbacks } from "./module"
1717
import type { ContextOptions, JSModuleLoader, JSModuleNormalizer, QuickJSHandle } from "./types"
1818
import { intrinsicsToFlags } from "./types"
19-
import type { SuccessOrFail } from "./vm-interface"
2019

2120
/**
2221
* Callback called regularly while the VM executes code.
@@ -33,7 +32,7 @@ export type InterruptHandler = (runtime: QuickJSRuntime) => boolean | undefined
3332
* by the runtime.
3433
* @source
3534
*/
36-
export type ExecutePendingJobsResult = SuccessOrFail<
35+
export type ExecutePendingJobsResult = DisposableResult<
3736
/** Number of jobs successfully executed. */
3837
number,
3938
/** The error that occurred. */
@@ -250,7 +249,7 @@ export class QuickJSRuntime extends UsingDisposable implements Disposable {
250249
if (ctxPtr === 0) {
251250
// No jobs executed.
252251
this.ffi.QTS_FreeValuePointerRuntime(this.rt.value, valuePtr)
253-
return { value: 0 }
252+
return DisposableResult.success(0)
254253
}
255254

256255
const context =
@@ -264,12 +263,10 @@ export class QuickJSRuntime extends UsingDisposable implements Disposable {
264263
if (typeOfRet === "number") {
265264
const executedJobs = context.getNumber(resultValue)
266265
resultValue.dispose()
267-
return { value: executedJobs }
266+
return DisposableResult.success(executedJobs)
268267
} else {
269-
const error = Object.assign(resultValue, { context })
270-
return {
271-
error,
272-
}
268+
const error = Object.assign(resultValue as QuickJSHandle, { context })
269+
return DisposableResult.fail(error, (error) => context.unwrapResult(error))
273270
}
274271
}
275272

packages/quickjs-emscripten/src/quickjs.test.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -414,11 +414,9 @@ function contextTests(getContext: GetTestContext, isDebug = false) {
414414
it('gets object keys that are symbols only when "includeSymbols" is true', () => {
415415
const obj = vm.newObject()
416416
const symA = vm.newUniqueSymbol("a")
417-
const symB = vm.newUniqueSymbol("b")
418-
const symC = vm.newUniqueSymbol("c")
419417
vm.setProp(obj, symA, vm.undefined)
420-
vm.setProp(obj, symB, vm.undefined)
421-
vm.setProp(obj, symC, vm.undefined)
418+
vm.setProp(obj, "b", vm.undefined)
419+
vm.setProp(obj, "c", vm.undefined)
422420

423421
const props = vm.unwrapResult(
424422
vm.getPropNames(obj, {
@@ -427,10 +425,8 @@ function contextTests(getContext: GetTestContext, isDebug = false) {
427425
}),
428426
)
429427

430-
assert.strictEqual(props.length, 3)
431-
assert.strictEqual(vm.dump(props[0]), symA)
432-
assert.strictEqual(vm.dump(props[1]), symB)
433-
assert.strictEqual(vm.dump(props[2]), symC)
428+
assert.strictEqual(props.length, 1)
429+
assert.ok(vm.eq(props[0], symA))
434430
props.dispose()
435431
})
436432
})

0 commit comments

Comments
 (0)