@usebruno/js 0.51.0 → 0.52.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 +6 -6
- package/src/bru.js +15 -23
- package/src/bruno-request.js +4 -0
- package/src/grpc/bruno-grpc-request.js +52 -0
- package/src/grpc/bruno-grpc-response.js +43 -0
- package/src/grpc/grpc-message-list.js +74 -0
- package/src/grpc/grpc-message.js +20 -0
- package/src/grpc/grpc-metadata-list.js +214 -0
- package/src/grpc/grpc-metadata.js +21 -0
- package/src/grpc/grpc-script-runtime.js +302 -0
- package/src/index.js +2 -0
- package/src/interpolate-string.js +2 -1
- package/src/runtime/assert-runtime.js +1 -0
- package/src/runtime/script-runtime.js +18 -2
- package/src/runtime/test-runtime.js +12 -7
- package/src/sandbox/bundle-browser-rollup.js +32 -32
- package/src/sandbox/node-vm/cjs-loader.js +390 -39
- package/src/sandbox/node-vm/index.js +96 -65
- package/src/sandbox/node-vm/index.spec.js +616 -0
- package/src/sandbox/quickjs/index.js +101 -17
- package/src/sandbox/quickjs/shims/bruno-grpc.js +29 -0
- package/src/sandbox/quickjs/shims/grpc/bruno-grpc-request.js +47 -0
- package/src/sandbox/quickjs/shims/grpc/bruno-grpc-response.js +40 -0
- package/src/sandbox/quickjs/shims/grpc/grpc-message-list.js +29 -0
- package/src/sandbox/quickjs/shims/grpc/grpc-metadata-list.js +31 -0
- package/src/sandbox/quickjs/utils/index.js +42 -23
- package/src/utils/error-formatter.js +13 -9
- package/src/utils/error-formatter.spec.js +97 -2
- package/src/utils/results.js +30 -3
|
@@ -2,11 +2,28 @@ const addBruShimToContext = require('./shims/bru');
|
|
|
2
2
|
const addBrunoRequestShimToContext = require('./shims/bruno-request');
|
|
3
3
|
const addConsoleShimToContext = require('./shims/console');
|
|
4
4
|
const addBrunoResponseShimToContext = require('./shims/bruno-response');
|
|
5
|
+
const addBrunoGrpcShimToContext = require('./shims/bruno-grpc');
|
|
5
6
|
const addTestShimToContext = require('./shims/test');
|
|
6
7
|
const addLibraryShimsToContext = require('./shims/lib');
|
|
7
8
|
const addLocalModuleLoaderShimToContext = require('./shims/local-module');
|
|
8
9
|
const { getRequireCode } = require('./shims/require');
|
|
9
|
-
const {
|
|
10
|
+
const { newQuickJSWASMModuleFromVariant, newVariant, RELEASE_SYNC } = require('quickjs-emscripten');
|
|
11
|
+
|
|
12
|
+
// The engine prints its dispose-abort assertion to stderr on its own. Swallow
|
|
13
|
+
// that line on the CLI, where stderr is the user's screen and a handled trap
|
|
14
|
+
// would read as a crash; keep it in the app, whose console is not user facing.
|
|
15
|
+
const isElectronHost = Boolean(process.versions.electron);
|
|
16
|
+
const isContainedAbortLine = (line) =>
|
|
17
|
+
String(line).includes('list_empty(&rt->gc_obj_list)') && String(line).includes('JS_FreeRuntime');
|
|
18
|
+
const quietEngineVariant = newVariant(RELEASE_SYNC, {
|
|
19
|
+
emscriptenModule: {
|
|
20
|
+
printErr: (line) => {
|
|
21
|
+
if (isElectronHost || !isContainedAbortLine(line)) {
|
|
22
|
+
console.error(line);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
});
|
|
10
27
|
|
|
11
28
|
// execute `npm run sandbox:bundle-libraries` if the below file doesn't exist
|
|
12
29
|
const getBundledCode = require('../bundle-browser-rollup');
|
|
@@ -16,8 +33,54 @@ const addCryptoUtilsShimToContext = require('./shims/lib/crypto-utils');
|
|
|
16
33
|
const { wrapScriptInClosure, SANDBOX } = require('../../utils/sandbox');
|
|
17
34
|
|
|
18
35
|
let QuickJSModule;
|
|
19
|
-
|
|
20
|
-
|
|
36
|
+
let quickJSModulePromise;
|
|
37
|
+
let quickJSModuleLoading = false;
|
|
38
|
+
let quickJSModuleRecycleCount = 0;
|
|
39
|
+
|
|
40
|
+
// Memoized WASM module for sync + async. reload swaps with no gap; failed
|
|
41
|
+
// reload restores the old memo, failed initial load clears it.
|
|
42
|
+
const loader = ({ reload = false } = {}) => {
|
|
43
|
+
if (!quickJSModulePromise || (reload && !quickJSModuleLoading)) {
|
|
44
|
+
const previousPromise = quickJSModulePromise;
|
|
45
|
+
quickJSModuleLoading = true;
|
|
46
|
+
quickJSModulePromise = newQuickJSWASMModuleFromVariant(quietEngineVariant)
|
|
47
|
+
.then((mod) => {
|
|
48
|
+
QuickJSModule = mod;
|
|
49
|
+
return mod;
|
|
50
|
+
})
|
|
51
|
+
.catch((loadError) => {
|
|
52
|
+
console.error(reload ? 'QuickJS module reload failed' : 'QuickJS module load failed', loadError);
|
|
53
|
+
quickJSModulePromise = reload ? previousPromise : null;
|
|
54
|
+
if (quickJSModulePromise) {
|
|
55
|
+
return quickJSModulePromise;
|
|
56
|
+
}
|
|
57
|
+
throw loadError;
|
|
58
|
+
})
|
|
59
|
+
.finally(() => {
|
|
60
|
+
quickJSModuleLoading = false;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return quickJSModulePromise;
|
|
64
|
+
};
|
|
65
|
+
loader().catch(() => {});
|
|
66
|
+
|
|
67
|
+
// On dispose WASM trap, recycle the module (old one keeps serving until ready).
|
|
68
|
+
const recycleQuickJSModuleOnAbort = (teardownError, ownerModule) => {
|
|
69
|
+
if (!(teardownError instanceof WebAssembly.RuntimeError)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
// Skip if this module was already replaced.
|
|
73
|
+
if (!ownerModule || ownerModule === QuickJSModule) {
|
|
74
|
+
quickJSModuleRecycleCount += 1;
|
|
75
|
+
console.warn(
|
|
76
|
+
quickJSModuleRecycleCount === 1
|
|
77
|
+
? 'QuickJS engine crashed during cleanup and was replaced; the run was not affected'
|
|
78
|
+
: `QuickJS engine replaced again (${quickJSModuleRecycleCount} this session)`
|
|
79
|
+
);
|
|
80
|
+
loader({ reload: true }).catch(() => {});
|
|
81
|
+
}
|
|
82
|
+
return true;
|
|
83
|
+
};
|
|
21
84
|
|
|
22
85
|
const toNumber = (value) => {
|
|
23
86
|
const num = Number(value);
|
|
@@ -57,8 +120,9 @@ const executeQuickJsVm = ({ script: externalScript, context: externalContext, sc
|
|
|
57
120
|
externalScript = removeQuotes(externalScript);
|
|
58
121
|
}
|
|
59
122
|
let managedQuickJsContext;
|
|
123
|
+
const quickJsModule = QuickJSModule;
|
|
60
124
|
try {
|
|
61
|
-
managedQuickJsContext = createManagedQuickJsContext(
|
|
125
|
+
managedQuickJsContext = createManagedQuickJsContext(quickJsModule);
|
|
62
126
|
const vm = managedQuickJsContext.vm;
|
|
63
127
|
const { bru, req, res, ...variables } = externalContext;
|
|
64
128
|
|
|
@@ -88,7 +152,13 @@ const executeQuickJsVm = ({ script: externalScript, context: externalContext, sc
|
|
|
88
152
|
} catch (error) {
|
|
89
153
|
console.error('Error executing the script!', error);
|
|
90
154
|
} finally {
|
|
91
|
-
|
|
155
|
+
try {
|
|
156
|
+
managedQuickJsContext?.dispose();
|
|
157
|
+
} catch (teardownError) {
|
|
158
|
+
if (!recycleQuickJSModuleOnAbort(teardownError, quickJsModule)) {
|
|
159
|
+
console.error('Error disposing QuickJS context', teardownError);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
92
162
|
}
|
|
93
163
|
};
|
|
94
164
|
|
|
@@ -99,9 +169,11 @@ const executeQuickJsVmAsync = async ({ script: externalScript, context: external
|
|
|
99
169
|
externalScript = externalScript?.trim();
|
|
100
170
|
|
|
101
171
|
let managedQuickJsContext;
|
|
172
|
+
let scriptError;
|
|
173
|
+
let quickJsModule;
|
|
102
174
|
try {
|
|
103
|
-
|
|
104
|
-
managedQuickJsContext = createManagedQuickJsContext(
|
|
175
|
+
quickJsModule = await loader();
|
|
176
|
+
managedQuickJsContext = createManagedQuickJsContext(quickJsModule);
|
|
105
177
|
const vm = managedQuickJsContext.vm;
|
|
106
178
|
|
|
107
179
|
// add crypto utilities required by the crypto-js library in bundledCode
|
|
@@ -120,6 +192,7 @@ const executeQuickJsVmAsync = async ({ script: externalScript, context: external
|
|
|
120
192
|
|
|
121
193
|
consoleFn && addConsoleShimToContext(vm, consoleFn);
|
|
122
194
|
bru && addBruShimToContext(vm, bru);
|
|
195
|
+
bru?.grpc && addBrunoGrpcShimToContext(vm, bru.grpc);
|
|
123
196
|
req && addBrunoRequestShimToContext(vm, req);
|
|
124
197
|
res && addBrunoResponseShimToContext(vm, res);
|
|
125
198
|
addLocalModuleLoaderShimToContext(vm, collectionPath);
|
|
@@ -137,22 +210,33 @@ const executeQuickJsVmAsync = async ({ script: externalScript, context: external
|
|
|
137
210
|
promiseHandle.dispose();
|
|
138
211
|
const resolvedHandle = vm.unwrapResult(resolvedResult);
|
|
139
212
|
resolvedHandle.dispose();
|
|
140
|
-
return;
|
|
141
213
|
} catch (error) {
|
|
142
214
|
error.__isQuickJS = true;
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
215
|
+
scriptError = error;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// The run waits for every pending deferred before returning: un-awaited
|
|
219
|
+
// async work is the user's choice, and the run is not done until it is.
|
|
220
|
+
// The wait is unbounded by design; cancelling the request is the way out.
|
|
221
|
+
if (managedQuickJsContext) {
|
|
222
|
+
// No try/catch: every awaited settle promise is pre-caught at creation
|
|
223
|
+
// (trackPendingDeferreds), so this await cannot reject and skip dispose.
|
|
224
|
+
await managedQuickJsContext.waitForPendingDeferreds();
|
|
149
225
|
try {
|
|
150
|
-
|
|
151
|
-
managedQuickJsContext?.dispose();
|
|
226
|
+
managedQuickJsContext.dispose();
|
|
152
227
|
} catch (teardownError) {
|
|
153
|
-
|
|
228
|
+
const recycled = recycleQuickJSModuleOnAbort(teardownError, quickJsModule);
|
|
229
|
+
if (!scriptError && !recycled) {
|
|
230
|
+
scriptError = teardownError;
|
|
231
|
+
} else if (!recycled) {
|
|
232
|
+
console.error('Error disposing QuickJS context', teardownError);
|
|
233
|
+
}
|
|
154
234
|
}
|
|
155
235
|
}
|
|
236
|
+
|
|
237
|
+
if (scriptError) {
|
|
238
|
+
throw scriptError;
|
|
239
|
+
}
|
|
156
240
|
};
|
|
157
241
|
|
|
158
242
|
module.exports = {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const addBrunoGrpcRequestShimToContext = require('./grpc/bruno-grpc-request');
|
|
2
|
+
const addBrunoGrpcResponseShimToContext = require('./grpc/bruno-grpc-response');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Installs `bru.grpc` onto the `bru` object the bru shim has already put on the global, so it has
|
|
6
|
+
* to run after it.
|
|
7
|
+
*/
|
|
8
|
+
const addBrunoGrpcShimToContext = (vm, grpc) => {
|
|
9
|
+
const bruObject = vm.getProp(vm.global, 'bru');
|
|
10
|
+
const grpcObject = vm.newObject();
|
|
11
|
+
|
|
12
|
+
const { evalCode: requestEvalCode } = addBrunoGrpcRequestShimToContext(vm, grpc.request, grpcObject);
|
|
13
|
+
// `response` is absent in `beforeCallStart`, which has no response yet.
|
|
14
|
+
const { evalCode: responseEvalCode } = grpc.response
|
|
15
|
+
? addBrunoGrpcResponseShimToContext(vm, grpc.response, grpcObject)
|
|
16
|
+
: { evalCode: [] };
|
|
17
|
+
|
|
18
|
+
vm.setProp(bruObject, 'grpc', grpcObject);
|
|
19
|
+
grpcObject.dispose();
|
|
20
|
+
bruObject.dispose();
|
|
21
|
+
|
|
22
|
+
// The list code reaches its target through `globalThis.bru.grpc`, so it can only run now that
|
|
23
|
+
// `grpc` is on `bru`. Each block is braced on its own — every bridge declares the same consts.
|
|
24
|
+
for (const code of [...requestEvalCode, ...responseEvalCode].filter(Boolean)) {
|
|
25
|
+
vm.evalCode(`{ ${code} }`);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
module.exports = addBrunoGrpcShimToContext;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const { marshallToVm } = require('../../utils');
|
|
2
|
+
const addGrpcMetadataListShimToContext = require('./grpc-metadata-list');
|
|
3
|
+
const addGrpcMessageListShimToContext = require('./grpc-message-list');
|
|
4
|
+
|
|
5
|
+
// Keep this in step with BrunoGrpcRequest.
|
|
6
|
+
const addBrunoGrpcRequestShimToContext = (vm, request, grpcObject) => {
|
|
7
|
+
const requestObject = vm.newObject();
|
|
8
|
+
|
|
9
|
+
const scalars = ['url', 'method', 'methodType', 'authMode', 'protoPath', 'name'];
|
|
10
|
+
|
|
11
|
+
for (const property of scalars) {
|
|
12
|
+
const value = marshallToVm(request[property], vm);
|
|
13
|
+
vm.setProp(requestObject, property, value);
|
|
14
|
+
value.dispose();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// request.metadata — writable in `beforeCallStart`, read-only in `afterCallEnd`
|
|
18
|
+
const metadataEvalCode = addGrpcMetadataListShimToContext(
|
|
19
|
+
vm,
|
|
20
|
+
request.metadata,
|
|
21
|
+
requestObject,
|
|
22
|
+
'metadata',
|
|
23
|
+
'globalThis.bru.grpc.request'
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// request.messages — the messages the call sent, read-only, and empty until it has sent any
|
|
27
|
+
const messagesEvalCode = addGrpcMessageListShimToContext(
|
|
28
|
+
vm,
|
|
29
|
+
request.messages,
|
|
30
|
+
requestObject,
|
|
31
|
+
'globalThis.bru.grpc.request'
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// request.message — present only in `beforeMessageSend`.
|
|
35
|
+
if (request.message) {
|
|
36
|
+
const message = marshallToVm(request.message, vm);
|
|
37
|
+
vm.setProp(requestObject, 'message', message);
|
|
38
|
+
message.dispose();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
vm.setProp(grpcObject, 'request', requestObject);
|
|
42
|
+
requestObject.dispose();
|
|
43
|
+
|
|
44
|
+
return { evalCode: [metadataEvalCode, messagesEvalCode] };
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
module.exports = addBrunoGrpcRequestShimToContext;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const { marshallToVm } = require('../../utils');
|
|
2
|
+
const addGrpcMetadataListShimToContext = require('./grpc-metadata-list');
|
|
3
|
+
const addGrpcMessageListShimToContext = require('./grpc-message-list');
|
|
4
|
+
|
|
5
|
+
// Keep this in step with BrunoGrpcResponse.
|
|
6
|
+
const addBrunoGrpcResponseShimToContext = (vm, response, grpcObject) => {
|
|
7
|
+
const responseObject = vm.newObject();
|
|
8
|
+
|
|
9
|
+
// Marshalled once, as on the request: the call is over, so no scalar can change mid-hook.
|
|
10
|
+
const scalars = ['statusCode', 'statusText', 'duration'];
|
|
11
|
+
|
|
12
|
+
for (const property of scalars) {
|
|
13
|
+
const value = marshallToVm(response?.[property], vm);
|
|
14
|
+
vm.setProp(responseObject, property, value);
|
|
15
|
+
value.dispose();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// response.metadata / .trailers / .messages — the same lists `bru.grpc.request` gets, read-only here
|
|
19
|
+
const listEvalCode = ['metadata', 'trailers'].map((property) =>
|
|
20
|
+
addGrpcMetadataListShimToContext(vm, response[property], responseObject, property, 'globalThis.bru.grpc.response')
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
listEvalCode.push(
|
|
24
|
+
addGrpcMessageListShimToContext(vm, response.messages, responseObject, 'globalThis.bru.grpc.response')
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
// response.message — present only in `afterMessageReceive`
|
|
28
|
+
if (response.message) {
|
|
29
|
+
const message = marshallToVm(response.message, vm);
|
|
30
|
+
vm.setProp(responseObject, 'message', message);
|
|
31
|
+
message.dispose();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
vm.setProp(grpcObject, 'response', responseObject);
|
|
35
|
+
responseObject.dispose();
|
|
36
|
+
|
|
37
|
+
return { evalCode: listEvalCode };
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
module.exports = addBrunoGrpcResponseShimToContext;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const { createPropertyListBridge } = require('../../utils/property-list-bridge');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Bridges a GrpcMessageList — `bru.grpc.request.messages`, `bru.grpc.response.messages`
|
|
5
|
+
* — onto a VM object. Keep in sync with GrpcMessageList
|
|
6
|
+
*
|
|
7
|
+
* @param {Object} vm - QuickJS VM instance
|
|
8
|
+
* @param {Object} list - The native GrpcMessageList
|
|
9
|
+
* @param {Object} targetObject - VM object handle the list is attached to
|
|
10
|
+
* @param {string} objectPath - Path to `targetObject` in the VM, e.g. `globalThis.bru.grpc.request`
|
|
11
|
+
* @returns {string} Code the caller must eval once `objectPath` resolves
|
|
12
|
+
*/
|
|
13
|
+
const addGrpcMessageListShimToContext = (vm, list, targetObject, objectPath) => {
|
|
14
|
+
const listObject = vm.newObject();
|
|
15
|
+
|
|
16
|
+
const { evalCode } = createPropertyListBridge(vm, list, listObject, {
|
|
17
|
+
globalPath: `${objectPath}.messages`,
|
|
18
|
+
syncReadMethods: ['count'],
|
|
19
|
+
syncReadObjectMethods: ['get', 'all', 'toJSON'],
|
|
20
|
+
withIterators: true
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
vm.setProp(targetObject, 'messages', listObject);
|
|
24
|
+
listObject.dispose();
|
|
25
|
+
|
|
26
|
+
return evalCode;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
module.exports = addGrpcMessageListShimToContext;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const { createPropertyListBridge } = require('../../utils/property-list-bridge');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Bridges a GrpcMetadataList — `bru.grpc.request.metadata`, `bru.grpc.response.metadata`,
|
|
5
|
+
* `bru.grpc.response.trailers` — onto a VM object. Keep in sync with GrpcMetadataList
|
|
6
|
+
*
|
|
7
|
+
* @param {Object} vm - QuickJS VM instance
|
|
8
|
+
* @param {Object} list - The native GrpcMetadataList
|
|
9
|
+
* @param {Object} targetObject - VM object handle the list is attached to
|
|
10
|
+
* @param {string} property - Property name on `targetObject`
|
|
11
|
+
* @param {string} objectPath - Path to `targetObject` in the VM, e.g. `globalThis.bru.grpc.request`
|
|
12
|
+
* @returns {string} Code the caller must eval once `objectPath` resolves
|
|
13
|
+
*/
|
|
14
|
+
const addGrpcMetadataListShimToContext = (vm, list, targetObject, property, objectPath) => {
|
|
15
|
+
const listObject = vm.newObject();
|
|
16
|
+
|
|
17
|
+
const { evalCode } = createPropertyListBridge(vm, list, listObject, {
|
|
18
|
+
globalPath: `${objectPath}.${property}`,
|
|
19
|
+
syncReadMethods: ['get', 'has', 'count', 'indexOf', 'toObject', 'toString'],
|
|
20
|
+
syncReadObjectMethods: ['one', 'all', 'toJSON'],
|
|
21
|
+
syncWriteMethods: ['upsert', 'add', 'remove', 'clear'],
|
|
22
|
+
withIterators: true
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
vm.setProp(targetObject, property, listObject);
|
|
26
|
+
listObject.dispose();
|
|
27
|
+
|
|
28
|
+
return evalCode;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
module.exports = addGrpcMetadataListShimToContext;
|
|
@@ -8,7 +8,7 @@ const createManagedQuickJsContext = (module) => {
|
|
|
8
8
|
const vm = module.newContext();
|
|
9
9
|
const disposeTracked = trackQuickJsContext(vm);
|
|
10
10
|
const evalCodeRetained = vm.evalCode.bind(vm);
|
|
11
|
-
const waitForPendingDeferreds = trackPendingDeferreds(vm);
|
|
11
|
+
const { waitForPendingDeferreds, disposePendingDeferreds } = trackPendingDeferreds(vm);
|
|
12
12
|
|
|
13
13
|
vm.evalCode = (code, filename = 'eval.js') => {
|
|
14
14
|
const result = evalCodeRetained(code, filename);
|
|
@@ -25,7 +25,7 @@ const createManagedQuickJsContext = (module) => {
|
|
|
25
25
|
return {
|
|
26
26
|
vm,
|
|
27
27
|
waitForPendingDeferreds,
|
|
28
|
-
dispose: () => disposeQuickJsContext(vm, disposeTracked)
|
|
28
|
+
dispose: () => disposeQuickJsContext(vm, disposeTracked, disposePendingDeferreds)
|
|
29
29
|
};
|
|
30
30
|
};
|
|
31
31
|
|
|
@@ -38,27 +38,41 @@ const createManagedQuickJsContext = (module) => {
|
|
|
38
38
|
* `QuickJSUseAfterFree`. Each `.settled` resolves once the deferred is
|
|
39
39
|
* resolved/rejected, so awaiting them keeps the context alive long enough.
|
|
40
40
|
*
|
|
41
|
-
* The hook
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* drains in place until none remain.
|
|
41
|
+
* The hook installs at context creation. `waitForPendingDeferreds` drains in
|
|
42
|
+
* place (waiting can chain new deferreds) with no timeout; `disposePendingDeferreds`
|
|
43
|
+
* frees unsettled resolve/reject handles, which left alive abort JS_FreeRuntime.
|
|
45
44
|
*/
|
|
46
|
-
|
|
47
45
|
const trackPendingDeferreds = (vm) => {
|
|
48
|
-
const
|
|
46
|
+
const pendingSettles = [];
|
|
47
|
+
const deferreds = [];
|
|
49
48
|
const originalNewPromise = vm.newPromise.bind(vm);
|
|
50
49
|
vm.newPromise = (...args) => {
|
|
51
50
|
const deferred = originalNewPromise(...args);
|
|
52
|
-
|
|
51
|
+
deferreds.push(deferred);
|
|
52
|
+
pendingSettles.push(deferred.settled.catch(() => { }));
|
|
53
53
|
return deferred;
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
while (
|
|
58
|
-
const batch =
|
|
56
|
+
const waitForPendingDeferreds = async () => {
|
|
57
|
+
while (pendingSettles.length) {
|
|
58
|
+
const batch = pendingSettles.splice(0);
|
|
59
59
|
await Promise.all(batch);
|
|
60
60
|
}
|
|
61
61
|
};
|
|
62
|
+
|
|
63
|
+
const disposePendingDeferreds = () => {
|
|
64
|
+
while (deferreds.length) {
|
|
65
|
+
const deferred = deferreds.pop();
|
|
66
|
+
if (deferred.alive) {
|
|
67
|
+
deferred.dispose();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
waitForPendingDeferreds,
|
|
74
|
+
disposePendingDeferreds
|
|
75
|
+
};
|
|
62
76
|
};
|
|
63
77
|
|
|
64
78
|
/**
|
|
@@ -98,22 +112,18 @@ const trackQuickJsContext = (vm) => {
|
|
|
98
112
|
};
|
|
99
113
|
|
|
100
114
|
/**
|
|
101
|
-
*
|
|
102
|
-
*
|
|
115
|
+
* Drains pending QuickJS jobs, frees leftover deferreds and tracked handles,
|
|
116
|
+
* and disposes the context. Jobs are drained BEFORE the handle flush: a job
|
|
117
|
+
* can call allocating shims or start new async work, and anything it creates
|
|
118
|
+
* must still be freed or JS_FreeRuntime aborts on the leftover GC objects.
|
|
103
119
|
*/
|
|
104
|
-
const disposeQuickJsContext = (vm, disposeTracked) => {
|
|
120
|
+
const disposeQuickJsContext = (vm, disposeTracked, disposePendingDeferreds) => {
|
|
105
121
|
if (!vm?.alive) {
|
|
106
122
|
return;
|
|
107
123
|
}
|
|
108
124
|
|
|
109
|
-
|
|
110
|
-
|
|
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.
|
|
125
|
+
// Executing a job can schedule more jobs (chained `.then()`s), so keep going
|
|
126
|
+
// until `hasPendingJob()` reports the queue is empty or a job throws.
|
|
117
127
|
while (vm.runtime?.hasPendingJob?.()) {
|
|
118
128
|
const result = vm.runtime.executePendingJobs();
|
|
119
129
|
// On error, dispose the error handle and stop draining.
|
|
@@ -122,6 +132,15 @@ const disposeQuickJsContext = (vm, disposeTracked) => {
|
|
|
122
132
|
break;
|
|
123
133
|
}
|
|
124
134
|
}
|
|
135
|
+
|
|
136
|
+
if (typeof disposePendingDeferreds === 'function') {
|
|
137
|
+
disposePendingDeferreds();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (typeof disposeTracked === 'function') {
|
|
141
|
+
disposeTracked();
|
|
142
|
+
}
|
|
143
|
+
|
|
125
144
|
vm.dispose();
|
|
126
145
|
};
|
|
127
146
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const YAML = require('yaml');
|
|
4
|
+
const { SCRIPT_TYPES } = require('@usebruno/common');
|
|
4
5
|
const { NODEVM_SCRIPT_WRAPPER_OFFSET, QUICKJS_SCRIPT_WRAPPER_OFFSET } = require('./sandbox');
|
|
5
6
|
|
|
6
7
|
const posixifyPath = (p) => (p ? p.replace(/\\/g, '/') : p);
|
|
@@ -11,17 +12,15 @@ const ALLOWED_SOURCE_EXTENSIONS = ['.bru', '.yml'];
|
|
|
11
12
|
const isAllowedSourceFile = (filePath) =>
|
|
12
13
|
typeof filePath === 'string' && ALLOWED_SOURCE_EXTENSIONS.some((ext) => filePath.endsWith(ext));
|
|
13
14
|
|
|
14
|
-
const SCRIPT_TYPES = Object.freeze({
|
|
15
|
-
PRE_REQUEST: 'pre-request',
|
|
16
|
-
POST_RESPONSE: 'post-response',
|
|
17
|
-
TEST: 'test'
|
|
18
|
-
});
|
|
19
|
-
|
|
20
15
|
// Bruno script types → OpenCollection YAML script types
|
|
21
16
|
const SCRIPT_TYPE_TO_YML = {
|
|
22
17
|
[SCRIPT_TYPES.PRE_REQUEST]: 'before-request',
|
|
23
18
|
[SCRIPT_TYPES.POST_RESPONSE]: 'after-response',
|
|
24
|
-
[SCRIPT_TYPES.TEST]: 'tests'
|
|
19
|
+
[SCRIPT_TYPES.TEST]: 'tests',
|
|
20
|
+
[SCRIPT_TYPES.BEFORE_CALL_START]: 'grpc:before-call-start',
|
|
21
|
+
[SCRIPT_TYPES.BEFORE_MESSAGE_SEND]: 'grpc:before-message-send',
|
|
22
|
+
[SCRIPT_TYPES.AFTER_MESSAGE_RECEIVE]: 'grpc:after-message-receive',
|
|
23
|
+
[SCRIPT_TYPES.AFTER_CALL_END]: 'grpc:after-call-end'
|
|
25
24
|
};
|
|
26
25
|
|
|
27
26
|
const readFile = (filePath, cache = null) => {
|
|
@@ -38,7 +37,11 @@ const readFile = (filePath, cache = null) => {
|
|
|
38
37
|
const BLOCK_PATTERNS = {
|
|
39
38
|
[SCRIPT_TYPES.PRE_REQUEST]: /^script:pre-request\s*\{/,
|
|
40
39
|
[SCRIPT_TYPES.POST_RESPONSE]: /^script:post-response\s*\{/,
|
|
41
|
-
[SCRIPT_TYPES.TEST]: /^tests\s*\{
|
|
40
|
+
[SCRIPT_TYPES.TEST]: /^tests\s*\{/,
|
|
41
|
+
[SCRIPT_TYPES.BEFORE_CALL_START]: /^script:grpc:before-call-start\s*\{/,
|
|
42
|
+
[SCRIPT_TYPES.BEFORE_MESSAGE_SEND]: /^script:grpc:before-message-send\s*\{/,
|
|
43
|
+
[SCRIPT_TYPES.AFTER_MESSAGE_RECEIVE]: /^script:grpc:after-message-receive\s*\{/,
|
|
44
|
+
[SCRIPT_TYPES.AFTER_CALL_END]: /^script:grpc:after-call-end\s*\{/
|
|
42
45
|
};
|
|
43
46
|
|
|
44
47
|
/** Find the 1-indexed line where a script block's content starts in a .bru file */
|
|
@@ -747,5 +750,6 @@ module.exports = {
|
|
|
747
750
|
findYmlScriptBlockStartLine,
|
|
748
751
|
findYmlScriptBlockEndLine,
|
|
749
752
|
adjustStackTrace,
|
|
750
|
-
getErrorTypeName
|
|
753
|
+
getErrorTypeName,
|
|
754
|
+
posixifyPath
|
|
751
755
|
};
|