Skip to content

Commit ad521af

Browse files
committed
Graduate shared workers to be non-experimental
1 parent 0edfd00 commit ad521af

File tree

36 files changed

+122
-193
lines changed

36 files changed

+122
-193
lines changed

docs/recipes/shared-workers.md

+41-41
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# Extending AVA using shared workers
22

3-
Shared workers are a new, powerful (and experimental) AVA feature. A program can be loaded in a [worker thread](https://nodejs.org/docs/latest/api/worker_threads.html) in AVA's main process and then communicate with code running in the test workers. This enables your tests to better utilize shared resources during a test run, as well as providing opportunities to set up these resources before tests start (or clean them up after).
3+
Shared workers are a powerful AVA feature. A program can be loaded in a [worker thread](https://nodejs.org/docs/latest/api/worker_threads.html) in AVA's main process and then communicate with code running in the test workers. This enables your tests to better utilize shared resources during a test run, as well as providing opportunities to set up these resources before tests start (or clean them up after).
44

55
When you use watch mode, shared workers remain loaded across runs.
66

7-
## Enabling the experiment
7+
## Enabling the experiment (only needed with AVA 3)
88

99
Shared workers are available when you use AVA with Node.js 12.17.0 or newer. AVA 3.13.0 or newer is required. It is an experimental feature so you need to enable it in your AVA configuration:
1010

@@ -31,20 +31,20 @@ Here we'll discuss building low-level plugins.
3131

3232
### Registering a shared worker
3333

34-
Plugins are registered inside test workers. They'll provide the path for the main program, which AVA will load in a [worker thread](https://nodejs.org/docs/latest/api/worker_threads.html) in its main process. For each unique path one worker thread is started.
34+
Plugins are registered inside test workers. They'll provide the path for the shared worker, which AVA will load in a [worker thread](https://nodejs.org/docs/latest/api/worker_threads.html) in its main process. For each unique path one worker thread is started.
3535

36-
Plugins communicate with their main program using a *protocol*. Protocols are versioned independently from AVA itself. This allows us to make improvements without breaking existing plugins. Protocols are only removed in major AVA releases.
36+
Plugins communicate with their shared worker using a *protocol*. Protocols are versioned independently from AVA itself. This allows us to make improvements without breaking existing plugins. Protocols are only removed in major AVA releases.
3737

3838
Plugins can be compatible with multiple protocols. AVA will select the best protocol it supports. If AVA does not support any of the specified protocols it'll throw an error. The selected protocol is available on the returned worker object.
3939

40-
**While shared workers are experimental, there is only an unversioned *experimental* protocol. Breaking changes may occur with any AVA release.**
40+
**For AVA 3, substitute `'ava4'` with `'experimental'`.**
4141

4242
```js
43-
const {registerSharedWorker} = require('ava/plugin');
43+
import {registerSharedWorker} from 'ava/plugin';
4444

4545
const shared = registerSharedWorker({
4646
filename: path.resolve(__dirname, 'worker.js'),
47-
supportedProtocols: ['experimental']
47+
supportedProtocols: ['ava4']
4848
});
4949
```
5050

@@ -55,61 +55,61 @@ You can supply a `teardown()` function which will be called after all tests have
5555
```js
5656
const worker = registerSharedWorker({
5757
filename: path.resolve(__dirname, 'worker.js'),
58-
supportedProtocols: ['experimental'],
58+
supportedProtocols: ['ava4'],
5959
teardown () {
6060
// Perform any clean-up within the test process itself.
6161
}
6262
});
6363
```
6464

65-
You can also provide some data passed to the main program when it is loaded. Of course, it is only loaded once, so this is only useful in limited circumstances:
65+
You can also provide some data passed to the shared worker when it is loaded. Of course, it is only loaded once, so this is only useful in limited circumstances:
6666

6767
```js
6868
const shared = registerSharedWorker({
6969
filename: path.resolve(__dirname, 'worker.js'),
7070
initialData: {hello: 'world'},
71-
supportedProtocols: ['experimental']
71+
supportedProtocols: ['ava4']
7272
});
7373
```
7474

75-
On this `shared` object, `protocol` is set to the selected protocol. Since the main program is loaded asynchronously, `available` provides a promise that fulfils when the main program first becomes available. `currentlyAvailable` reflects whether the worker is, well, currently available.
75+
On this `shared` object, `protocol` is set to the selected protocol. Since the shared worker is loaded asynchronously, `available` provides a promise that fulfils when the shared worker first becomes available. `currentlyAvailable` reflects whether the worker is, well, currently available.
7676

7777
There are two more methods available on the `shared` object, which we'll get to soon.
7878

79-
#### Initializing the main program
79+
#### Initializing the shared worker
8080

81-
AVA loads the main program (as identified through the `filename` option) in a worker thread. The program must export a factory method. For CJS programs this can be done by assigning `module.exports` or `exports.default`. For ESM programs you must use `export default`. If the `filename` to an ESM program is an absolute path it must be specified using the `file:` protocol.
81+
AVA loads the shared worker (as identified through the `filename` option) in a worker thread. This must be an ES module file with a default export. The filename must be an absolute path using the `file:` protocol or a `URL` instance.
8282

83-
Like when calling `registerSharedWorker()`, the factory method must negotiate a protocol:
83+
The default export must be a factory method. Like when calling `registerSharedWorker()`, it must negotiate a protocol:
8484

8585
```js
86-
exports.default = ({negotiateProtocol}) => {
87-
const main = negotiateProtocol(['experimental']);
88-
};
86+
export default ({negotiateProtocol}) => {
87+
const main = negotiateProtocol(['ava4']);
88+
}
8989
```
9090

9191
On this `main` object, `protocol` is set to the selected protocol. `initialData` holds the data provided when the worker was first registered.
9292

93-
When you're done initializing the main program you must call `main.ready()`. This makes the worker available in test workers. You can call `main.ready()` asynchronously.
93+
When you're done initializing the shared worker you must call `main.ready()`. This makes the worker available in test workers. You can call `main.ready()` asynchronously.
9494

9595
Any errors thrown by the factory method will crash the worker thread and make the worker unavailable in test workers. The same goes for unhandled rejections. The factory method may return a promise.
9696

97-
### Communicating between test workers and the worker thread
97+
### Communicating between test workers and the shared worker
9898

99-
AVA's low-level shared worker infrastructure is primarily about communication. You can send messages from test workers to the shared worker thread, and the other way around. Higher-level logic can be implemented on top of this message passing infrastructure.
99+
AVA's low-level shared worker infrastructure is primarily about communication. You can send messages from test workers to the shared worker, and the other way around. Higher-level logic can be implemented on top of this message passing infrastructure.
100100

101101
Message data is serialized using the [V8 Serialization API](https://nodejs.org/docs/latest-v12.x/api/v8.html#v8_serialization_api). Please read up on some [important limitations](https://nodejs.org/docs/latest-v12.x/api/worker_threads.html#worker_threads_port_postmessage_value_transferlist).
102102

103-
In the main program you can subscribe to messages from test workers:
103+
In the shared worker you can subscribe to messages from test workers:
104104

105105
```js
106-
exports.default = async ({negotiateProtocol}) => {
107-
const main = negotiateProtocol(['experimental']).ready();
106+
export default async ({negotiateProtocol}) => {
107+
const main = negotiateProtocol(['ava4']).ready();
108108

109109
for await (const message of main.subscribe()) {
110110
//
111111
}
112-
};
112+
}
113113
```
114114

115115
Messages have IDs that are unique for the main AVA process. Across AVA runs you may see the same ID. Access the ID using the `id` property.
@@ -121,8 +121,8 @@ You can reply to a received message by calling `reply()`. This publishes a messa
121121
To illustrate this here's a "game" of Marco Polo:
122122

123123
```js
124-
exports.default = ({negotiateProtocol}) => {
125-
const main = negotiateProtocol(['experimental']).ready();
124+
export default ({negotiateProtocol}) => {
125+
const main = negotiateProtocol(['ava4']).ready();
126126

127127
play(main.subscribe());
128128
};
@@ -134,23 +134,23 @@ const play = async (messages) => {
134134
play(response.replies());
135135
}
136136
}
137-
};
137+
}
138138
```
139139

140140
(Of course this sets up many reply listeners which is rather inefficient.)
141141

142142
You can also broadcast messages to all connected test workers:
143143

144144
```js
145-
exports.default = async ({negotiateProtocol}) => {
146-
const main = negotiateProtocol(['experimental']).ready();
145+
export default async ({negotiateProtocol}) => {
146+
const main = negotiateProtocol(['ava4']).ready();
147147

148148
for await (const message of main.subscribe()) {
149149
if (message.data === 'Bingo!') {
150150
main.broadcast('Bingo!');
151151
}
152152
}
153-
};
153+
}
154154
```
155155

156156
Like with `reply()`, `broadcast()` returns a published message which can receive replies. Call `replies()` to get an asynchronous iterator for reply messages.
@@ -162,13 +162,13 @@ These test workers have a unique ID (which, like message IDs, is unique for the
162162
Of course you don't need to wait for a message *from* a test worker to access this object. Use `main.testWorkers()` to get an asynchronous iterator which produces each newly connected test worker:
163163

164164
```js
165-
exports.default = async ({negotiateProtocol}) => {
166-
const main = negotiateProtocol(['experimental']).ready();
165+
export default async ({negotiateProtocol}) => {
166+
const main = negotiateProtocol(['ava4']).ready();
167167

168168
for await (const testWorker of main.testWorkers()) {
169169
main.broadcast(`New test file: ${testWorker.file}`);
170170
}
171-
};
171+
}
172172
```
173173

174174
Within test workers, once the shared worker is available, you can publish messages:
@@ -197,31 +197,31 @@ Messages are always produced in their own turn of the event loop. This means you
197197

198198
### Cleaning up resources
199199

200-
Test workers come and go while the worker thread remains. It's therefore important to clean up resources.
200+
Test workers come and go while the shared worker remains. It's therefore important to clean up resources.
201201

202202
Messages are subscribed to using async iterators. These return when the test worker exits.
203203

204204
You can register teardown functions to be run when the test worker exits:
205205

206206
```js
207-
exports.default = async ({negotiateProtocol}) => {
208-
const main = negotiateProtocol(['experimental']).ready();
207+
export default async ({negotiateProtocol}) => {
208+
const main = negotiateProtocol(['ava4']).ready();
209209

210210
for await (const testWorker of main.testWorkers()) {
211211
testWorker.teardown(() => {
212212
// Bye bye…
213213
});
214214
}
215-
};
215+
}
216216
```
217217

218218
The most recently registered function is called first, and so forth. Functions execute sequentially.
219219

220220
More interestingly, a wrapped teardown function is returned so that you can call it manually. AVA still ensures the function only runs once.
221221

222222
```js
223-
exports.default = ({negotiateProtocol}) => {
224-
const main = negotiateProtocol(['experimental']).ready();
223+
export default ({negotiateProtocol}) => {
224+
const main = negotiateProtocol(['ava4']).ready();
225225

226226
for await (const worker of testWorkers) {
227227
counters.set(worker, 0);
@@ -231,7 +231,7 @@ exports.default = ({negotiateProtocol}) => {
231231

232232
waitForTen(worker.subscribe(), teardown);
233233
}
234-
};
234+
}
235235

236236
const counters = new WeakMap();
237237

@@ -255,4 +255,4 @@ Not sure what to build? Previously folks have expressed a desire for mutexes, ma
255255

256256
We could also extend the shared worker implementation in AVA itself. Perhaps so you can run code before a new test run, even with watch mode. Or so you can initialize a shared worker based on the AVA configuration, not when a test file runs.
257257

258-
Please [comment here](https://github.com/avajs/ava/issues/2605) with ideas, questions and feedback.
258+
Please [comment here](https://github.com/avajs/ava/discussions/2703) with ideas, questions and feedback.

lib/load-config.js

+1-3
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@ import {packageConfig, packageJsonPath} from 'pkg-conf';
88

99
const NO_SUCH_FILE = Symbol('no ava.config.js file');
1010
const MISSING_DEFAULT_EXPORT = Symbol('missing default export');
11-
const EXPERIMENTS = new Set([
12-
'sharedWorkers',
13-
]);
11+
const EXPERIMENTS = new Set();
1412

1513
const importConfig = async ({configFile, fileForErrorMessage}) => {
1614
const {default: config = MISSING_DEFAULT_EXPORT} = await import(url.pathToFileURL(configFile)); // eslint-disable-line node/no-unsupported-features/es-syntax

lib/plugin-support/shared-worker-loader.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ loadFactory(workerData.filename).then(factory => {
173173

174174
factory({
175175
negotiateProtocol(supported) {
176-
if (!supported.includes('experimental')) {
176+
if (!supported.includes('ava4')) {
177177
fatal = new Error(`This version of AVA (${pkg.version}) is not compatible with shared worker plugin at ${workerData.filename}`);
178178
throw fatal;
179179
}
@@ -213,7 +213,7 @@ loadFactory(workerData.filename).then(factory => {
213213

214214
return {
215215
initialData: workerData.initialData,
216-
protocol: 'experimental',
216+
protocol: 'ava4',
217217

218218
ready() {
219219
signalAvailable();

lib/worker/plugin.cjs

+2-5
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ function createSharedWorker(filename, initialData, teardown) {
6767

6868
return {
6969
available: channel.available,
70-
protocol: 'experimental',
70+
protocol: 'ava4',
7171

7272
get currentlyAvailable() {
7373
return channel.currentlyAvailable;
@@ -90,15 +90,12 @@ function registerSharedWorker({
9090
teardown,
9191
}) {
9292
const options_ = options.get();
93-
if (!options_.experiments.sharedWorkers) {
94-
throw new Error('Shared workers are experimental. Opt in to them in your AVA configuration');
95-
}
9693

9794
if (!options_.workerThreads) {
9895
throw new Error('Shared workers can be used only when worker threads are enabled');
9996
}
10097

101-
if (!supportedProtocols.includes('experimental')) {
98+
if (!supportedProtocols.includes('ava4')) {
10299
throw new Error(`This version of AVA (${pkg.version}) does not support any of the desired shared worker protocols: ${supportedProtocols.join(',')}`);
103100
}
104101

0 commit comments

Comments
 (0)