@usebruno/js 0.48.0 → 0.49.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/package.json +4 -3
- package/src/bru.js +26 -3
- package/src/runtime/script-runtime.js +11 -10
- package/src/runtime/scripted-entries.js +16 -0
- package/src/runtime/test-runtime.js +6 -5
- package/src/sandbox/quickjs/index.js +22 -7
- package/src/sandbox/quickjs/shims/bru.js +16 -0
- package/src/sandbox/quickjs/utils/index.js +131 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usebruno/js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.49.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"files": [
|
|
@@ -9,16 +9,17 @@
|
|
|
9
9
|
],
|
|
10
10
|
"scripts": {
|
|
11
11
|
"test": "node --experimental-vm-modules $(npx which jest) --testPathIgnorePatterns test.js",
|
|
12
|
+
"test:ci": "node --experimental-vm-modules ../../node_modules/jest/bin/jest.js --testPathIgnorePatterns test.js",
|
|
12
13
|
"sandbox:bundle-libraries": "node ./src/sandbox/bundle-libraries.js",
|
|
13
14
|
"prepack": "npm run test"
|
|
14
15
|
},
|
|
15
16
|
"dependencies": {
|
|
16
|
-
"@usebruno/common": "0.
|
|
17
|
+
"@usebruno/common": "0.23.0",
|
|
17
18
|
"@usebruno/query": "0.2.2",
|
|
18
19
|
"ajv": "^8.12.0",
|
|
19
20
|
"ajv-formats": "^2.1.1",
|
|
20
21
|
"atob": "^2.1.2",
|
|
21
|
-
"axios": "1.
|
|
22
|
+
"axios": "1.16.0",
|
|
22
23
|
"btoa": "^1.2.1",
|
|
23
24
|
"chai": "^4.3.7",
|
|
24
25
|
"chai-string": "^1.5.0",
|
package/src/bru.js
CHANGED
|
@@ -2,7 +2,7 @@ const { cloneDeep } = require('lodash');
|
|
|
2
2
|
const { uuid } = require('./utils');
|
|
3
3
|
const xmlFormat = require('xml-formatter');
|
|
4
4
|
const { interpolate: _interpolate } = require('@usebruno/common');
|
|
5
|
-
const {
|
|
5
|
+
const { createSendRequest } = require('@usebruno/requests').scripting;
|
|
6
6
|
const { jar: createCookieJar, getCookiesForUrl } = require('@usebruno/requests').cookies;
|
|
7
7
|
const CookieList = require('./cookie-list');
|
|
8
8
|
const Handlebars = require('handlebars');
|
|
@@ -102,8 +102,17 @@ class Bru {
|
|
|
102
102
|
this.setVisualizations = setVisualizations;
|
|
103
103
|
this.onConsoleLog = onConsoleLog || null;
|
|
104
104
|
this.collectionName = collectionName;
|
|
105
|
-
//
|
|
106
|
-
this.
|
|
105
|
+
// Set by the host-side __bruSetScope global at the top of each segment's IIFE.
|
|
106
|
+
this._currentScope = null;
|
|
107
|
+
this.scriptedRequestEntries = [];
|
|
108
|
+
this.sendRequest = (...args) => {
|
|
109
|
+
const scopeSnapshot = this._currentScope ? { ...this._currentScope } : null;
|
|
110
|
+
const send = createSendRequest(certsAndProxyConfig, {
|
|
111
|
+
onComplete: (entry) =>
|
|
112
|
+
this._recordScriptedRequest({ source: 'sendRequest', scope: scopeSnapshot, ...entry })
|
|
113
|
+
});
|
|
114
|
+
return send(...args);
|
|
115
|
+
};
|
|
107
116
|
this.runtime = runtime;
|
|
108
117
|
this.requestUrl = requestUrl;
|
|
109
118
|
this.cookies = new CookieList({
|
|
@@ -205,6 +214,16 @@ class Bru {
|
|
|
205
214
|
return this.collectionPath;
|
|
206
215
|
}
|
|
207
216
|
|
|
217
|
+
_recordScriptedRequest(entry) {
|
|
218
|
+
// Prefer scope passed in by the caller (snapshot at call time). Fall back to
|
|
219
|
+
// _currentScope for callers that don't supply one (e.g. bru.runRequest).
|
|
220
|
+
const { scope: providedScope, ...rest } = entry;
|
|
221
|
+
const scope = providedScope !== undefined
|
|
222
|
+
? providedScope
|
|
223
|
+
: (this._currentScope ? { ...this._currentScope } : null);
|
|
224
|
+
this.scriptedRequestEntries.push({ ...rest, scope });
|
|
225
|
+
}
|
|
226
|
+
|
|
208
227
|
getEnvName() {
|
|
209
228
|
return this.envVariables.__name__;
|
|
210
229
|
}
|
|
@@ -270,6 +289,10 @@ class Bru {
|
|
|
270
289
|
}
|
|
271
290
|
}
|
|
272
291
|
|
|
292
|
+
hasGlobalEnvVar(key) {
|
|
293
|
+
return Object.hasOwn(this.globalEnvironmentVariables, key);
|
|
294
|
+
}
|
|
295
|
+
|
|
273
296
|
getGlobalEnvVar(key) {
|
|
274
297
|
return this.interpolate(this.globalEnvironmentVariables[key]);
|
|
275
298
|
}
|
|
@@ -8,6 +8,7 @@ const { createBruTestResultMethods } = require('../utils/results');
|
|
|
8
8
|
const { runScriptInNodeVm } = require('../sandbox/node-vm');
|
|
9
9
|
const { executeQuickJsVmAsync } = require('../sandbox/quickjs');
|
|
10
10
|
const { SANDBOX } = require('../utils/sandbox');
|
|
11
|
+
const { bindRunRequest, createScopeSetter } = require('./scripted-entries');
|
|
11
12
|
|
|
12
13
|
class ScriptRuntime {
|
|
13
14
|
constructor(props) {
|
|
@@ -80,7 +81,8 @@ class ScriptRuntime {
|
|
|
80
81
|
test,
|
|
81
82
|
expect: chai.expect,
|
|
82
83
|
assert: chai.assert,
|
|
83
|
-
__brunoTestResults: __brunoTestResults
|
|
84
|
+
__brunoTestResults: __brunoTestResults,
|
|
85
|
+
__bruSetScope: createScopeSetter(bru)
|
|
84
86
|
};
|
|
85
87
|
|
|
86
88
|
if (onConsoleLog && typeof onConsoleLog === 'function') {
|
|
@@ -98,9 +100,7 @@ class ScriptRuntime {
|
|
|
98
100
|
};
|
|
99
101
|
}
|
|
100
102
|
|
|
101
|
-
|
|
102
|
-
context.bru.runRequest = runRequestByItemPathname;
|
|
103
|
-
}
|
|
103
|
+
bindRunRequest(bru, runRequestByItemPathname);
|
|
104
104
|
|
|
105
105
|
// Helper to build the result object for pre-request scripts
|
|
106
106
|
// Extracted to avoid duplication across runtime branches
|
|
@@ -115,7 +115,8 @@ class ScriptRuntime {
|
|
|
115
115
|
results: cleanJson(__brunoTestResults.getResults()),
|
|
116
116
|
nextRequestName: bru.nextRequest,
|
|
117
117
|
skipRequest: bru.skipRequest,
|
|
118
|
-
stopExecution: bru.stopExecution
|
|
118
|
+
stopExecution: bru.stopExecution,
|
|
119
|
+
scriptedRequestEntries: cleanJson(bru.scriptedRequestEntries || [])
|
|
119
120
|
});
|
|
120
121
|
|
|
121
122
|
// Track script errors to attach partial results before re-throwing
|
|
@@ -233,7 +234,8 @@ class ScriptRuntime {
|
|
|
233
234
|
test,
|
|
234
235
|
expect: chai.expect,
|
|
235
236
|
assert: chai.assert,
|
|
236
|
-
__brunoTestResults: __brunoTestResults
|
|
237
|
+
__brunoTestResults: __brunoTestResults,
|
|
238
|
+
__bruSetScope: createScopeSetter(bru)
|
|
237
239
|
};
|
|
238
240
|
|
|
239
241
|
if (onConsoleLog && typeof onConsoleLog === 'function') {
|
|
@@ -251,9 +253,7 @@ class ScriptRuntime {
|
|
|
251
253
|
};
|
|
252
254
|
}
|
|
253
255
|
|
|
254
|
-
|
|
255
|
-
context.bru.runRequest = runRequestByItemPathname;
|
|
256
|
-
}
|
|
256
|
+
bindRunRequest(bru, runRequestByItemPathname);
|
|
257
257
|
|
|
258
258
|
// Helper to build the result object for post-response scripts
|
|
259
259
|
// Extracted to avoid duplication across runtime branches
|
|
@@ -268,7 +268,8 @@ class ScriptRuntime {
|
|
|
268
268
|
results: cleanJson(__brunoTestResults.getResults()),
|
|
269
269
|
nextRequestName: bru.nextRequest,
|
|
270
270
|
skipRequest: bru.skipRequest,
|
|
271
|
-
stopExecution: bru.stopExecution
|
|
271
|
+
stopExecution: bru.stopExecution,
|
|
272
|
+
scriptedRequestEntries: cleanJson(bru.scriptedRequestEntries || [])
|
|
272
273
|
});
|
|
273
274
|
|
|
274
275
|
// Track script errors to attach partial results before re-throwing
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Forwards the caller's bru as a second arg so the host can attribute the call.
|
|
2
|
+
const bindRunRequest = (bru, runRequestByItemPathname) => {
|
|
3
|
+
if (!runRequestByItemPathname) return;
|
|
4
|
+
bru.runRequest = (relativePathname) =>
|
|
5
|
+
runRequestByItemPathname(relativePathname, bru);
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
// Kept off bru to stay out of user-facing autocomplete.
|
|
9
|
+
const createScopeSetter = (bru) => (scope) => {
|
|
10
|
+
bru._currentScope = scope || null;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
module.exports = {
|
|
14
|
+
bindRunRequest,
|
|
15
|
+
createScopeSetter
|
|
16
|
+
};
|
|
@@ -9,6 +9,7 @@ const { runScriptInNodeVm } = require('../sandbox/node-vm');
|
|
|
9
9
|
const jsonwebtoken = require('jsonwebtoken');
|
|
10
10
|
const { executeQuickJsVmAsync } = require('../sandbox/quickjs');
|
|
11
11
|
const { SANDBOX } = require('../utils/sandbox');
|
|
12
|
+
const { bindRunRequest, createScopeSetter } = require('./scripted-entries');
|
|
12
13
|
|
|
13
14
|
class TestRuntime {
|
|
14
15
|
constructor(props) {
|
|
@@ -84,7 +85,8 @@ class TestRuntime {
|
|
|
84
85
|
expect: chai.expect,
|
|
85
86
|
assert: chai.assert,
|
|
86
87
|
__brunoTestResults: __brunoTestResults,
|
|
87
|
-
jwt: jsonwebtoken
|
|
88
|
+
jwt: jsonwebtoken,
|
|
89
|
+
__bruSetScope: createScopeSetter(bru)
|
|
88
90
|
};
|
|
89
91
|
|
|
90
92
|
if (onConsoleLog && typeof onConsoleLog === 'function') {
|
|
@@ -102,9 +104,7 @@ class TestRuntime {
|
|
|
102
104
|
};
|
|
103
105
|
}
|
|
104
106
|
|
|
105
|
-
|
|
106
|
-
context.bru.runRequest = runRequestByItemPathname;
|
|
107
|
-
}
|
|
107
|
+
bindRunRequest(bru, runRequestByItemPathname);
|
|
108
108
|
|
|
109
109
|
let scriptError = null;
|
|
110
110
|
|
|
@@ -147,7 +147,8 @@ class TestRuntime {
|
|
|
147
147
|
persistentEnvVariables: cleanJson(bru.persistentEnvVariables),
|
|
148
148
|
oauth2CredentialsToReset: bru.oauth2CredentialsToReset,
|
|
149
149
|
results: cleanJson(__brunoTestResults.getResults()),
|
|
150
|
-
nextRequestName: bru.nextRequest
|
|
150
|
+
nextRequestName: bru.nextRequest,
|
|
151
|
+
scriptedRequestEntries: cleanJson(bru.scriptedRequestEntries || [])
|
|
151
152
|
};
|
|
152
153
|
|
|
153
154
|
if (scriptError) {
|
|
@@ -11,7 +11,7 @@ const { newQuickJSWASMModule, memoizePromiseFactory } = require('quickjs-emscrip
|
|
|
11
11
|
// execute `npm run sandbox:bundle-libraries` if the below file doesn't exist
|
|
12
12
|
const getBundledCode = require('../bundle-browser-rollup');
|
|
13
13
|
const addPathShimToContext = require('./shims/lib/path');
|
|
14
|
-
const { marshallToVm } = require('./utils');
|
|
14
|
+
const { marshallToVm, createManagedQuickJsContext } = require('./utils');
|
|
15
15
|
const addCryptoUtilsShimToContext = require('./shims/lib/crypto-utils');
|
|
16
16
|
const { wrapScriptInClosure, SANDBOX } = require('../../utils/sandbox');
|
|
17
17
|
|
|
@@ -56,9 +56,10 @@ const executeQuickJsVm = ({ script: externalScript, context: externalContext, sc
|
|
|
56
56
|
|
|
57
57
|
externalScript = removeQuotes(externalScript);
|
|
58
58
|
}
|
|
59
|
-
|
|
59
|
+
let managedQuickJsContext;
|
|
60
60
|
try {
|
|
61
|
-
|
|
61
|
+
managedQuickJsContext = createManagedQuickJsContext(QuickJSModule);
|
|
62
|
+
const vm = managedQuickJsContext.vm;
|
|
62
63
|
const { bru, req, res, ...variables } = externalContext;
|
|
63
64
|
|
|
64
65
|
bru && addBruShimToContext(vm, bru);
|
|
@@ -74,7 +75,7 @@ const executeQuickJsVm = ({ script: externalScript, context: externalContext, sc
|
|
|
74
75
|
|
|
75
76
|
let scriptText = scriptType === 'template-literal' ? templateLiteralText : jsExpressionText;
|
|
76
77
|
|
|
77
|
-
const result = vm.
|
|
78
|
+
const result = vm.evalCodeRetained(scriptText);
|
|
78
79
|
if (result.error) {
|
|
79
80
|
let e = vm.dump(result.error);
|
|
80
81
|
result.error.dispose();
|
|
@@ -86,6 +87,8 @@ const executeQuickJsVm = ({ script: externalScript, context: externalContext, sc
|
|
|
86
87
|
}
|
|
87
88
|
} catch (error) {
|
|
88
89
|
console.error('Error executing the script!', error);
|
|
90
|
+
} finally {
|
|
91
|
+
managedQuickJsContext?.dispose();
|
|
89
92
|
}
|
|
90
93
|
};
|
|
91
94
|
|
|
@@ -95,9 +98,11 @@ const executeQuickJsVmAsync = async ({ script: externalScript, context: external
|
|
|
95
98
|
}
|
|
96
99
|
externalScript = externalScript?.trim();
|
|
97
100
|
|
|
101
|
+
let managedQuickJsContext;
|
|
98
102
|
try {
|
|
99
103
|
const module = await loader();
|
|
100
|
-
|
|
104
|
+
managedQuickJsContext = createManagedQuickJsContext(module);
|
|
105
|
+
const vm = managedQuickJsContext.vm;
|
|
101
106
|
|
|
102
107
|
// add crypto utilities required by the crypto-js library in bundledCode
|
|
103
108
|
await addCryptoUtilsShimToContext(vm);
|
|
@@ -126,17 +131,27 @@ const executeQuickJsVmAsync = async ({ script: externalScript, context: external
|
|
|
126
131
|
|
|
127
132
|
const script = wrapScriptInClosure(externalScript, SANDBOX.QUICKJS);
|
|
128
133
|
|
|
129
|
-
const result = vm.
|
|
134
|
+
const result = vm.evalCodeRetained(script, scriptPath);
|
|
130
135
|
const promiseHandle = vm.unwrapResult(result);
|
|
131
136
|
const resolvedResult = await vm.resolvePromise(promiseHandle);
|
|
132
137
|
promiseHandle.dispose();
|
|
133
138
|
const resolvedHandle = vm.unwrapResult(resolvedResult);
|
|
134
139
|
resolvedHandle.dispose();
|
|
135
|
-
// vm.dispose();
|
|
136
140
|
return;
|
|
137
141
|
} catch (error) {
|
|
138
142
|
error.__isQuickJS = true;
|
|
139
143
|
throw error;
|
|
144
|
+
} finally {
|
|
145
|
+
// Wait for any in-flight async work (sendRequest, axios, cookie jar, timers,
|
|
146
|
+
// un-awaited promises) to settle before tearing down the VM. Disposing while
|
|
147
|
+
// a deferred is still pending lets its later host callback touch a freed
|
|
148
|
+
// context, throwing `QuickJSUseAfterFree`.
|
|
149
|
+
try {
|
|
150
|
+
await managedQuickJsContext?.waitForPendingDeferreds?.();
|
|
151
|
+
managedQuickJsContext?.dispose();
|
|
152
|
+
} catch (teardownError) {
|
|
153
|
+
throw teardownError;
|
|
154
|
+
}
|
|
140
155
|
}
|
|
141
156
|
};
|
|
142
157
|
|
|
@@ -117,6 +117,12 @@ const addBruShimToContext = (vm, bru) => {
|
|
|
117
117
|
vm.setProp(bruObject, 'getAllGlobalEnvVars', getAllGlobalEnvVars);
|
|
118
118
|
getAllGlobalEnvVars.dispose();
|
|
119
119
|
|
|
120
|
+
let hasGlobalEnvVar = vm.newFunction('hasGlobalEnvVar', function (key) {
|
|
121
|
+
return marshallToVm(bru.hasGlobalEnvVar(vm.dump(key)), vm);
|
|
122
|
+
});
|
|
123
|
+
vm.setProp(bruObject, 'hasGlobalEnvVar', hasGlobalEnvVar);
|
|
124
|
+
hasGlobalEnvVar.dispose();
|
|
125
|
+
|
|
120
126
|
// TODO: deleteAllGlobalEnvVars works in the request lifecycle but does not update the UI.
|
|
121
127
|
// Re-enable once the UI sync issue is resolved.
|
|
122
128
|
// let deleteAllGlobalEnvVars = vm.newFunction('deleteAllGlobalEnvVars', function () {
|
|
@@ -401,10 +407,20 @@ const addBruShimToContext = (vm, bru) => {
|
|
|
401
407
|
});
|
|
402
408
|
sendRequestHandle.consume((handle) => vm.setProp(bruObject, '_sendRequest', handle));
|
|
403
409
|
|
|
410
|
+
// On vm.global, not bru, to stay off user-facing autocomplete.
|
|
411
|
+
let setScopeHandle = vm.newFunction('__bruSetScope', (scopeArg) => {
|
|
412
|
+
bru._currentScope = vm.dump(scopeArg) || null;
|
|
413
|
+
});
|
|
414
|
+
setScopeHandle.consume((handle) => vm.setProp(vm.global, '__bruSetScope', handle));
|
|
415
|
+
|
|
404
416
|
const sleep = vm.newFunction('sleep', (timer) => {
|
|
405
417
|
const t = vm.getString(timer);
|
|
406
418
|
const promise = vm.newPromise();
|
|
407
419
|
setTimeout(() => {
|
|
420
|
+
// The VM may have been disposed while this native timer was pending
|
|
421
|
+
// (e.g. a setTimeout/sleep whose promise the script never awaited).
|
|
422
|
+
// Touching the VM after teardown throws QuickJSUseAfterFree, so bail out.
|
|
423
|
+
if (!vm.alive) return;
|
|
408
424
|
promise.resolve(vm.newString('slept'));
|
|
409
425
|
}, t);
|
|
410
426
|
promise.settled.then(vm.runtime.executePendingJobs);
|
|
@@ -1,3 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a QuickJS context with centralized lifecycle management:
|
|
3
|
+
* - vm.evalCode() auto-disposes result handles (for shim setup code)
|
|
4
|
+
* - vm.evalCodeRetained() returns the raw result (for user script execution)
|
|
5
|
+
* - all newObject/newFunction/newArray handles are tracked and disposed on teardown
|
|
6
|
+
*/
|
|
7
|
+
const createManagedQuickJsContext = (module) => {
|
|
8
|
+
const vm = module.newContext();
|
|
9
|
+
const disposeTracked = trackQuickJsContext(vm);
|
|
10
|
+
const evalCodeRetained = vm.evalCode.bind(vm);
|
|
11
|
+
const waitForPendingDeferreds = trackPendingDeferreds(vm);
|
|
12
|
+
|
|
13
|
+
vm.evalCode = (code, filename = 'eval.js') => {
|
|
14
|
+
const result = evalCodeRetained(code, filename);
|
|
15
|
+
if (result.error) {
|
|
16
|
+
const error = vm.dump(result.error);
|
|
17
|
+
result.error.dispose();
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
result.value.dispose();
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
vm.evalCodeRetained = evalCodeRetained;
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
vm,
|
|
27
|
+
waitForPendingDeferreds,
|
|
28
|
+
dispose: () => disposeQuickJsContext(vm, disposeTracked)
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Track every deferred created by the async shims (sendRequest, axios, cookie
|
|
34
|
+
* jar, sleep, ...) so teardown can wait for them to settle. A user script that
|
|
35
|
+
* fires-and-forgets async work (e.g. an un-awaited setTimeout) resolves the
|
|
36
|
+
* wrapping closure immediately; without this, the VM is disposed before the
|
|
37
|
+
* deferred's host callback runs, and touching the freed context throws
|
|
38
|
+
* `QuickJSUseAfterFree`. Each `.settled` resolves once the deferred is
|
|
39
|
+
* resolved/rejected, so awaiting them keeps the context alive long enough.
|
|
40
|
+
*
|
|
41
|
+
* The hook is installed now (at context creation) so it captures promises as
|
|
42
|
+
* the script runs. Returns a function that drains the captured deferreds at
|
|
43
|
+
* teardown; new deferreds can be created while we wait (a chained timer), so it
|
|
44
|
+
* drains in place until none remain.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
const trackPendingDeferreds = (vm) => {
|
|
48
|
+
const pendingDeferreds = [];
|
|
49
|
+
const originalNewPromise = vm.newPromise.bind(vm);
|
|
50
|
+
vm.newPromise = (...args) => {
|
|
51
|
+
const deferred = originalNewPromise(...args);
|
|
52
|
+
pendingDeferreds.push(deferred.settled.catch(() => { }));
|
|
53
|
+
return deferred;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return async () => {
|
|
57
|
+
while (pendingDeferreds.length) {
|
|
58
|
+
const batch = pendingDeferreds.splice(0);
|
|
59
|
+
await Promise.all(batch);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Tracks handles created via newObject/newFunction/newArray so they can all be
|
|
66
|
+
* disposed before the context. quickjs-emscripten requires every heap handle to
|
|
67
|
+
* be disposed individually; shims attach then drop their ref via .dispose().
|
|
68
|
+
*/
|
|
69
|
+
const trackQuickJsContext = (vm) => {
|
|
70
|
+
const handles = [];
|
|
71
|
+
|
|
72
|
+
const track = (handle) => {
|
|
73
|
+
handles.push(handle);
|
|
74
|
+
return handle;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Replace an allocator with a wrapper that records every handle it returns,
|
|
78
|
+
// so teardown can dispose them all. Behaviour is otherwise identical.
|
|
79
|
+
const trackAllocations = (method) => {
|
|
80
|
+
const original = vm[method]?.bind(vm);
|
|
81
|
+
if (!original) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
vm[method] = (...args) => track(original(...args));
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
['newObject', 'newFunction', 'newArray'].forEach(trackAllocations);
|
|
89
|
+
|
|
90
|
+
// Dispose newest-first: later handles may reference earlier ones.
|
|
91
|
+
return () => {
|
|
92
|
+
for (const handle of handles.reverse()) {
|
|
93
|
+
if (handle?.alive) {
|
|
94
|
+
handle.dispose();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Clears shim globals, drains pending QuickJS jobs, and disposes the context.
|
|
102
|
+
* Pass disposeTracked from trackQuickJsContext() to free shim handles first.
|
|
103
|
+
*/
|
|
104
|
+
const disposeQuickJsContext = (vm, disposeTracked) => {
|
|
105
|
+
if (!vm?.alive) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (typeof disposeTracked === 'function') {
|
|
110
|
+
disposeTracked();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Drain the runtime's pending job queue (resolved/rejected promise callbacks)
|
|
114
|
+
// before disposing. Executing a job can schedule more jobs (chained `.then()`s),
|
|
115
|
+
// so we keep going until `hasPendingJob()` reports the queue is empty or a job
|
|
116
|
+
// throws.
|
|
117
|
+
while (vm.runtime?.hasPendingJob?.()) {
|
|
118
|
+
const result = vm.runtime.executePendingJobs();
|
|
119
|
+
// On error, dispose the error handle and stop draining.
|
|
120
|
+
if (result.error) {
|
|
121
|
+
result.error.dispose();
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
vm.dispose();
|
|
126
|
+
};
|
|
127
|
+
|
|
1
128
|
const marshallToVm = (value, vm) => {
|
|
2
129
|
if (value === undefined) {
|
|
3
130
|
return vm.undefined;
|
|
@@ -79,5 +206,8 @@ async function invokeFunction(vm, quickFn, args = []) {
|
|
|
79
206
|
|
|
80
207
|
module.exports = {
|
|
81
208
|
marshallToVm,
|
|
82
|
-
invokeFunction
|
|
209
|
+
invokeFunction,
|
|
210
|
+
createManagedQuickJsContext,
|
|
211
|
+
disposeQuickJsContext,
|
|
212
|
+
trackQuickJsContext
|
|
83
213
|
};
|