@rstest/browser 0.8.5 → 0.9.1
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/LICENSE-APACHE-2.0 +202 -0
- package/NOTICE +11 -0
- package/dist/361.js +8 -0
- package/dist/augmentExpect.d.ts +73 -0
- package/dist/browser-container/container-static/css/index.5c72297783.css +1 -0
- package/dist/browser-container/container-static/js/{565.226c9ef5.js → 101.36a8ccdf84.js} +4024 -3856
- package/dist/browser-container/container-static/js/101.36a8ccdf84.js.LICENSE.txt +1 -0
- package/dist/browser-container/container-static/js/{index.c1d17467.js → index.28d833de0b.js} +732 -675
- package/dist/browser-container/container-static/js/{lib-react.97ee79b0.js → lib-react.dcf2a5e57a.js} +10 -10
- package/dist/browser-container/container-static/js/lib-react.dcf2a5e57a.js.LICENSE.txt +1 -0
- package/dist/browser-container/index.html +1 -1
- package/dist/browser.d.ts +2 -0
- package/dist/browser.js +583 -0
- package/dist/browserRpcRegistry.d.ts +18 -0
- package/dist/client/api.d.ts +3 -0
- package/dist/client/browserRpc.d.ts +2 -0
- package/dist/client/dispatchTransport.d.ts +11 -0
- package/dist/client/entry.d.ts +1 -5
- package/dist/client/locator.d.ts +125 -0
- package/dist/client/snapshot.d.ts +0 -6
- package/dist/concurrency.d.ts +12 -0
- package/dist/dispatchCapabilities.d.ts +34 -0
- package/dist/dispatchRouter.d.ts +20 -0
- package/dist/headedSerialTaskQueue.d.ts +8 -0
- package/dist/headlessLatestRerunScheduler.d.ts +19 -0
- package/dist/headlessTransport.d.ts +12 -0
- package/dist/hostController.d.ts +16 -0
- package/dist/index.js +1790 -296
- package/dist/protocol.d.ts +44 -33
- package/dist/providers/index.d.ts +79 -0
- package/dist/providers/playwright/compileLocator.d.ts +3 -0
- package/dist/providers/playwright/dispatchBrowserRpc.d.ts +13 -0
- package/dist/providers/playwright/expectUtils.d.ts +24 -0
- package/dist/providers/playwright/implementation.d.ts +2 -0
- package/dist/providers/playwright/index.d.ts +1 -0
- package/dist/providers/playwright/runtime.d.ts +5 -0
- package/dist/providers/playwright/textMatcher.d.ts +8 -0
- package/dist/rpcProtocol.d.ts +145 -0
- package/dist/runSession.d.ts +33 -0
- package/dist/sessionRegistry.d.ts +34 -0
- package/dist/sourceMap/sourceMapLoader.d.ts +14 -0
- package/dist/watchCliShortcuts.d.ts +6 -0
- package/dist/watchRerunPlanner.d.ts +21 -0
- package/package.json +17 -12
- package/src/AGENTS.md +128 -0
- package/src/augmentExpect.ts +62 -0
- package/src/browser.ts +3 -0
- package/src/browserRpcRegistry.ts +57 -0
- package/src/client/AGENTS.md +82 -0
- package/src/client/api.ts +213 -0
- package/src/client/browserRpc.ts +86 -0
- package/src/client/dispatchTransport.ts +178 -0
- package/src/client/entry.ts +96 -33
- package/src/client/locator.ts +452 -0
- package/src/client/snapshot.ts +32 -97
- package/src/client/sourceMapSupport.ts +26 -37
- package/src/concurrency.ts +62 -0
- package/src/dispatchCapabilities.ts +162 -0
- package/src/dispatchRouter.ts +82 -0
- package/src/env.d.ts +8 -1
- package/src/headedSerialTaskQueue.ts +19 -0
- package/src/headlessLatestRerunScheduler.ts +76 -0
- package/src/headlessTransport.ts +28 -0
- package/src/hostController.ts +1538 -384
- package/src/protocol.ts +66 -31
- package/src/providers/index.ts +103 -0
- package/src/providers/playwright/compileLocator.ts +130 -0
- package/src/providers/playwright/dispatchBrowserRpc.ts +372 -0
- package/src/providers/playwright/expectUtils.ts +57 -0
- package/src/providers/playwright/implementation.ts +33 -0
- package/src/providers/playwright/index.ts +1 -0
- package/src/providers/playwright/runtime.ts +32 -0
- package/src/providers/playwright/textMatcher.ts +10 -0
- package/src/rpcProtocol.ts +220 -0
- package/src/runSession.ts +110 -0
- package/src/sessionRegistry.ts +89 -0
- package/src/sourceMap/sourceMapLoader.ts +96 -0
- package/src/watchCliShortcuts.ts +77 -0
- package/src/watchRerunPlanner.ts +77 -0
- package/dist/browser-container/container-static/css/index.5a71c757.css +0 -1
- package/dist/browser-container/container-static/js/565.226c9ef5.js.LICENSE.txt +0 -1
- package/dist/browser-container/container-static/js/lib-react.97ee79b0.js.LICENSE.txt +0 -1
- package/dist/browser-container/container-static/js/scheduler.5accca0c.js +0 -407
- package/dist/browser-container/scheduler.html +0 -19
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import type { Rstest } from '@rstest/core/browser';
|
|
3
|
+
|
|
4
|
+
// Shared headless concurrency policy.
|
|
5
|
+
// Keep this in one place so executors reuse the same worker semantics.
|
|
6
|
+
const DEFAULT_MAX_HEADLESS_WORKERS = 12;
|
|
7
|
+
|
|
8
|
+
export type HeadlessConcurrencyContext = Pick<Rstest, 'command'> & {
|
|
9
|
+
normalizedConfig: {
|
|
10
|
+
pool: {
|
|
11
|
+
maxWorkers?: string | number;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const getNumCpus = (): number => {
|
|
17
|
+
return os.availableParallelism?.() ?? os.cpus().length;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const parseWorkers = (
|
|
21
|
+
maxWorkers: string | number,
|
|
22
|
+
numCpus = getNumCpus(),
|
|
23
|
+
): number => {
|
|
24
|
+
const parsed = Number.parseInt(maxWorkers.toString(), 10);
|
|
25
|
+
|
|
26
|
+
if (typeof maxWorkers === 'string' && maxWorkers.trim().endsWith('%')) {
|
|
27
|
+
const workers = Math.floor((parsed / 100) * numCpus);
|
|
28
|
+
return Math.max(workers, 1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return parsed > 0 ? parsed : 1;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const resolveDefaultHeadlessWorkers = (
|
|
35
|
+
command: HeadlessConcurrencyContext['command'],
|
|
36
|
+
numCpus = getNumCpus(),
|
|
37
|
+
): number => {
|
|
38
|
+
const baseWorkers = Math.max(
|
|
39
|
+
Math.min(DEFAULT_MAX_HEADLESS_WORKERS, numCpus - 1),
|
|
40
|
+
1,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return command === 'watch'
|
|
44
|
+
? Math.max(Math.floor(baseWorkers / 2), 1)
|
|
45
|
+
: baseWorkers;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const getHeadlessConcurrency = (
|
|
49
|
+
context: HeadlessConcurrencyContext,
|
|
50
|
+
totalTests: number,
|
|
51
|
+
): number => {
|
|
52
|
+
if (totalTests <= 0) {
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const maxWorkers = context.normalizedConfig.pool.maxWorkers;
|
|
57
|
+
if (maxWorkers !== undefined) {
|
|
58
|
+
return Math.min(parseWorkers(maxWorkers), totalTests);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return Math.min(resolveDefaultHeadlessWorkers(context.command), totalTests);
|
|
62
|
+
};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { Reporter } from '@rstest/core/browser';
|
|
2
|
+
import { HostDispatchRouter } from './dispatchRouter';
|
|
3
|
+
import type {
|
|
4
|
+
BrowserClientMessage,
|
|
5
|
+
BrowserDispatchHandler,
|
|
6
|
+
BrowserDispatchRequest,
|
|
7
|
+
SnapshotRpcRequest,
|
|
8
|
+
} from './protocol';
|
|
9
|
+
|
|
10
|
+
export type HostDispatchRouterOptions = ConstructorParameters<
|
|
11
|
+
typeof HostDispatchRouter
|
|
12
|
+
>[0];
|
|
13
|
+
|
|
14
|
+
type RunnerPayload<TType extends BrowserClientMessage['type']> =
|
|
15
|
+
Extract<BrowserClientMessage, { type: TType }> extends {
|
|
16
|
+
payload: infer TPayload;
|
|
17
|
+
}
|
|
18
|
+
? TPayload
|
|
19
|
+
: never;
|
|
20
|
+
|
|
21
|
+
type ReporterHookArg<THook extends keyof Reporter> = Parameters<
|
|
22
|
+
NonNullable<Reporter[THook]>
|
|
23
|
+
>[0];
|
|
24
|
+
|
|
25
|
+
type RunnerDispatchFileReadyPayload = ReporterHookArg<'onTestFileReady'>;
|
|
26
|
+
type RunnerDispatchSuiteStartPayload = ReporterHookArg<'onTestSuiteStart'>;
|
|
27
|
+
type RunnerDispatchSuiteResultPayload = ReporterHookArg<'onTestSuiteResult'>;
|
|
28
|
+
type RunnerDispatchCaseStartPayload = ReporterHookArg<'onTestCaseStart'>;
|
|
29
|
+
|
|
30
|
+
export type RunnerDispatchCallbacks = {
|
|
31
|
+
onTestFileStart: (payload: RunnerPayload<'file-start'>) => Promise<void>;
|
|
32
|
+
onTestFileReady: (payload: RunnerDispatchFileReadyPayload) => Promise<void>;
|
|
33
|
+
onTestSuiteStart: (payload: RunnerDispatchSuiteStartPayload) => Promise<void>;
|
|
34
|
+
onTestSuiteResult: (
|
|
35
|
+
payload: RunnerDispatchSuiteResultPayload,
|
|
36
|
+
) => Promise<void>;
|
|
37
|
+
onTestCaseStart: (payload: RunnerDispatchCaseStartPayload) => Promise<void>;
|
|
38
|
+
onTestCaseResult: (payload: RunnerPayload<'case-result'>) => Promise<void>;
|
|
39
|
+
onTestFileComplete: (
|
|
40
|
+
payload: RunnerPayload<'file-complete'>,
|
|
41
|
+
) => Promise<void>;
|
|
42
|
+
onLog: (payload: RunnerPayload<'log'>) => Promise<void>;
|
|
43
|
+
onFatal: (payload: RunnerPayload<'fatal'>) => Promise<void>;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type CreateHostDispatchRouterOptions = {
|
|
47
|
+
routerOptions?: HostDispatchRouterOptions;
|
|
48
|
+
runnerCallbacks: RunnerDispatchCallbacks;
|
|
49
|
+
runSnapshotRpc: (request: SnapshotRpcRequest) => Promise<unknown>;
|
|
50
|
+
extensionHandlers?: Map<string, BrowserDispatchHandler>;
|
|
51
|
+
onDuplicateNamespace?: (namespace: string) => void;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const toSnapshotRpcRequest = (
|
|
55
|
+
request: BrowserDispatchRequest,
|
|
56
|
+
): SnapshotRpcRequest | null => {
|
|
57
|
+
switch (request.method) {
|
|
58
|
+
case 'resolveSnapshotPath':
|
|
59
|
+
return {
|
|
60
|
+
id: request.requestId,
|
|
61
|
+
method: 'resolveSnapshotPath',
|
|
62
|
+
args: request.args as { testPath: string },
|
|
63
|
+
};
|
|
64
|
+
case 'readSnapshotFile':
|
|
65
|
+
return {
|
|
66
|
+
id: request.requestId,
|
|
67
|
+
method: 'readSnapshotFile',
|
|
68
|
+
args: request.args as { filepath: string },
|
|
69
|
+
};
|
|
70
|
+
case 'saveSnapshotFile':
|
|
71
|
+
return {
|
|
72
|
+
id: request.requestId,
|
|
73
|
+
method: 'saveSnapshotFile',
|
|
74
|
+
args: request.args as { filepath: string; content: string },
|
|
75
|
+
};
|
|
76
|
+
case 'removeSnapshotFile':
|
|
77
|
+
return {
|
|
78
|
+
id: request.requestId,
|
|
79
|
+
method: 'removeSnapshotFile',
|
|
80
|
+
args: request.args as { filepath: string },
|
|
81
|
+
};
|
|
82
|
+
default:
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const createHostDispatchRouter = ({
|
|
88
|
+
routerOptions,
|
|
89
|
+
runnerCallbacks,
|
|
90
|
+
runSnapshotRpc,
|
|
91
|
+
extensionHandlers,
|
|
92
|
+
onDuplicateNamespace,
|
|
93
|
+
}: CreateHostDispatchRouterOptions): HostDispatchRouter => {
|
|
94
|
+
const router = new HostDispatchRouter(routerOptions);
|
|
95
|
+
|
|
96
|
+
router.register('runner', async (request: BrowserDispatchRequest) => {
|
|
97
|
+
switch (request.method) {
|
|
98
|
+
case 'file-start':
|
|
99
|
+
await runnerCallbacks.onTestFileStart(
|
|
100
|
+
request.args as RunnerPayload<'file-start'>,
|
|
101
|
+
);
|
|
102
|
+
break;
|
|
103
|
+
case 'file-ready':
|
|
104
|
+
await runnerCallbacks.onTestFileReady(
|
|
105
|
+
request.args as RunnerDispatchFileReadyPayload,
|
|
106
|
+
);
|
|
107
|
+
break;
|
|
108
|
+
case 'suite-start':
|
|
109
|
+
await runnerCallbacks.onTestSuiteStart(
|
|
110
|
+
request.args as RunnerDispatchSuiteStartPayload,
|
|
111
|
+
);
|
|
112
|
+
break;
|
|
113
|
+
case 'suite-result':
|
|
114
|
+
await runnerCallbacks.onTestSuiteResult(
|
|
115
|
+
request.args as RunnerDispatchSuiteResultPayload,
|
|
116
|
+
);
|
|
117
|
+
break;
|
|
118
|
+
case 'case-start':
|
|
119
|
+
await runnerCallbacks.onTestCaseStart(
|
|
120
|
+
request.args as RunnerDispatchCaseStartPayload,
|
|
121
|
+
);
|
|
122
|
+
break;
|
|
123
|
+
case 'case-result':
|
|
124
|
+
await runnerCallbacks.onTestCaseResult(
|
|
125
|
+
request.args as RunnerPayload<'case-result'>,
|
|
126
|
+
);
|
|
127
|
+
break;
|
|
128
|
+
case 'file-complete':
|
|
129
|
+
await runnerCallbacks.onTestFileComplete(
|
|
130
|
+
request.args as RunnerPayload<'file-complete'>,
|
|
131
|
+
);
|
|
132
|
+
break;
|
|
133
|
+
case 'log':
|
|
134
|
+
await runnerCallbacks.onLog(request.args as RunnerPayload<'log'>);
|
|
135
|
+
break;
|
|
136
|
+
case 'fatal':
|
|
137
|
+
await runnerCallbacks.onFatal(request.args as RunnerPayload<'fatal'>);
|
|
138
|
+
break;
|
|
139
|
+
default:
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
router.register('snapshot', async (request: BrowserDispatchRequest) => {
|
|
145
|
+
const snapshotRequest = toSnapshotRpcRequest(request);
|
|
146
|
+
if (!snapshotRequest) {
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
return runSnapshotRpc(snapshotRequest);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
for (const [namespace, handler] of extensionHandlers ?? []) {
|
|
153
|
+
if (router.has(namespace)) {
|
|
154
|
+
onDuplicateNamespace?.(namespace);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
router.register(namespace, handler);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return router;
|
|
162
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BrowserDispatchRequest,
|
|
3
|
+
BrowserDispatchResponse,
|
|
4
|
+
} from './protocol';
|
|
5
|
+
|
|
6
|
+
type DispatchHandler = (request: BrowserDispatchRequest) => Promise<unknown>;
|
|
7
|
+
|
|
8
|
+
type HostDispatchRouterOptions = {
|
|
9
|
+
isRunTokenStale?: (runToken: number) => boolean;
|
|
10
|
+
onStale?: (request: BrowserDispatchRequest) => void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const toErrorMessage = (error: unknown): string => {
|
|
14
|
+
return error instanceof Error ? error.message : String(error);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Host-side routing layer for dispatch envelopes.
|
|
19
|
+
* Capabilities are registered by namespace, while routing stays transport-agnostic.
|
|
20
|
+
*/
|
|
21
|
+
export class HostDispatchRouter {
|
|
22
|
+
private handlers = new Map<string, DispatchHandler>();
|
|
23
|
+
private options: HostDispatchRouterOptions;
|
|
24
|
+
|
|
25
|
+
constructor(options?: HostDispatchRouterOptions) {
|
|
26
|
+
this.options = options ?? {};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
register(namespace: string, handler: DispatchHandler): void {
|
|
30
|
+
this.handlers.set(namespace, handler);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
unregister(namespace: string): void {
|
|
34
|
+
this.handlers.delete(namespace);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
has(namespace: string): boolean {
|
|
38
|
+
return this.handlers.has(namespace);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async dispatch(
|
|
42
|
+
request: BrowserDispatchRequest,
|
|
43
|
+
): Promise<BrowserDispatchResponse> {
|
|
44
|
+
const runToken = request.runToken;
|
|
45
|
+
if (
|
|
46
|
+
typeof runToken === 'number' &&
|
|
47
|
+
this.options.isRunTokenStale?.(runToken)
|
|
48
|
+
) {
|
|
49
|
+
// Return a stale marker so callers can drop old-run writes safely.
|
|
50
|
+
this.options.onStale?.(request);
|
|
51
|
+
return {
|
|
52
|
+
requestId: request.requestId,
|
|
53
|
+
runToken,
|
|
54
|
+
stale: true,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const handler = this.handlers.get(request.namespace);
|
|
59
|
+
if (!handler) {
|
|
60
|
+
return {
|
|
61
|
+
requestId: request.requestId,
|
|
62
|
+
runToken,
|
|
63
|
+
error: `No dispatch handler registered for namespace "${request.namespace}"`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const result = await handler(request);
|
|
69
|
+
return {
|
|
70
|
+
requestId: request.requestId,
|
|
71
|
+
runToken,
|
|
72
|
+
result,
|
|
73
|
+
};
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return {
|
|
76
|
+
requestId: request.requestId,
|
|
77
|
+
runToken,
|
|
78
|
+
error: toErrorMessage(error),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
package/src/env.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
BrowserClientMessage,
|
|
3
|
+
BrowserDispatchRequest,
|
|
4
|
+
BrowserHostConfig,
|
|
5
|
+
} from './protocol';
|
|
2
6
|
|
|
3
7
|
declare module '@rstest/browser-manifest' {
|
|
4
8
|
export type ManifestProjectConfig = {
|
|
@@ -35,6 +39,9 @@ declare global {
|
|
|
35
39
|
interface Window {
|
|
36
40
|
__RSTEST_BROWSER_OPTIONS__?: BrowserHostConfig;
|
|
37
41
|
__rstest_dispatch__?: (message: BrowserClientMessage) => void;
|
|
42
|
+
__rstest_dispatch_rpc__?: (
|
|
43
|
+
request: BrowserDispatchRequest,
|
|
44
|
+
) => Promise<unknown>;
|
|
38
45
|
__rstest_container_dispatch__?: (data: unknown) => void;
|
|
39
46
|
__rstest_container_on__?: (data: unknown) => void;
|
|
40
47
|
__RSTEST_DONE__?: boolean;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type HeadedSerialTask = () => Promise<void>;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Serializes headed browser file execution so only one task runs at a time.
|
|
5
|
+
* The queue keeps draining even if an earlier task rejects.
|
|
6
|
+
*/
|
|
7
|
+
export const createHeadedSerialTaskQueue = () => {
|
|
8
|
+
let queue: Promise<void> = Promise.resolve();
|
|
9
|
+
|
|
10
|
+
const enqueue = (task: HeadedSerialTask): Promise<void> => {
|
|
11
|
+
const next = queue.then(task);
|
|
12
|
+
queue = next.catch(() => {});
|
|
13
|
+
return next;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
enqueue,
|
|
18
|
+
};
|
|
19
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export type HeadlessLatestRerunScheduler<TFile> = {
|
|
2
|
+
enqueueLatest: (files: TFile[]) => Promise<void>;
|
|
3
|
+
whenIdle: () => Promise<void>;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
type HeadlessLatestRerunSchedulerOptions<TFile, TRun> = {
|
|
7
|
+
getActiveRun: () => TRun | null;
|
|
8
|
+
isRunCancelled: (run: TRun) => boolean;
|
|
9
|
+
invalidateActiveRun: () => void;
|
|
10
|
+
interruptActiveRun: (run: TRun) => Promise<void>;
|
|
11
|
+
runFiles: (files: TFile[]) => Promise<void>;
|
|
12
|
+
onError?: (error: unknown) => Promise<void> | void;
|
|
13
|
+
onInterrupt?: (run: TRun) => void;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Latest-only rerun scheduler for headless watch mode.
|
|
18
|
+
* Keeps only the newest pending payload and interrupts active run generations.
|
|
19
|
+
*/
|
|
20
|
+
export const createHeadlessLatestRerunScheduler = <TFile, TRun>(
|
|
21
|
+
options: HeadlessLatestRerunSchedulerOptions<TFile, TRun>,
|
|
22
|
+
): HeadlessLatestRerunScheduler<TFile> => {
|
|
23
|
+
let pendingFiles: TFile[] | null = null;
|
|
24
|
+
let draining: Promise<void> | null = null;
|
|
25
|
+
let latestEnqueueVersion = 0;
|
|
26
|
+
|
|
27
|
+
const runDrainLoop = async (): Promise<void> => {
|
|
28
|
+
while (pendingFiles) {
|
|
29
|
+
const nextFiles = pendingFiles;
|
|
30
|
+
pendingFiles = null;
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
await options.runFiles(nextFiles);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
try {
|
|
36
|
+
await options.onError?.(error);
|
|
37
|
+
} catch {
|
|
38
|
+
// Keep draining even if error reporting fails.
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const ensureDrainLoop = (): void => {
|
|
45
|
+
if (draining) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
draining = runDrainLoop().finally(() => {
|
|
50
|
+
draining = null;
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
async enqueueLatest(files: TFile[]): Promise<void> {
|
|
56
|
+
const enqueueVersion = ++latestEnqueueVersion;
|
|
57
|
+
const activeRun = options.getActiveRun();
|
|
58
|
+
if (activeRun && !options.isRunCancelled(activeRun)) {
|
|
59
|
+
options.onInterrupt?.(activeRun);
|
|
60
|
+
options.invalidateActiveRun();
|
|
61
|
+
await options.interruptActiveRun(activeRun);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// If a newer enqueue arrived while interrupting, drop this stale payload.
|
|
65
|
+
if (enqueueVersion !== latestEnqueueVersion) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
pendingFiles = files;
|
|
70
|
+
ensureDrainLoop();
|
|
71
|
+
},
|
|
72
|
+
async whenIdle(): Promise<void> {
|
|
73
|
+
await draining;
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BrowserClientMessage,
|
|
3
|
+
BrowserDispatchRequest,
|
|
4
|
+
BrowserDispatchResponse,
|
|
5
|
+
} from './protocol';
|
|
6
|
+
import { DISPATCH_MESSAGE_TYPE, DISPATCH_RPC_BRIDGE_NAME } from './protocol';
|
|
7
|
+
import type { BrowserProviderPage } from './providers';
|
|
8
|
+
|
|
9
|
+
type HeadlessRunnerTransportHandlers = {
|
|
10
|
+
onDispatchMessage: (message: BrowserClientMessage) => Promise<void>;
|
|
11
|
+
onDispatchRpc: (
|
|
12
|
+
request: BrowserDispatchRequest,
|
|
13
|
+
) => Promise<BrowserDispatchResponse>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Headless transport adapter.
|
|
18
|
+
* This only binds page bridge functions and delegates all scheduling decisions upstream.
|
|
19
|
+
*/
|
|
20
|
+
export const attachHeadlessRunnerTransport = async (
|
|
21
|
+
page: BrowserProviderPage,
|
|
22
|
+
handlers: HeadlessRunnerTransportHandlers,
|
|
23
|
+
): Promise<void> => {
|
|
24
|
+
// Fire-and-forget runner lifecycle messages (ready/log/result/fatal).
|
|
25
|
+
await page.exposeFunction(DISPATCH_MESSAGE_TYPE, handlers.onDispatchMessage);
|
|
26
|
+
// Request/response RPC bridge shared by snapshot and future namespaces.
|
|
27
|
+
await page.exposeFunction(DISPATCH_RPC_BRIDGE_NAME, handlers.onDispatchRpc);
|
|
28
|
+
};
|