mirror of
https://github.com/Kong/insomnia.git
synced 2026-10-11 16:43:22 -04:00
fix(quickjs): free bridge promise resolvers before disposing the VM
`vm.newPromise()` allocates three JSValues — the promise plus its `resolve` and `reject` functions — and quickjs-emscripten frees the two functions only from inside `resolve()`/`reject()`. `installSendRequestBridge` returned the promise handle but could skip both (the `disposedRef` guard did exactly that), so a `sendRequest()` still in flight at teardown left two live function objects in the runtime and `JS_FreeRuntime` aborted on `assert(list_empty(&rt->gc_obj_list))` — a native Emscripten abort that takes down the Worker and the WASM module shared with the template-tag sandbox. Track every unsettled deferred and dispose it before `vm.dispose()`, which is the cleanup the library documents for this case. Guard the late-response path on `vm.alive`/`deferred.alive` instead of a hand-rolled flag. Also plumb the templating-db auth token from `run-script-quickjs.ts` through the worker into the engine — it was accepted but never passed, so every bridge fetch was rejected 401 by the protocol's auth gate. The crash reproduces in vitest after all: the mocked fetch in the existing tests settles in the same microtask checkpoint as the call, so the deferred was always settled before teardown. Settling it on a later macrotask reproduces the exact production abort, so the smoke test no longer needs `test.fail()`.
This commit is contained in:
1 parent
a2930ba59e
commit
5ad0278188
6 files changed
+231
-69
No files matched your search
@@ -6,15 +6,13 @@ import { test } from '../../playwright/test';
|
||||
// PR 2 of the QuickJS script-sandbox rollout plan —
|
||||
// https://gist.github.com/jackkav/3ebf8768bf84be024a3a138874919354
|
||||
//
|
||||
// KNOWN BUG (unresolved): insomnia.sendRequest() crashes under the real Electron/Worker/QuickJS
|
||||
// environment with `Aborted(Assertion failed: list_empty(&rt->gc_obj_list), at:
|
||||
// .../quickjs.c,2036,JS_FreeRuntime)` on the first real network round trip, even after three
|
||||
// targeted fixes (see quickjs-script-engine.ts's module doc comment and installSendRequestBridge).
|
||||
// This never reproduces in vitest (mocked fetch, or a plain Node http server) — only real Electron
|
||||
// fetch() timing inside the dedicated Worker reproduces it. `test.fail()` marks this as an expected
|
||||
// failure: CI stays green while the bug is open, and flips red (telling us to remove the
|
||||
// annotation) the moment someone actually fixes it.
|
||||
test.describe('QuickJS sendRequest bridge (known bug)', () => {
|
||||
// End-to-end cover for the `insomnia.sendRequest()` bridge: the real Electron protocol handler, the
|
||||
// real auth token (forwarded by run-script-quickjs.ts), and real curl. The teardown hazard this used
|
||||
// to crash on is covered far more cheaply by
|
||||
// `packages/insomnia/src/scripting/quickjs-script-engine.test.ts`'s "sendRequest bridge teardown"
|
||||
// block, which reproduces the `gc_obj_list` abort in vitest by settling the mocked fetch on a later
|
||||
// macrotask.
|
||||
test.describe('QuickJS sendRequest bridge', () => {
|
||||
test.slow(process.platform === 'darwin' || process.platform === 'win32', 'Slow app start on these platforms');
|
||||
|
||||
test.beforeEach(async ({ app, page }) => {
|
||||
@@ -48,8 +46,6 @@ test.describe('QuickJS sendRequest bridge (known bug)', () => {
|
||||
};
|
||||
|
||||
test('runs a real sendRequest() round trip through the QuickJS engine', async ({ page, insomnia }) => {
|
||||
test.fail(true, 'insomnia.sendRequest() crashes under real Electron/Worker timing — see file header comment');
|
||||
|
||||
await enableQuickJsSandbox(page);
|
||||
|
||||
// "testQueryParams" has no body and no folder-inherited after-response script — see
|
||||
|
||||
@@ -200,12 +200,6 @@ describe('runScriptInQuickJs', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// KNOWN BUG (unresolved): a real sendRequest() round trip crashes under the real Electron/Worker
|
||||
// environment (see quickjs-script-engine.ts's module doc comment and
|
||||
// packages/insomnia-smoke-test/tests/smoke/quickjs-sendrequest-bridge.test.ts, which reproduces it
|
||||
// end to end). These unit tests mock `fetch` entirely, so they only cover the bridge's own request
|
||||
// normalization / response reconstruction / error propagation logic — they pass even though the
|
||||
// real-environment crash is still open, which is exactly the gap this WIP branch exists to flag.
|
||||
describe('runScriptInQuickJs sendRequest bridge', () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
@@ -362,3 +356,130 @@ describe('runScriptInQuickJs sendRequest bridge', () => {
|
||||
expect((result.environment.data as Record<string, unknown>).callbackError).toBe('DNS resolution failed');
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* `vm.newPromise()` allocates three JSValues — the promise plus its `resolve`/`reject` functions —
|
||||
* and quickjs-emscripten frees the latter two only from inside `resolve()`/`reject()`. Disposing the
|
||||
* runtime while a bridge promise is still pending therefore leaks two live function objects and
|
||||
* trips QuickJS's `assert(list_empty(&rt->gc_obj_list))` in `JS_FreeRuntime`. That is a native
|
||||
* Emscripten `abort()`, not a normal exception, so it takes down the whole script worker rather than
|
||||
* surfacing as a script error.
|
||||
*
|
||||
* The earlier bridge tests can't catch it because a `mockResolvedValue` fetch settles inside the
|
||||
* same microtask checkpoint as the call, so the deferred is always settled before teardown. These
|
||||
* tests deliberately settle the fetch on a *later macrotask* — the only thing real Electron `fetch()`
|
||||
* timing adds — and each one aborts the module without the teardown fix in `runScriptInQuickJs`.
|
||||
*/
|
||||
describe('runScriptInQuickJs sendRequest bridge teardown', () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
const stubSlowFetch = (delayMs: number) => {
|
||||
const fetchMock = vi.fn().mockImplementation(
|
||||
() =>
|
||||
new Promise(resolve =>
|
||||
setTimeout(
|
||||
() =>
|
||||
resolve({
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: () =>
|
||||
Promise.resolve(
|
||||
JSON.stringify({ code: 200, status: 'OK', headers: [], body: 'late', responseTime: delayMs }),
|
||||
),
|
||||
}),
|
||||
delayMs,
|
||||
),
|
||||
),
|
||||
);
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
return fetchMock;
|
||||
};
|
||||
|
||||
it('tears down cleanly when the script never awaits its sendRequest', async () => {
|
||||
stubSlowFetch(120);
|
||||
const context = baseContext();
|
||||
|
||||
// Postman-style fire-and-forget: the script returns while the request is still in flight.
|
||||
const result = await runScriptInQuickJs({
|
||||
script: `
|
||||
insomnia.sendRequest('https://example.com', () => {});
|
||||
insomnia.environment.set('finished', true);
|
||||
`,
|
||||
context,
|
||||
});
|
||||
|
||||
expect((result.environment.data as Record<string, unknown>).finished).toBe(true);
|
||||
|
||||
// The response lands after teardown; it must be a silent no-op rather than an abort or an
|
||||
// unhandled QuickJSUseAfterFree rejection out of the settle callback.
|
||||
await new Promise(resolve => setTimeout(resolve, 250));
|
||||
|
||||
// A later run proving the shared WASM module survived — after an Emscripten abort this throws.
|
||||
const laterResult = await runScriptInQuickJs({
|
||||
script: 'insomnia.environment.set("ranCleanly", true);',
|
||||
context: baseContext(),
|
||||
});
|
||||
expect((laterResult.environment.data as Record<string, unknown>).ranCleanly).toBe(true);
|
||||
});
|
||||
|
||||
it('tears down cleanly when the deadline fires while a sendRequest is in flight', async () => {
|
||||
stubSlowFetch(2000);
|
||||
const context = baseContext();
|
||||
context.settings = { timeout: 100 } as any;
|
||||
|
||||
await expect(
|
||||
runScriptInQuickJs({ script: `await insomnia.sendRequest('https://example.com');`, context }),
|
||||
).rejects.toThrow(/Executing script timeout: 100/);
|
||||
|
||||
const laterResult = await runScriptInQuickJs({
|
||||
script: 'insomnia.environment.set("ranCleanly", true);',
|
||||
context: baseContext(),
|
||||
});
|
||||
expect((laterResult.environment.data as Record<string, unknown>).ranCleanly).toBe(true);
|
||||
});
|
||||
|
||||
it('tears down cleanly when only some of several sendRequests have settled', async () => {
|
||||
let call = 0;
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn().mockImplementation(
|
||||
() =>
|
||||
new Promise(resolve =>
|
||||
// First call answers immediately, second is still outstanding at teardown.
|
||||
setTimeout(
|
||||
() =>
|
||||
resolve({
|
||||
ok: true,
|
||||
status: 200,
|
||||
text: () =>
|
||||
Promise.resolve(
|
||||
JSON.stringify({ code: 200, status: 'OK', headers: [], body: 'x', responseTime: 1 }),
|
||||
),
|
||||
}),
|
||||
call++ === 0 ? 0 : 2000,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
const context = baseContext();
|
||||
|
||||
const result = await runScriptInQuickJs({
|
||||
script: `
|
||||
const first = await insomnia.sendRequest('https://example.com/one');
|
||||
insomnia.sendRequest('https://example.com/two', () => {});
|
||||
insomnia.environment.set('firstBody', first.body);
|
||||
`,
|
||||
context,
|
||||
});
|
||||
|
||||
expect((result.environment.data as Record<string, unknown>).firstBody).toBe('x');
|
||||
|
||||
const laterResult = await runScriptInQuickJs({
|
||||
script: 'insomnia.environment.set("ranCleanly", true);',
|
||||
context: baseContext(),
|
||||
});
|
||||
expect((laterResult.environment.data as Record<string, unknown>).ranCleanly).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { QuickJSContext, QuickJSHandle } from 'quickjs-emscripten';
|
||||
import type { QuickJSContext, QuickJSDeferredPromise, QuickJSHandle } from 'quickjs-emscripten';
|
||||
|
||||
import { Console } from '../../../insomnia-scripting-environment/src/objects/console';
|
||||
import type { RequestContext } from '../../../insomnia-scripting-environment/src/objects/interfaces';
|
||||
@@ -22,13 +22,6 @@ import { getQuickJSModule } from '../templating/sandbox/quickjs-runtime';
|
||||
* only `code`/`status`/`headers`/`body`/`responseTime`/`json()`/`text()` (no chai assertions, no
|
||||
* `originalRequest`). Full parity with the hidden-window `Response` class is deferred.
|
||||
*
|
||||
* KNOWN BUG (unresolved): a real sendRequest() round trip crashes under the real Electron/Worker
|
||||
* environment with `Aborted(Assertion failed: list_empty(&rt->gc_obj_list), at:
|
||||
* .../quickjs.c,2036,JS_FreeRuntime)` — see installSendRequestBridge's comment and
|
||||
* packages/insomnia-smoke-test/tests/smoke/quickjs-sendrequest-bridge.test.ts, which reproduces it.
|
||||
* Never reproduces in vitest (mocked fetch, or a real Node http server) — only real Electron fetch()
|
||||
* timing inside the dedicated Worker.
|
||||
*
|
||||
* Not supported (throws inside the script if called): insomnia.test()/pm.test(), collectionVariables,
|
||||
* vault, cookies, client certificates, and mutating the request.
|
||||
*/
|
||||
@@ -53,13 +46,14 @@ export const runScriptInQuickJs = async ({
|
||||
const environmentData: Record<string, unknown> = { ...context.environment.data };
|
||||
const variablesData: Record<string, unknown> = { ...context.transientVariables?.data };
|
||||
|
||||
// Set right after `vm.dispose()` below. The sendRequest bridge is a real async host round trip
|
||||
// (a `fetch()` to the Electron main process) whose resolution isn't driven by QuickJS's own job
|
||||
// queue the way in-VM promises are — checking this flag before ever touching `vm`/a promise
|
||||
// handle from that callback is what makes a late-arriving response after this function has
|
||||
// already returned (deadline/interrupt fired, or the caller otherwise moved on) a safe no-op
|
||||
// instead of a `QuickJSUseAfterFree` (or a native WASM abort disposing an in-use runtime).
|
||||
const disposedRef = { current: false };
|
||||
// Every VM promise handed to the script by `installSendRequestBridge` that hasn't settled yet.
|
||||
// Each one owns three JSValues — the promise plus its `resolve`/`reject` functions — and
|
||||
// quickjs-emscripten only frees the two functions from inside `resolve()`/`reject()`. A VM
|
||||
// promise still pending when the runtime is freed therefore leaks two live function objects,
|
||||
// which trips QuickJS's `assert(list_empty(&rt->gc_obj_list))` in `JS_FreeRuntime`: a native
|
||||
// Emscripten abort rather than a catchable error, so it takes the script worker down instead of
|
||||
// surfacing as a script failure. `finally` below settles this set before disposing.
|
||||
const pendingDeferreds = new Set<QuickJSDeferredPromise>();
|
||||
|
||||
try {
|
||||
// Polled during synchronous execution so a tight sync loop in the script can't bypass the timeout.
|
||||
@@ -69,7 +63,7 @@ export const runScriptInQuickJs = async ({
|
||||
installConsole(vm, scriptConsole);
|
||||
installKeyValueBridge(vm, '__envGet', '__envSet', environmentData);
|
||||
installKeyValueBridge(vm, '__varGet', '__varSet', variablesData);
|
||||
installSendRequestBridge(vm, authToken, disposedRef);
|
||||
installSendRequestBridge(vm, pendingDeferreds, authToken);
|
||||
setGlobalString(vm, '__requestJSON', JSON.stringify(context.request ?? {}));
|
||||
|
||||
evalOrThrow(vm, BOOTSTRAP, '<quickjs-script-bootstrap>');
|
||||
@@ -77,7 +71,12 @@ export const runScriptInQuickJs = async ({
|
||||
|
||||
await driveTaskToCompletion(vm, deadline, timeoutMs);
|
||||
} finally {
|
||||
disposedRef.current = true;
|
||||
// Ordering matters: free the resolve/reject functions of anything still in flight (a script
|
||||
// that never awaited its sendRequest, or a run that hit the deadline mid-request) *before*
|
||||
// the runtime, or `JS_FreeRuntime` aborts. `dispose()` is documented as idempotent, so
|
||||
// already-settled deferreds still in the set are harmless.
|
||||
pendingDeferreds.forEach(deferred => deferred.dispose());
|
||||
pendingDeferreds.clear();
|
||||
vm.dispose();
|
||||
}
|
||||
|
||||
@@ -203,44 +202,37 @@ const sendRequestViaFetch = async (requestBodyJson: string, authToken?: string):
|
||||
};
|
||||
|
||||
/** Registers the async `__sendRequest(bodyJson)` bridge backing `insomnia.sendRequest()` in BOOTSTRAP. */
|
||||
const installSendRequestBridge = (vm: QuickJSContext, authToken?: string, disposedRef?: { current: boolean }): void => {
|
||||
const installSendRequestBridge = (
|
||||
vm: QuickJSContext,
|
||||
pendingDeferreds: Set<QuickJSDeferredPromise>,
|
||||
authToken?: string,
|
||||
): void => {
|
||||
const fn = vm.newFunction('__sendRequest', bodyHandle => {
|
||||
const bodyJson = vm.getString(bodyHandle);
|
||||
const deferred = vm.newPromise();
|
||||
// Registered so the run's `finally` can free this deferred's resolve/reject functions if the
|
||||
// fetch is still outstanding when the VM is torn down — see `pendingDeferreds`' comment.
|
||||
pendingDeferreds.add(deferred);
|
||||
// `Promise.resolve().then(...)` defers the real fetch() call by one microtask tick so it never
|
||||
// runs while still nested inside this C-to-JS callback frame — matching host-bridge.ts's
|
||||
// installHostBridge (the proven-working equivalent for the template-tag sandbox). Calling the
|
||||
// async bridge function directly here instead (no extra tick) let its `await fetch(...)`
|
||||
// resolve while still on that native callback's call stack, which corrupted the WASM heap under
|
||||
// real Electron fetch() timing (reproduced as a `QuickJSUseAfterFree` / `gc_obj_list` abort on
|
||||
// `vm.newString` inside the resolution handler) — a reentrancy window neither a mocked fetch in
|
||||
// unit tests nor a plain Node http client ever hit, since both settle within the same tick.
|
||||
// NOTE: this deferral alone did NOT resolve the crash (see the module doc comment above) — kept
|
||||
// because it's still a real fix for the reentrancy hazard it targets, just not a sufficient one.
|
||||
// runs while still nested inside this C-to-JS callback frame — matching plugin-tag-sandbox.ts's
|
||||
// installHostBridge, the proven-working equivalent for the template-tag sandbox.
|
||||
Promise.resolve()
|
||||
.then(() => sendRequestViaFetch(bodyJson, authToken))
|
||||
.then(
|
||||
value => {
|
||||
if (!disposedRef?.current) {
|
||||
resolveDeferredWithString(vm, deferred, JSON.stringify({ ok: true, value }));
|
||||
}
|
||||
},
|
||||
err => {
|
||||
if (!disposedRef?.current) {
|
||||
resolveDeferredWithString(
|
||||
vm,
|
||||
deferred,
|
||||
JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err) }),
|
||||
);
|
||||
}
|
||||
},
|
||||
value => settle(vm, deferred, pendingDeferreds, JSON.stringify({ ok: true, value })),
|
||||
err =>
|
||||
settle(
|
||||
vm,
|
||||
deferred,
|
||||
pendingDeferreds,
|
||||
JSON.stringify({ ok: false, error: err instanceof Error ? err.message : String(err) }),
|
||||
),
|
||||
);
|
||||
// The settled VM promise schedules a job; pump it so the awaiting script resumes.
|
||||
deferred.settled.then(() => {
|
||||
if (!disposedRef?.current) {
|
||||
if (vm.alive) {
|
||||
// executePendingJobs() returns a DisposableResult — on failure its `.error` is a live
|
||||
// QuickJSHandle. Discarding the result outright (as this used to) leaks that handle,
|
||||
// which is exactly what trips "gc_obj_list not empty" on a later vm.dispose().
|
||||
// QuickJSHandle that leaks if the result is discarded rather than disposed.
|
||||
vm.runtime.executePendingJobs().dispose();
|
||||
}
|
||||
});
|
||||
@@ -249,14 +241,28 @@ const installSendRequestBridge = (vm: QuickJSContext, authToken?: string, dispos
|
||||
setGlobal(vm, '__sendRequest', fn);
|
||||
};
|
||||
|
||||
const resolveDeferredWithString = (
|
||||
/**
|
||||
* Resolve a bridge deferred with a JSON payload, if the VM is still around to receive it.
|
||||
*
|
||||
* A `fetch()` can land after the run has finished (the script never awaited it, or the deadline
|
||||
* fired first), by which point the VM is gone: `vm.newString` on a disposed context throws
|
||||
* `QuickJSUseAfterFree` synchronously inside this `.then`, i.e. an unhandled rejection in the
|
||||
* worker. `deferred.alive` covers the narrower case of a deferred already force-disposed by the
|
||||
* run's `finally` while the context itself is somehow still up.
|
||||
*/
|
||||
const settle = (
|
||||
vm: QuickJSContext,
|
||||
deferred: ReturnType<QuickJSContext['newPromise']>,
|
||||
deferred: QuickJSDeferredPromise,
|
||||
pendingDeferreds: Set<QuickJSDeferredPromise>,
|
||||
value: string,
|
||||
): void => {
|
||||
if (!vm.alive || !deferred.alive) {
|
||||
return;
|
||||
}
|
||||
const handle = vm.newString(value);
|
||||
deferred.resolve(handle);
|
||||
handle.dispose();
|
||||
pendingDeferreds.delete(deferred);
|
||||
};
|
||||
|
||||
/** Registers `<getName>(key)`/`<setName>(key, jsonValue)` host functions backed by a plain object. */
|
||||
|
||||
@@ -12,6 +12,8 @@ interface WorkerRequest {
|
||||
id: string;
|
||||
script: string;
|
||||
context: RequestContext;
|
||||
/** Forwarded by `run-script-quickjs.ts`; a Worker can't reach `window.main` to fetch it itself. */
|
||||
authToken?: string;
|
||||
}
|
||||
|
||||
interface WorkerErrorResponse {
|
||||
@@ -27,9 +29,9 @@ interface WorkerSuccessResponse {
|
||||
export type WorkerResponse = WorkerSuccessResponse | WorkerErrorResponse;
|
||||
|
||||
self.onmessage = async (event: MessageEvent<WorkerRequest>) => {
|
||||
const { id, script, context } = event.data;
|
||||
const { id, script, context, authToken } = event.data;
|
||||
try {
|
||||
const result = await runScriptInQuickJs({ script, context });
|
||||
const result = await runScriptInQuickJs({ script, context, authToken });
|
||||
self.postMessage({ id, result } satisfies WorkerSuccessResponse);
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err : new Error(String(err));
|
||||
|
||||
@@ -56,12 +56,23 @@ class FakeWorker {
|
||||
describe('runScriptInQuickJs (worker client)', () => {
|
||||
let fakeWorker: FakeWorker;
|
||||
|
||||
/**
|
||||
* The client fetches the templating-db auth token over IPC before posting, so the postMessage
|
||||
* lands a microtask later than the call. Awaiting this lets a test assert on `postedMessages`.
|
||||
*/
|
||||
const flushTokenFetch = () => new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
beforeEach(async () => {
|
||||
fakeWorker = new FakeWorker();
|
||||
vi.stubGlobal(
|
||||
'Worker',
|
||||
vi.fn(() => fakeWorker),
|
||||
);
|
||||
// A Worker can't reach `window.main`, so the client fetches the bridge auth token here and
|
||||
// forwards it in every message — see `getTemplatingDbAuthToken`.
|
||||
vi.stubGlobal('window', {
|
||||
main: { templatingDb: { getAuthToken: vi.fn().mockResolvedValue('test-auth-token') } },
|
||||
});
|
||||
// The module keeps a lazily-created singleton worker at module scope — reset it between tests
|
||||
// by re-importing fresh so each test gets its own FakeWorker instance.
|
||||
vi.resetModules();
|
||||
@@ -76,9 +87,13 @@ describe('runScriptInQuickJs (worker client)', () => {
|
||||
const context = baseContext();
|
||||
|
||||
const promise = runScriptInQuickJs({ script: 'console.log(1)', context });
|
||||
await flushTokenFetch();
|
||||
expect(fakeWorker.postedMessages).toHaveLength(1);
|
||||
const { id } = fakeWorker.postedMessages[0];
|
||||
const { id, authToken } = fakeWorker.postedMessages[0];
|
||||
expect(typeof id).toBe('string');
|
||||
// Forwarded so the engine's sendRequest bridge can authenticate against the templating-db
|
||||
// protocol; without it every bridge fetch comes back 401.
|
||||
expect(authToken).toBe('test-auth-token');
|
||||
|
||||
const result = { ...context, logs: ['log: 1\n'] };
|
||||
fakeWorker.emitMessage({ id, result });
|
||||
@@ -91,6 +106,7 @@ describe('runScriptInQuickJs (worker client)', () => {
|
||||
const context = baseContext();
|
||||
|
||||
const promise = runScriptInQuickJs({ script: 'throw new Error("boom")', context });
|
||||
await flushTokenFetch();
|
||||
const { id } = fakeWorker.postedMessages[0];
|
||||
fakeWorker.emitMessage({ id, error: { message: 'boom', name: 'Error' } });
|
||||
|
||||
@@ -102,6 +118,7 @@ describe('runScriptInQuickJs (worker client)', () => {
|
||||
const context = baseContext();
|
||||
|
||||
const promise = runScriptInQuickJs({ script: 'console.log(1)', context });
|
||||
await flushTokenFetch();
|
||||
fakeWorker.emitMessage({ id: 'not-a-real-id', result: {} });
|
||||
|
||||
const { id } = fakeWorker.postedMessages[0];
|
||||
@@ -136,6 +153,7 @@ describe('runScriptInQuickJs (worker client)', () => {
|
||||
);
|
||||
|
||||
const secondCall = runScriptInQuickJs({ script: 'console.log(1)', context });
|
||||
await flushTokenFetch();
|
||||
expect(secondWorker.postedMessages).toHaveLength(1);
|
||||
const { id } = secondWorker.postedMessages[0];
|
||||
const result = { ...context, logs: [] };
|
||||
|
||||
@@ -57,16 +57,35 @@ const getWorker = (): Worker => {
|
||||
return instance;
|
||||
};
|
||||
|
||||
export const runScriptInQuickJs = ({
|
||||
// A Worker has no `window.main`, so — exactly as `ui/worker/templating-handler.ts` does for the
|
||||
// templating worker — fetch the `insomnia-templating-worker-database://` auth token here once and
|
||||
// forward it on every postMessage. Without it the engine's `insomnia.sendRequest()` bridge fetch is
|
||||
// rejected by the protocol's auth gate with a 401.
|
||||
let authTokenPromise: Promise<string> | null = null;
|
||||
const getTemplatingDbAuthToken = (): Promise<string> => {
|
||||
if (!authTokenPromise) {
|
||||
authTokenPromise = window.main.templatingDb.getAuthToken();
|
||||
}
|
||||
return authTokenPromise;
|
||||
};
|
||||
|
||||
export const runScriptInQuickJs = async ({
|
||||
script,
|
||||
context,
|
||||
}: {
|
||||
script: string;
|
||||
context: RequestContext;
|
||||
}): Promise<RequestContext> => {
|
||||
// The worker and the pending entry are set up before awaiting the token, so a crash during the
|
||||
// token round trip still rejects this call through the `error` listener rather than hanging.
|
||||
const instance = getWorker();
|
||||
const id = `quickjs-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
return new Promise<RequestContext>((resolve, reject) => {
|
||||
const result = new Promise<RequestContext>((resolve, reject) => {
|
||||
pending.set(id, { resolve, reject });
|
||||
getWorker().postMessage({ id, script, context });
|
||||
});
|
||||
|
||||
const authToken = await getTemplatingDbAuthToken();
|
||||
instance.postMessage({ id, script, context, authToken });
|
||||
|
||||
return result;
|
||||
};
|
||||
Reference in new issue
Block a user