quickjs-emscripten-sync 1.8.4 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +231 -150
- package/dist/index.d.ts +45 -7
- package/dist/quickjs-emscripten-sync.js +573 -342
- package/dist/quickjs-emscripten-sync.umd.cjs +17 -8
- package/package.json +4 -2
- package/src/index.test.ts +250 -5
- package/src/index.ts +85 -7
- package/src/marshal/custom.ts +16 -2
- package/src/marshal/hostref.ts +18 -0
- package/src/marshal/index.ts +12 -0
- package/src/marshal/mapset.ts +28 -0
- package/src/marshal/primitive.ts +2 -11
- package/src/marshal/properties.ts +8 -6
- package/src/unmarshal/custom.ts +35 -4
- package/src/unmarshal/function.ts +2 -2
- package/src/unmarshal/hostref.ts +21 -0
- package/src/unmarshal/index.ts +21 -1
- package/src/unmarshal/mapset.ts +43 -0
- package/src/unmarshal/object.ts +16 -20
- package/src/unmarshal/primitive.ts +6 -17
- package/src/unmarshal/properties.ts +7 -6
- package/src/vmmap.ts +59 -69
- package/src/vmutil.ts +94 -11
- package/src/wrapper.ts +97 -30
package/src/vmmap.ts
CHANGED
|
@@ -1,18 +1,29 @@
|
|
|
1
1
|
import type { QuickJSContext, QuickJSHandle } from "quickjs-emscripten";
|
|
2
2
|
|
|
3
|
+
import { unwrapResult } from "./vmutil";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Bidirectional map between host values and QuickJS handles.
|
|
7
|
+
*
|
|
8
|
+
* Each registered pair gets a numeric id. A value may be registered under two
|
|
9
|
+
* keys (e.g. a proxy-wrapped object and the underlying object), each with its
|
|
10
|
+
* own handle, so every lookup direction is backed by an explicit map and id
|
|
11
|
+
* reverse-lookups, keeping `delete` O(1).
|
|
12
|
+
*/
|
|
3
13
|
export default class VMMap {
|
|
4
14
|
ctx: QuickJSContext;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
15
|
+
_keyToId = new Map<any, number>();
|
|
16
|
+
_key2ToId = new Map<any, number>();
|
|
17
|
+
_idToHandle = new Map<number, QuickJSHandle>();
|
|
18
|
+
_idToHandle2 = new Map<number, QuickJSHandle>();
|
|
19
|
+
_idToKey = new Map<number, any>();
|
|
20
|
+
_idToKey2 = new Map<number, any>();
|
|
10
21
|
_disposables = new Set<QuickJSHandle>();
|
|
11
22
|
_mapGet: QuickJSHandle;
|
|
12
23
|
_mapSet: QuickJSHandle;
|
|
13
24
|
_mapDelete: QuickJSHandle;
|
|
14
25
|
_mapClear: QuickJSHandle;
|
|
15
|
-
|
|
26
|
+
_nextId = Number.MIN_SAFE_INTEGER;
|
|
16
27
|
|
|
17
28
|
constructor(ctx: QuickJSContext) {
|
|
18
29
|
this.ctx = ctx;
|
|
@@ -68,18 +79,19 @@ export default class VMMap {
|
|
|
68
79
|
return v === handle || v === handle2;
|
|
69
80
|
}
|
|
70
81
|
|
|
71
|
-
const
|
|
72
|
-
this.
|
|
73
|
-
this.
|
|
74
|
-
this.
|
|
82
|
+
const id = this._nextId++;
|
|
83
|
+
this._keyToId.set(key, id);
|
|
84
|
+
this._idToHandle.set(id, handle);
|
|
85
|
+
this._idToKey.set(id, key);
|
|
75
86
|
if (key2) {
|
|
76
|
-
this.
|
|
87
|
+
this._key2ToId.set(key2, id);
|
|
88
|
+
this._idToKey2.set(id, key2);
|
|
77
89
|
if (handle2) {
|
|
78
|
-
this.
|
|
90
|
+
this._idToHandle2.set(id, handle2);
|
|
79
91
|
}
|
|
80
92
|
}
|
|
81
93
|
|
|
82
|
-
this.ctx.newNumber(
|
|
94
|
+
this.ctx.newNumber(id).consume(c => {
|
|
83
95
|
this._call(this._mapSet, undefined, handle, c, handle2 ?? this.ctx.undefined);
|
|
84
96
|
});
|
|
85
97
|
|
|
@@ -104,8 +116,8 @@ export default class VMMap {
|
|
|
104
116
|
}
|
|
105
117
|
|
|
106
118
|
get(key: any) {
|
|
107
|
-
const
|
|
108
|
-
const handle = typeof
|
|
119
|
+
const id = this._keyToId.get(key) ?? this._key2ToId.get(key);
|
|
120
|
+
const handle = typeof id === "number" ? this._idToHandle.get(id) : undefined;
|
|
109
121
|
|
|
110
122
|
if (!handle) return;
|
|
111
123
|
if (!handle.alive) {
|
|
@@ -120,7 +132,7 @@ export default class VMMap {
|
|
|
120
132
|
if (!handle.alive) {
|
|
121
133
|
return;
|
|
122
134
|
}
|
|
123
|
-
return this.
|
|
135
|
+
return this._idToKey.get(this.ctx.getNumber(this._call(this._mapGet, undefined, handle)));
|
|
124
136
|
}
|
|
125
137
|
|
|
126
138
|
has(key: any) {
|
|
@@ -132,46 +144,29 @@ export default class VMMap {
|
|
|
132
144
|
}
|
|
133
145
|
|
|
134
146
|
keys() {
|
|
135
|
-
return this.
|
|
147
|
+
return this._keyToId.keys();
|
|
136
148
|
}
|
|
137
149
|
|
|
138
150
|
delete(key: any, dispose?: boolean) {
|
|
139
|
-
const
|
|
140
|
-
if (typeof
|
|
151
|
+
const id = this._keyToId.get(key) ?? this._key2ToId.get(key);
|
|
152
|
+
if (typeof id === "undefined") return;
|
|
141
153
|
|
|
142
|
-
const handle = this.
|
|
143
|
-
const handle2 = this.
|
|
154
|
+
const handle = this._idToHandle.get(id);
|
|
155
|
+
const handle2 = this._idToHandle2.get(id);
|
|
144
156
|
this._call(
|
|
145
157
|
this._mapDelete,
|
|
146
158
|
undefined,
|
|
147
159
|
...[handle, handle2].filter((h): h is QuickJSHandle => !!h?.alive),
|
|
148
160
|
);
|
|
149
161
|
|
|
150
|
-
this.
|
|
151
|
-
this.
|
|
152
|
-
this.
|
|
153
|
-
this.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
break;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
for (const [k, v] of this._map2) {
|
|
163
|
-
if (v === num) {
|
|
164
|
-
this._map2.delete(k);
|
|
165
|
-
break;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
for (const [k, v] of this._counterMap) {
|
|
170
|
-
if (v === key) {
|
|
171
|
-
this._counterMap.delete(k);
|
|
172
|
-
break;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
162
|
+
const key1 = this._idToKey.get(id);
|
|
163
|
+
const key2 = this._idToKey2.get(id);
|
|
164
|
+
if (typeof key1 !== "undefined") this._keyToId.delete(key1);
|
|
165
|
+
if (typeof key2 !== "undefined") this._key2ToId.delete(key2);
|
|
166
|
+
this._idToHandle.delete(id);
|
|
167
|
+
this._idToHandle2.delete(id);
|
|
168
|
+
this._idToKey.delete(id);
|
|
169
|
+
this._idToKey2.delete(id);
|
|
175
170
|
|
|
176
171
|
if (dispose) {
|
|
177
172
|
if (handle?.alive) handle.dispose();
|
|
@@ -187,12 +182,13 @@ export default class VMMap {
|
|
|
187
182
|
}
|
|
188
183
|
|
|
189
184
|
clear() {
|
|
190
|
-
this.
|
|
191
|
-
this.
|
|
192
|
-
this.
|
|
193
|
-
this.
|
|
194
|
-
this.
|
|
195
|
-
this.
|
|
185
|
+
this._nextId = 0;
|
|
186
|
+
this._keyToId.clear();
|
|
187
|
+
this._key2ToId.clear();
|
|
188
|
+
this._idToHandle.clear();
|
|
189
|
+
this._idToHandle2.clear();
|
|
190
|
+
this._idToKey.clear();
|
|
191
|
+
this._idToKey2.clear();
|
|
196
192
|
if (this._mapClear.alive) {
|
|
197
193
|
this._call(this._mapClear, undefined);
|
|
198
194
|
}
|
|
@@ -204,12 +200,12 @@ export default class VMMap {
|
|
|
204
200
|
v.dispose();
|
|
205
201
|
}
|
|
206
202
|
}
|
|
207
|
-
for (const v of this.
|
|
203
|
+
for (const v of this._idToHandle.values()) {
|
|
208
204
|
if (v.alive) {
|
|
209
205
|
v.dispose();
|
|
210
206
|
}
|
|
211
207
|
}
|
|
212
|
-
for (const v of this.
|
|
208
|
+
for (const v of this._idToHandle2.values()) {
|
|
213
209
|
if (v.alive) {
|
|
214
210
|
v.dispose();
|
|
215
211
|
}
|
|
@@ -219,37 +215,31 @@ export default class VMMap {
|
|
|
219
215
|
}
|
|
220
216
|
|
|
221
217
|
get size() {
|
|
222
|
-
return this.
|
|
218
|
+
return this._keyToId.size;
|
|
223
219
|
}
|
|
224
220
|
|
|
225
221
|
[Symbol.iterator](): Iterator<[any, QuickJSHandle, any, QuickJSHandle | undefined]> {
|
|
226
|
-
const keys = this.
|
|
222
|
+
const keys = this._keyToId.keys();
|
|
227
223
|
return {
|
|
228
224
|
next: () => {
|
|
229
|
-
|
|
230
225
|
while (true) {
|
|
231
226
|
const k1 = keys.next();
|
|
232
227
|
if (k1.done) return { value: undefined, done: true };
|
|
233
|
-
const
|
|
234
|
-
if (typeof
|
|
235
|
-
const v1 = this.
|
|
236
|
-
const v2 = this.
|
|
228
|
+
const id = this._keyToId.get(k1.value);
|
|
229
|
+
if (typeof id === "undefined") continue;
|
|
230
|
+
const v1 = this._idToHandle.get(id);
|
|
231
|
+
const v2 = this._idToHandle2.get(id);
|
|
237
232
|
if (!v1) continue;
|
|
238
|
-
const k2 = this.
|
|
233
|
+
const k2 = this._idToKey2.get(id);
|
|
239
234
|
return { value: [k1.value, v1, k2, v2], done: false };
|
|
240
235
|
}
|
|
241
236
|
},
|
|
242
237
|
};
|
|
243
238
|
}
|
|
244
239
|
|
|
245
|
-
_get2(num: number) {
|
|
246
|
-
for (const [k, v] of this._map2) {
|
|
247
|
-
if (v === num) return k;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
240
|
_call(fn: QuickJSHandle, thisArg: QuickJSHandle | undefined, ...args: QuickJSHandle[]) {
|
|
252
|
-
return
|
|
241
|
+
return unwrapResult(
|
|
242
|
+
this.ctx,
|
|
253
243
|
this.ctx.callFunction(
|
|
254
244
|
fn,
|
|
255
245
|
typeof thisArg === "undefined" ? this.ctx.undefined : thisArg,
|
package/src/vmutil.ts
CHANGED
|
@@ -3,38 +3,108 @@ import type {
|
|
|
3
3
|
QuickJSContext,
|
|
4
4
|
QuickJSHandle,
|
|
5
5
|
QuickJSDeferredPromise,
|
|
6
|
+
SuccessOrFail,
|
|
6
7
|
} from "quickjs-emscripten";
|
|
7
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Unwrap a VM result, disposing the error handle on failure.
|
|
11
|
+
*
|
|
12
|
+
* `ctx.unwrapResult` throws the error handle without disposing it, which leaks
|
|
13
|
+
* the handle. Under memory pressure the leak cascades: reading the error needs
|
|
14
|
+
* the VM, which is already exhausted, so cleanup is skipped entirely. Here we
|
|
15
|
+
* read the error host-side, always dispose the handle, then throw a host Error.
|
|
16
|
+
*/
|
|
17
|
+
export function unwrapResult<T>(ctx: QuickJSContext, result: SuccessOrFail<T, QuickJSHandle>): T {
|
|
18
|
+
if ("error" in result && result.error) {
|
|
19
|
+
const { error } = result;
|
|
20
|
+
let dumped: any;
|
|
21
|
+
try {
|
|
22
|
+
dumped = ctx.dump(error);
|
|
23
|
+
} catch {
|
|
24
|
+
// VM may be unable to read the error (e.g. out of memory); fall back below.
|
|
25
|
+
} finally {
|
|
26
|
+
if (error.alive) error.dispose();
|
|
27
|
+
}
|
|
28
|
+
const err = new Error(
|
|
29
|
+
typeof dumped === "object" && dumped && "message" in dumped
|
|
30
|
+
? String(dumped.message)
|
|
31
|
+
: dumped !== undefined
|
|
32
|
+
? String(dumped)
|
|
33
|
+
: "quickjs-emscripten-sync: VM evaluation failed",
|
|
34
|
+
);
|
|
35
|
+
if (typeof dumped === "object" && dumped) {
|
|
36
|
+
if ("name" in dumped) err.name = String(dumped.name);
|
|
37
|
+
if ("stack" in dumped) err.stack = String(dumped.stack);
|
|
38
|
+
}
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
41
|
+
return result.value;
|
|
42
|
+
}
|
|
43
|
+
|
|
8
44
|
export function fn(
|
|
9
45
|
ctx: QuickJSContext,
|
|
10
46
|
code: string,
|
|
11
47
|
): ((thisArg: QuickJSHandle | undefined, ...args: QuickJSHandle[]) => QuickJSHandle) & Disposable {
|
|
12
|
-
const handle =
|
|
48
|
+
const handle = unwrapResult(ctx, ctx.evalCode(code));
|
|
13
49
|
const f: any = (thisArg: QuickJSHandle | undefined, ...args: QuickJSHandle[]): any => {
|
|
14
|
-
return
|
|
50
|
+
return unwrapResult(ctx, ctx.callFunction(handle, thisArg ?? ctx.undefined, ...args));
|
|
15
51
|
};
|
|
16
52
|
const disposeFn = () => handle.dispose();
|
|
17
53
|
f.dispose = disposeFn;
|
|
18
54
|
f[Symbol.dispose] = disposeFn;
|
|
19
|
-
f.alive = true;
|
|
20
55
|
Object.defineProperty(f, "alive", {
|
|
21
56
|
get: () => handle.alive,
|
|
22
57
|
});
|
|
23
58
|
return f;
|
|
24
59
|
}
|
|
25
60
|
|
|
61
|
+
// Compiled functions for `call` can be cached per context, keyed by code: the
|
|
62
|
+
// code passed to `call` is always a constant literal, so recompiling the same
|
|
63
|
+
// helper on every call (isHandleObject, defineProperties, etc.) was a dominant
|
|
64
|
+
// cost. Caching is opt-in per context via `enableFnCache` because cached
|
|
65
|
+
// handles outlive a single call and must be disposed with `disposeFnCache`;
|
|
66
|
+
// the Arena enables it in its constructor and disposes it in `dispose`. For
|
|
67
|
+
// contexts without a cache, `call` keeps its original compile-and-dispose
|
|
68
|
+
// behaviour so standalone callers don't leak handles.
|
|
69
|
+
const fnCache = new WeakMap<QuickJSContext, Map<string, QuickJSHandle>>();
|
|
70
|
+
|
|
71
|
+
/** Enable per-context caching of compiled functions used by `call`. */
|
|
72
|
+
export function enableFnCache(ctx: QuickJSContext): void {
|
|
73
|
+
if (!fnCache.has(ctx)) fnCache.set(ctx, new Map());
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Dispose all compiled functions cached for a context and disable caching. */
|
|
77
|
+
export function disposeFnCache(ctx: QuickJSContext): void {
|
|
78
|
+
const cache = fnCache.get(ctx);
|
|
79
|
+
if (!cache) return;
|
|
80
|
+
for (const handle of cache.values()) {
|
|
81
|
+
if (handle.alive) handle.dispose();
|
|
82
|
+
}
|
|
83
|
+
fnCache.delete(ctx);
|
|
84
|
+
}
|
|
85
|
+
|
|
26
86
|
export function call(
|
|
27
87
|
ctx: QuickJSContext,
|
|
28
88
|
code: string,
|
|
29
89
|
thisArg?: QuickJSHandle,
|
|
30
90
|
...args: QuickJSHandle[]
|
|
31
91
|
): QuickJSHandle {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
92
|
+
const cache = fnCache.get(ctx);
|
|
93
|
+
if (!cache) {
|
|
94
|
+
const f = fn(ctx, code);
|
|
95
|
+
try {
|
|
96
|
+
return f(thisArg, ...args);
|
|
97
|
+
} finally {
|
|
98
|
+
f.dispose();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let handle = cache.get(code);
|
|
103
|
+
if (!handle || !handle.alive) {
|
|
104
|
+
handle = unwrapResult(ctx, ctx.evalCode(code));
|
|
105
|
+
cache.set(code, handle);
|
|
37
106
|
}
|
|
107
|
+
return unwrapResult(ctx, ctx.callFunction(handle, thisArg ?? ctx.undefined, ...args));
|
|
38
108
|
}
|
|
39
109
|
|
|
40
110
|
export function instanceOf(ctx: QuickJSContext, a: QuickJSHandle, b: QuickJSHandle): boolean {
|
|
@@ -42,9 +112,8 @@ export function instanceOf(ctx: QuickJSContext, a: QuickJSHandle, b: QuickJSHand
|
|
|
42
112
|
}
|
|
43
113
|
|
|
44
114
|
export function isHandleObject(ctx: QuickJSContext, h: QuickJSHandle): boolean {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
);
|
|
115
|
+
const type = ctx.typeof(h);
|
|
116
|
+
return type === "function" || (type === "object" && !ctx.sameValue(h, ctx.null));
|
|
48
117
|
}
|
|
49
118
|
|
|
50
119
|
export function json(ctx: QuickJSContext, target: any): QuickJSHandle {
|
|
@@ -53,6 +122,20 @@ export function json(ctx: QuickJSContext, target: any): QuickJSHandle {
|
|
|
53
122
|
return call(ctx, `JSON.parse`, undefined, ctx.newString(json));
|
|
54
123
|
}
|
|
55
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Run `cb` with `handle`, then dispose `handle` even if `cb` throws.
|
|
127
|
+
*
|
|
128
|
+
* Unlike `handle.consume`, which skips disposal when its callback throws, this
|
|
129
|
+
* helper disposes in a `finally` so error paths don't leak the handle.
|
|
130
|
+
*/
|
|
131
|
+
export function consume<T extends QuickJSHandle, K>(handle: T, cb: (handle: T) => K): K {
|
|
132
|
+
try {
|
|
133
|
+
return cb(handle);
|
|
134
|
+
} finally {
|
|
135
|
+
if (handle.alive) handle.dispose();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
56
139
|
export function consumeAll<T extends QuickJSHandle[], K>(handles: T, cb: (handles: T) => K): K {
|
|
57
140
|
try {
|
|
58
141
|
return cb(handles);
|
package/src/wrapper.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { QuickJSHandle, QuickJSContext } from "quickjs-emscripten";
|
|
2
2
|
|
|
3
3
|
import { isObject } from "./util";
|
|
4
|
-
import { call, isHandleObject, mayConsumeAll } from "./vmutil";
|
|
4
|
+
import { call, consume, isHandleObject, mayConsumeAll } from "./vmutil";
|
|
5
5
|
|
|
6
6
|
export type SyncMode = "both" | "vm" | "host";
|
|
7
7
|
|
|
@@ -15,18 +15,28 @@ export function wrap<T = any>(
|
|
|
15
15
|
marshal: (target: any) => [QuickJSHandle, boolean],
|
|
16
16
|
syncMode?: (target: T) => SyncMode | undefined,
|
|
17
17
|
wrappable?: (target: unknown) => boolean,
|
|
18
|
+
syncEnabled = true,
|
|
18
19
|
): Wrapped<T> | undefined {
|
|
19
|
-
//
|
|
20
|
+
// These built-ins rely on internal slots or non-property access, so a proxy
|
|
21
|
+
// would break them; they are marshalled by value instead of being wrapped.
|
|
20
22
|
if (
|
|
21
23
|
!isObject(target) ||
|
|
22
24
|
target instanceof Promise ||
|
|
23
25
|
target instanceof Date ||
|
|
26
|
+
target instanceof ArrayBuffer ||
|
|
27
|
+
ArrayBuffer.isView(target) ||
|
|
28
|
+
target instanceof Map ||
|
|
29
|
+
target instanceof Set ||
|
|
24
30
|
(wrappable && !wrappable(target))
|
|
25
31
|
)
|
|
26
32
|
return undefined;
|
|
27
33
|
|
|
28
34
|
if (isWrapped(target, proxyKeySymbol)) return target;
|
|
29
35
|
|
|
36
|
+
// Sync globally disabled: skip the proxy, but still treat the object as
|
|
37
|
+
// "wrapped" so the rest of the pipeline handles it uniformly.
|
|
38
|
+
if (!syncEnabled) return target as Wrapped<T>;
|
|
39
|
+
|
|
30
40
|
const rec = new Proxy(target as any, {
|
|
31
41
|
get(obj, key) {
|
|
32
42
|
return key === proxyKeySymbol ? obj : Reflect.get(obj, key);
|
|
@@ -34,7 +44,9 @@ export function wrap<T = any>(
|
|
|
34
44
|
set(obj, key, value, receiver) {
|
|
35
45
|
const v = unwrap(value, proxyKeySymbol);
|
|
36
46
|
const sync = syncMode?.(receiver) ?? "host";
|
|
37
|
-
|
|
47
|
+
// Set on the target directly (not via `receiver`) so creating a new
|
|
48
|
+
// property does not re-enter the `defineProperty` trap.
|
|
49
|
+
if ((sync !== "vm" && !Reflect.set(obj, key, v)) || sync === "host" || !ctx.alive)
|
|
38
50
|
return true;
|
|
39
51
|
|
|
40
52
|
mayConsumeAll(
|
|
@@ -68,6 +80,35 @@ export function wrap<T = any>(
|
|
|
68
80
|
return true;
|
|
69
81
|
});
|
|
70
82
|
},
|
|
83
|
+
defineProperty(obj, key, descriptor) {
|
|
84
|
+
const sync = syncMode?.(rec) ?? "host";
|
|
85
|
+
const desc: PropertyDescriptor = { ...descriptor };
|
|
86
|
+
if ("value" in desc) desc.value = unwrap(desc.value, proxyKeySymbol);
|
|
87
|
+
if (typeof desc.get === "function") desc.get = unwrap(desc.get, proxyKeySymbol);
|
|
88
|
+
if (typeof desc.set === "function") desc.set = unwrap(desc.set, proxyKeySymbol);
|
|
89
|
+
|
|
90
|
+
if (sync !== "vm" && !Reflect.defineProperty(obj, key, desc)) return false;
|
|
91
|
+
if (sync === "host" || !ctx.alive) return true;
|
|
92
|
+
|
|
93
|
+
mayConsumeAll(
|
|
94
|
+
[marshal(rec), marshal(key), marshal(desc)],
|
|
95
|
+
(recHandle, keyHandle, descHandle) => {
|
|
96
|
+
const [handle2, unwrapped] = unwrapHandle(ctx, recHandle, proxyKeySymbolHandle);
|
|
97
|
+
const define = (h: QuickJSHandle) =>
|
|
98
|
+
call(
|
|
99
|
+
ctx,
|
|
100
|
+
`(o, k, d) => { Object.defineProperty(o, k, d); }`,
|
|
101
|
+
undefined,
|
|
102
|
+
h,
|
|
103
|
+
keyHandle,
|
|
104
|
+
descHandle,
|
|
105
|
+
).dispose();
|
|
106
|
+
if (unwrapped) handle2.consume(define);
|
|
107
|
+
else define(handle2);
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
return true;
|
|
111
|
+
},
|
|
71
112
|
}) as Wrapped<T>;
|
|
72
113
|
return rec;
|
|
73
114
|
}
|
|
@@ -80,12 +121,16 @@ export function wrapHandle(
|
|
|
80
121
|
unmarshal: (handle: QuickJSHandle) => any,
|
|
81
122
|
syncMode?: (target: QuickJSHandle) => SyncMode | undefined,
|
|
82
123
|
wrappable?: (target: QuickJSHandle, ctx: QuickJSContext) => boolean,
|
|
124
|
+
syncEnabled = true,
|
|
83
125
|
): [Wrapped<QuickJSHandle> | undefined, boolean] {
|
|
84
126
|
if (!isHandleObject(ctx, handle) || (wrappable && !wrappable(handle, ctx)))
|
|
85
127
|
return [undefined, false];
|
|
86
128
|
|
|
87
129
|
if (isHandleWrapped(ctx, handle, proxyKeySymbolHandle)) return [handle, false];
|
|
88
130
|
|
|
131
|
+
// Sync globally disabled: skip the VM-side proxy.
|
|
132
|
+
if (!syncEnabled) return [handle as Wrapped<QuickJSHandle>, false];
|
|
133
|
+
|
|
89
134
|
const getSyncMode = (h: QuickJSHandle) => {
|
|
90
135
|
const res = syncMode?.(unmarshal(h));
|
|
91
136
|
if (typeof res === "string") return ctx.newString(res);
|
|
@@ -108,23 +153,35 @@ export function wrapHandle(
|
|
|
108
153
|
Reflect.deleteProperty(unwrap(target, proxyKeySymbol), key);
|
|
109
154
|
};
|
|
110
155
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
156
|
+
const definer = (h: QuickJSHandle, keyHandle: QuickJSHandle, descHandle: QuickJSHandle) => {
|
|
157
|
+
const target = unmarshal(h);
|
|
158
|
+
if (!target) return;
|
|
159
|
+
const key = unmarshal(keyHandle);
|
|
160
|
+
if (key === "__proto__") return; // for security
|
|
161
|
+
const desc = unmarshal(descHandle);
|
|
162
|
+
Object.defineProperty(unwrap(target, proxyKeySymbol), key, desc);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const proxyFuncs = ctx.newFunction("proxyFuncs", (t, ...args) => {
|
|
166
|
+
const name = ctx.getNumber(t);
|
|
167
|
+
switch (name) {
|
|
168
|
+
case 1:
|
|
169
|
+
return getSyncMode(args[0]);
|
|
170
|
+
case 2:
|
|
171
|
+
return setter(args[0], args[1], args[2]);
|
|
172
|
+
case 3:
|
|
173
|
+
return deleter(args[0], args[1]);
|
|
174
|
+
case 4:
|
|
175
|
+
return definer(args[0], args[1], args[2]);
|
|
176
|
+
}
|
|
177
|
+
return ctx.undefined;
|
|
178
|
+
});
|
|
179
|
+
// Use the exception-safe consume so proxyFuncs is disposed even if compiling
|
|
180
|
+
// the proxy below throws (e.g. under memory pressure).
|
|
181
|
+
return consume(proxyFuncs, proxyFuncs => [
|
|
182
|
+
call(
|
|
183
|
+
ctx,
|
|
184
|
+
`(target, sym, proxyFuncs) => {
|
|
128
185
|
const rec = new Proxy(target, {
|
|
129
186
|
get(obj, key, receiver) {
|
|
130
187
|
return key === sym ? obj : Reflect.get(obj, key, receiver)
|
|
@@ -134,7 +191,7 @@ export function wrapHandle(
|
|
|
134
191
|
? value[sym] ?? value
|
|
135
192
|
: value;
|
|
136
193
|
const sync = proxyFuncs(1, receiver) ?? "vm";
|
|
137
|
-
if (sync === "host" || Reflect.set(obj, key, v
|
|
194
|
+
if (sync === "host" || Reflect.set(obj, key, v)) {
|
|
138
195
|
if (sync !== "vm") {
|
|
139
196
|
proxyFuncs(2, receiver, key, v);
|
|
140
197
|
}
|
|
@@ -150,16 +207,25 @@ export function wrapHandle(
|
|
|
150
207
|
}
|
|
151
208
|
return true;
|
|
152
209
|
},
|
|
210
|
+
defineProperty(obj, key, descriptor) {
|
|
211
|
+
const sync = proxyFuncs(1, rec) ?? "vm";
|
|
212
|
+
if (sync === "host" || Reflect.defineProperty(obj, key, descriptor)) {
|
|
213
|
+
if (sync !== "vm") {
|
|
214
|
+
proxyFuncs(4, rec, key, descriptor);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return true;
|
|
218
|
+
},
|
|
153
219
|
});
|
|
154
220
|
return rec;
|
|
155
221
|
}`,
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
222
|
+
undefined,
|
|
223
|
+
handle,
|
|
224
|
+
proxyKeySymbolHandle,
|
|
225
|
+
proxyFuncs,
|
|
226
|
+
) as Wrapped<QuickJSHandle>,
|
|
227
|
+
true,
|
|
228
|
+
]);
|
|
163
229
|
}
|
|
164
230
|
|
|
165
231
|
export function unwrap<T>(obj: T, key: string | symbol): T {
|
|
@@ -187,8 +253,9 @@ export function isHandleWrapped(
|
|
|
187
253
|
return !!ctx.dump(
|
|
188
254
|
call(
|
|
189
255
|
ctx,
|
|
190
|
-
//
|
|
191
|
-
|
|
256
|
+
// Built-ins that must not be wrapped (internal slots / non-property access)
|
|
257
|
+
// report as "wrapped" so wrapHandle leaves them alone.
|
|
258
|
+
`(a, s) => (a instanceof Promise) || (a instanceof Date) || (a instanceof ArrayBuffer) || (ArrayBuffer.isView(a)) || (a instanceof Map) || (a instanceof Set) || (typeof a === "object" && a !== null || typeof a === "function") && !!a[s]`,
|
|
192
259
|
undefined,
|
|
193
260
|
handle,
|
|
194
261
|
key,
|