ava 3.12.0 → 3.15.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/lib/run-status.js CHANGED
@@ -27,6 +27,7 @@ class RunStatus extends Emittery {
27
27
  passedKnownFailingTests: 0,
28
28
  passedTests: 0,
29
29
  selectedTests: 0,
30
+ sharedWorkerErrors: 0,
30
31
  skippedTests: 0,
31
32
  timeouts: 0,
32
33
  todoTests: 0,
@@ -93,6 +94,9 @@ class RunStatus extends Emittery {
93
94
  this.addPendingTest(event);
94
95
  }
95
96
 
97
+ break;
98
+ case 'shared-worker-error':
99
+ stats.sharedWorkerErrors++;
96
100
  break;
97
101
  case 'test-failed':
98
102
  stats.failedTests++;
@@ -164,6 +168,7 @@ class RunStatus extends Emittery {
164
168
  this.stats.failedHooks > 0 ||
165
169
  this.stats.failedTests > 0 ||
166
170
  this.stats.failedWorkers > 0 ||
171
+ this.stats.sharedWorkerErrors > 0 ||
167
172
  this.stats.timeouts > 0 ||
168
173
  this.stats.uncaughtExceptions > 0 ||
169
174
  this.stats.unhandledRejections > 0
package/lib/runner.js CHANGED
@@ -29,8 +29,11 @@ class Runner extends Emittery {
29
29
 
30
30
  this.activeRunnables = new Set();
31
31
  this.boundCompareTestSnapshot = this.compareTestSnapshot.bind(this);
32
+ this.skippedSnapshots = false;
33
+ this.boundSkipSnapshot = this.skipSnapshot.bind(this);
32
34
  this.interrupted = false;
33
35
  this.snapshots = null;
36
+ this.nextTaskIndex = 0;
34
37
  this.tasks = {
35
38
  after: [],
36
39
  afterAlways: [],
@@ -42,6 +45,7 @@ class Runner extends Emittery {
42
45
  serial: [],
43
46
  todo: []
44
47
  };
48
+ this.waitForReady = [];
45
49
 
46
50
  const uniqueTestTitles = new Set();
47
51
  this.registerUniqueTitle = title => {
@@ -75,6 +79,8 @@ class Runner extends Emittery {
75
79
  });
76
80
  }
77
81
 
82
+ metadata.taskIndex = this.nextTaskIndex++;
83
+
78
84
  const {args, buildTitle, implementations, rawTitle} = parseTestArgs(testArgs);
79
85
 
80
86
  if (this.checkSelectedByLineNumbers) {
@@ -195,8 +201,19 @@ class Runner extends Emittery {
195
201
  return this.snapshots.compare(options);
196
202
  }
197
203
 
204
+ skipSnapshot() {
205
+ this.skippedSnapshots = true;
206
+ }
207
+
198
208
  saveSnapshotState() {
199
- if (this.updateSnapshots && (this.runOnlyExclusive || this.skippingTests)) {
209
+ if (
210
+ this.updateSnapshots &&
211
+ (
212
+ this.runOnlyExclusive ||
213
+ this.skippingTests ||
214
+ this.skippedSnapshots
215
+ )
216
+ ) {
200
217
  return {cannotSave: true};
201
218
  }
202
219
 
@@ -205,9 +222,11 @@ class Runner extends Emittery {
205
222
  }
206
223
 
207
224
  if (this.updateSnapshots) {
208
- // TODO: There may be unused snapshot files if no test caused the
209
- // snapshots to be loaded. Prune them. But not if tests (including hooks!)
210
- // were skipped. Perhaps emit a warning if this occurs?
225
+ return {touchedFiles: snapshotManager.cleanSnapshots({
226
+ file: this.file,
227
+ fixedLocation: this.snapshotDir,
228
+ projectDir: this.projectDir
229
+ })};
211
230
  }
212
231
 
213
232
  return {};
@@ -284,7 +303,7 @@ class Runner extends Emittery {
284
303
  return result;
285
304
  }
286
305
 
287
- async runHooks(tasks, contextRef, titleSuffix, testPassed) {
306
+ async runHooks(tasks, contextRef, {titleSuffix, testPassed, associatedTaskIndex} = {}) {
288
307
  const hooks = tasks.map(task => new Runnable({
289
308
  contextRef,
290
309
  experiments: this.experiments,
@@ -293,8 +312,9 @@ class Runner extends Emittery {
293
312
  task.implementation :
294
313
  t => task.implementation.apply(null, [t].concat(task.args)),
295
314
  compareTestSnapshot: this.boundCompareTestSnapshot,
315
+ skipSnapshot: this.boundSkipSnapshot,
296
316
  updateSnapshots: this.updateSnapshots,
297
- metadata: task.metadata,
317
+ metadata: {...task.metadata, associatedTaskIndex},
298
318
  powerAssert: this.powerAssert,
299
319
  title: `${task.title}${titleSuffix || ''}`,
300
320
  isHook: true,
@@ -325,7 +345,14 @@ class Runner extends Emittery {
325
345
 
326
346
  async runTest(task, contextRef) {
327
347
  const hookSuffix = ` for ${task.title}`;
328
- let hooksOk = await this.runHooks(this.tasks.beforeEach, contextRef, hookSuffix);
348
+ let hooksOk = await this.runHooks(
349
+ this.tasks.beforeEach,
350
+ contextRef,
351
+ {
352
+ titleSuffix: hookSuffix,
353
+ associatedTaskIndex: task.metadata.taskIndex
354
+ }
355
+ );
329
356
 
330
357
  let testOk = false;
331
358
  if (hooksOk) {
@@ -338,6 +365,7 @@ class Runner extends Emittery {
338
365
  task.implementation :
339
366
  t => task.implementation.apply(null, [t].concat(task.args)),
340
367
  compareTestSnapshot: this.boundCompareTestSnapshot,
368
+ skipSnapshot: this.boundSkipSnapshot,
341
369
  updateSnapshots: this.updateSnapshots,
342
370
  metadata: task.metadata,
343
371
  powerAssert: this.powerAssert,
@@ -357,7 +385,14 @@ class Runner extends Emittery {
357
385
  logs: result.logs
358
386
  });
359
387
 
360
- hooksOk = await this.runHooks(this.tasks.afterEach, contextRef, hookSuffix, testOk);
388
+ hooksOk = await this.runHooks(
389
+ this.tasks.afterEach,
390
+ contextRef,
391
+ {
392
+ titleSuffix: hookSuffix,
393
+ testPassed: testOk,
394
+ associatedTaskIndex: task.metadata.taskIndex
395
+ });
361
396
  } else {
362
397
  this.emit('stateChange', {
363
398
  type: 'test-failed',
@@ -371,7 +406,14 @@ class Runner extends Emittery {
371
406
  }
372
407
  }
373
408
 
374
- const alwaysOk = await this.runHooks(this.tasks.afterEachAlways, contextRef, hookSuffix, testOk);
409
+ const alwaysOk = await this.runHooks(
410
+ this.tasks.afterEachAlways,
411
+ contextRef,
412
+ {
413
+ titleSuffix: hookSuffix,
414
+ testPassed: testOk,
415
+ associatedTaskIndex: task.metadata.taskIndex
416
+ });
375
417
  return alwaysOk && hooksOk && testOk;
376
418
  }
377
419
 
@@ -444,6 +486,8 @@ class Runner extends Emittery {
444
486
  });
445
487
  }
446
488
 
489
+ await Promise.all(this.waitForReady);
490
+
447
491
  if (concurrentTests.length === 0 && serialTests.length === 0) {
448
492
  this.emit('finish');
449
493
  // Don't run any hooks if there are no tests to run.
@@ -104,13 +104,32 @@ function combineEntries(entries) {
104
104
  const buffers = [];
105
105
  let byteLength = 0;
106
106
 
107
- const sortedKeys = [...entries.keys()].sort();
107
+ const sortedKeys = [...entries.keys()].sort((keyA, keyB) => {
108
+ const [a, b] = [entries.get(keyA), entries.get(keyB)];
109
+ const taskDifference = a.taskIndex - b.taskIndex;
110
+
111
+ if (taskDifference !== 0) {
112
+ return taskDifference;
113
+ }
114
+
115
+ const [assocA, assocB] = [a.associatedTaskIndex, b.associatedTaskIndex];
116
+ if (assocA !== undefined && assocB !== undefined) {
117
+ const assocDifference = assocA - assocB;
118
+
119
+ if (assocDifference !== 0) {
120
+ return assocDifference;
121
+ }
122
+ }
123
+
124
+ return a.snapIndex - b.snapIndex;
125
+ });
126
+
108
127
  for (const key of sortedKeys) {
109
128
  const keyBuffer = Buffer.from(`\n\n## ${key}\n\n`, 'utf8');
110
129
  buffers.push(keyBuffer);
111
130
  byteLength += keyBuffer.byteLength;
112
131
 
113
- const formattedEntries = entries.get(key);
132
+ const formattedEntries = entries.get(key).buffers;
114
133
  const last = formattedEntries[formattedEntries.length - 1];
115
134
  for (const entry of formattedEntries) {
116
135
  buffers.push(entry);
@@ -176,10 +195,11 @@ function encodeSnapshots(buffersByHash) {
176
195
  byteOffset += 2;
177
196
 
178
197
  const entries = [];
179
- for (const pair of buffersByHash) {
180
- const hash = pair[0];
181
- const snapshotBuffers = pair[1];
182
-
198
+ // Maps can't have duplicate keys, so all items in [...buffersByHash.keys()]
199
+ // are unique, so sortedHashes should be deterministic.
200
+ const sortedHashes = [...buffersByHash.keys()].sort();
201
+ const sortedBuffersByHash = [...sortedHashes.map(hash => [hash, buffersByHash.get(hash)])];
202
+ for (const [hash, snapshotBuffers] of sortedBuffersByHash) {
183
203
  buffers.push(Buffer.from(hash, 'hex'));
184
204
  byteOffset += MD5_HASH_LENGTH;
185
205
 
@@ -332,6 +352,7 @@ class Manager {
332
352
  const descriptor = concordance.describe(options.expected, concordanceOptions);
333
353
  const snapshot = concordance.serialize(descriptor);
334
354
  const entry = formatEntry(options.label, descriptor);
355
+ const {taskIndex, snapIndex, associatedTaskIndex} = options;
335
356
 
336
357
  return () => { // Must be called in order!
337
358
  this.hasChanges = true;
@@ -353,9 +374,9 @@ class Manager {
353
374
  snapshots.push(snapshot);
354
375
 
355
376
  if (this.reportEntries.has(options.belongsTo)) {
356
- this.reportEntries.get(options.belongsTo).push(entry);
377
+ this.reportEntries.get(options.belongsTo).buffers.push(entry);
357
378
  } else {
358
- this.reportEntries.set(options.belongsTo, [entry]);
379
+ this.reportEntries.set(options.belongsTo, {buffers: [entry], taskIndex, snapIndex, associatedTaskIndex});
359
380
  }
360
381
  };
361
382
  }
@@ -428,12 +449,49 @@ const determineSnapshotDir = mem(({file, fixedLocation, projectDir}) => {
428
449
 
429
450
  exports.determineSnapshotDir = determineSnapshotDir;
430
451
 
431
- function load({file, fixedLocation, projectDir, recordNewSnapshots, updating}) {
452
+ function determineSnapshotPaths({file, fixedLocation, projectDir}) {
432
453
  const dir = determineSnapshotDir({file, fixedLocation, projectDir});
433
454
  const relFile = path.relative(projectDir, resolveSourceFile(file));
434
455
  const name = path.basename(relFile);
435
456
  const reportFile = `${name}.md`;
436
457
  const snapFile = `${name}.snap`;
458
+
459
+ return {
460
+ dir,
461
+ relFile,
462
+ snapFile,
463
+ reportFile
464
+ };
465
+ }
466
+
467
+ function cleanFile(file) {
468
+ try {
469
+ fs.unlinkSync(file);
470
+ return [file];
471
+ } catch (error) {
472
+ if (error.code === 'ENOENT') {
473
+ return [];
474
+ }
475
+
476
+ throw error;
477
+ }
478
+ }
479
+
480
+ // Remove snapshot and report if they exist. Returns an array containing the
481
+ // paths of the touched files.
482
+ function cleanSnapshots({file, fixedLocation, projectDir}) {
483
+ const {dir, snapFile, reportFile} = determineSnapshotPaths({file, fixedLocation, projectDir});
484
+
485
+ return [
486
+ ...cleanFile(path.join(dir, snapFile)),
487
+ ...cleanFile(path.join(dir, reportFile))
488
+ ];
489
+ }
490
+
491
+ exports.cleanSnapshots = cleanSnapshots;
492
+
493
+ function load({file, fixedLocation, projectDir, recordNewSnapshots, updating}) {
494
+ const {dir, relFile, snapFile, reportFile} = determineSnapshotPaths({file, fixedLocation, projectDir});
437
495
  const snapPath = path.join(dir, snapFile);
438
496
 
439
497
  let appendOnly = !updating;
package/lib/test.js CHANGED
@@ -187,7 +187,8 @@ class ExecutionContext extends assert.Assertions {
187
187
  }
188
188
 
189
189
  get passed() {
190
- return testMap.get(this).testPassed;
190
+ const test = testMap.get(this);
191
+ return test.isHook ? test.testPassed : !test.assertError;
191
192
  }
192
193
 
193
194
  _throwsArgStart(assertion, file, line) {
@@ -229,7 +230,17 @@ class Test {
229
230
  const index = id ? 0 : this.nextSnapshotIndex++;
230
231
  const label = id ? '' : message || `Snapshot ${index + 1}`; // Human-readable labels start counting at 1.
231
232
 
232
- const {record, ...result} = options.compareTestSnapshot({belongsTo, deferRecording, expected, index, label});
233
+ const {taskIndex, associatedTaskIndex} = this.metadata;
234
+ const {record, ...result} = options.compareTestSnapshot({
235
+ belongsTo,
236
+ deferRecording,
237
+ expected,
238
+ index,
239
+ label,
240
+ taskIndex,
241
+ snapIndex: this.snapshotCount,
242
+ associatedTaskIndex
243
+ });
233
244
  if (record) {
234
245
  this.deferredSnapshotRecordings.push(record);
235
246
  }
@@ -238,6 +249,10 @@ class Test {
238
249
  };
239
250
 
240
251
  this.skipSnapshot = () => {
252
+ if (typeof options.skipSnapshot === 'function') {
253
+ options.skipSnapshot();
254
+ }
255
+
241
256
  if (options.updateSnapshots) {
242
257
  this.addFailedAssertion(new Error('Snapshot assertions cannot be skipped when updating snapshots'));
243
258
  } else {
@@ -297,11 +312,8 @@ class Test {
297
312
  };
298
313
  }
299
314
 
300
- if (this.metadata.inline) {
301
- throw new Error('`t.end()` is not supported inside `t.try()`');
302
- } else {
303
- throw new Error('`t.end()` is not supported in this context. To use `t.end()` as a callback, you must use "callback mode" via `test.cb(testName, fn)`');
304
- }
315
+ const error_ = this.metadata.inline ? new Error('`t.end()` is not supported inside `t.try()`') : new Error('`t.end()` is not supported in this context. To use `t.end()` as a callback, you must use "callback mode" via `test.cb(testName, fn)`');
316
+ throw error_;
305
317
  }
306
318
 
307
319
  endCallback(error, savedError) {
@@ -735,11 +747,7 @@ class Test {
735
747
  if (this.metadata.failing) {
736
748
  passed = !passed;
737
749
 
738
- if (passed) {
739
- error = null;
740
- } else {
741
- error = new Error('Test was expected to fail, but succeeded, you should stop marking the test as failing');
742
- }
750
+ error = passed ? null : new Error('Test was expected to fail, but succeeded, you should stop marking the test as failing');
743
751
  }
744
752
 
745
753
  return {
package/lib/worker/ipc.js CHANGED
@@ -1,30 +1,13 @@
1
1
  'use strict';
2
- const Emittery = require('emittery');
2
+ const events = require('events');
3
+ const pEvent = require('p-event');
3
4
  const {controlFlow} = require('../ipc-flow-control');
5
+ const {get: getOptions} = require('./options');
4
6
 
5
- const emitter = new Emittery();
6
- process.on('message', message => {
7
- if (!message.ava) {
8
- return;
9
- }
10
-
11
- switch (message.ava.type) {
12
- case 'options':
13
- emitter.emit('options', message.ava.options);
14
- break;
15
- case 'peer-failed':
16
- emitter.emit('peerFailed');
17
- break;
18
- case 'pong':
19
- emitter.emit('pong');
20
- break;
21
- default:
22
- break;
23
- }
24
- });
7
+ const selectAvaMessage = type => message => message.ava && message.ava.type === type;
25
8
 
26
- exports.options = emitter.once('options');
27
- exports.peerFailed = emitter.once('peerFailed');
9
+ exports.options = pEvent(process, 'message', selectAvaMessage('options')).then(message => message.ava.options);
10
+ exports.peerFailed = pEvent(process, 'message', selectAvaMessage('peer-failed'));
28
11
 
29
12
  const bufferedSend = controlFlow(process);
30
13
  function send(evt) {
@@ -33,18 +16,27 @@ function send(evt) {
33
16
 
34
17
  exports.send = send;
35
18
 
19
+ let refs = 1;
20
+ function ref() {
21
+ if (++refs === 1) {
22
+ process.channel.ref();
23
+ }
24
+ }
25
+
36
26
  function unref() {
37
- process.channel.unref();
27
+ if (refs > 0 && --refs === 0) {
28
+ process.channel.unref();
29
+ }
38
30
  }
39
31
 
40
32
  exports.unref = unref;
41
33
 
42
34
  let pendingPings = Promise.resolve();
43
35
  async function flush() {
44
- process.channel.ref();
36
+ ref();
45
37
  const promise = pendingPings.then(async () => { // eslint-disable-line promise/prefer-await-to-then
46
38
  send({type: 'ping'});
47
- await emitter.once('pong');
39
+ await pEvent(process, 'message', selectAvaMessage('pong'));
48
40
  if (promise === pendingPings) {
49
41
  unref();
50
42
  }
@@ -54,3 +46,156 @@ async function flush() {
54
46
  }
55
47
 
56
48
  exports.flush = flush;
49
+
50
+ let channelCounter = 0;
51
+ let messageCounter = 0;
52
+
53
+ const channelEmitters = new Map();
54
+ function createChannelEmitter(channelId) {
55
+ if (channelEmitters.size === 0) {
56
+ process.on('message', message => {
57
+ if (!message.ava) {
58
+ return;
59
+ }
60
+
61
+ const {channelId, type, ...payload} = message.ava;
62
+ if (
63
+ type === 'shared-worker-error' ||
64
+ type === 'shared-worker-message' ||
65
+ type === 'shared-worker-ready'
66
+ ) {
67
+ const emitter = channelEmitters.get(channelId);
68
+ if (emitter !== undefined) {
69
+ emitter.emit(type, payload);
70
+ }
71
+ }
72
+ });
73
+ }
74
+
75
+ const emitter = new events.EventEmitter();
76
+ channelEmitters.set(channelId, emitter);
77
+ return [emitter, () => channelEmitters.delete(channelId)];
78
+ }
79
+
80
+ function registerSharedWorker(filename, initialData) {
81
+ const channelId = `${getOptions().forkId}/channel/${++channelCounter}`;
82
+ const [channelEmitter, unsubscribe] = createChannelEmitter(channelId);
83
+
84
+ let forcedUnref = false;
85
+ let refs = 0;
86
+ const forceUnref = () => {
87
+ if (forcedUnref) {
88
+ return;
89
+ }
90
+
91
+ forcedUnref = true;
92
+ if (refs > 0) {
93
+ unref();
94
+ }
95
+ };
96
+
97
+ const refChannel = () => {
98
+ if (!forcedUnref && ++refs === 1) {
99
+ ref();
100
+ }
101
+ };
102
+
103
+ const unrefChannel = () => {
104
+ if (!forcedUnref && refs > 0 && --refs === 0) {
105
+ unref();
106
+ }
107
+ };
108
+
109
+ send({
110
+ type: 'shared-worker-connect',
111
+ channelId,
112
+ filename,
113
+ initialData
114
+ });
115
+
116
+ let currentlyAvailable = false;
117
+ let error = null;
118
+
119
+ refChannel();
120
+ const ready = pEvent(channelEmitter, 'shared-worker-ready').then(() => { // eslint-disable-line promise/prefer-await-to-then
121
+ currentlyAvailable = error === null;
122
+ }).finally(unrefChannel);
123
+
124
+ const messageEmitters = new Set();
125
+ const handleMessage = message => {
126
+ // Wait for a turn of the event loop, to allow new subscriptions to be set
127
+ // up in response to the previous message.
128
+ setImmediate(() => {
129
+ for (const emitter of messageEmitters) {
130
+ emitter.emit('message', message);
131
+ }
132
+ });
133
+ };
134
+
135
+ channelEmitter.on('shared-worker-message', handleMessage);
136
+
137
+ pEvent(channelEmitter, 'shared-worker-error').then(() => { // eslint-disable-line promise/prefer-await-to-then
138
+ unsubscribe();
139
+ forceUnref();
140
+
141
+ error = new Error('The shared worker is no longer available');
142
+ currentlyAvailable = false;
143
+ for (const emitter of messageEmitters) {
144
+ emitter.emit('error', error);
145
+ }
146
+ });
147
+
148
+ return {
149
+ forceUnref,
150
+ ready,
151
+ channel: {
152
+ available: ready,
153
+
154
+ get currentlyAvailable() {
155
+ return currentlyAvailable;
156
+ },
157
+
158
+ async * receive() {
159
+ if (error !== null) {
160
+ throw error;
161
+ }
162
+
163
+ const emitter = new events.EventEmitter();
164
+ messageEmitters.add(emitter);
165
+ try {
166
+ refChannel();
167
+ for await (const [message] of events.on(emitter, 'message')) {
168
+ yield message;
169
+ }
170
+ } finally {
171
+ unrefChannel();
172
+ messageEmitters.delete(emitter);
173
+ }
174
+ },
175
+
176
+ post(serializedData, replyTo) {
177
+ if (error !== null) {
178
+ throw error;
179
+ }
180
+
181
+ if (!currentlyAvailable) {
182
+ throw new Error('Shared worker is not yet available');
183
+ }
184
+
185
+ const messageId = `${channelId}/message/${++messageCounter}`;
186
+ send({
187
+ type: 'shared-worker-message',
188
+ channelId,
189
+ messageId,
190
+ replyTo,
191
+ serializedData
192
+ });
193
+
194
+ return messageId;
195
+ }
196
+ }
197
+ };
198
+ }
199
+
200
+ exports.registerSharedWorker = registerSharedWorker;
201
+
@@ -0,0 +1,121 @@
1
+ const v8 = require('v8');
2
+ const pkg = require('../../package.json');
3
+ const subprocess = require('./subprocess');
4
+ const options = require('./options');
5
+
6
+ const workers = new Map();
7
+ const workerTeardownFns = new WeakMap();
8
+
9
+ function createSharedWorker(filename, initialData, teardown) {
10
+ const channel = subprocess.registerSharedWorker(filename, initialData, teardown);
11
+
12
+ class ReceivedMessage {
13
+ constructor(id, serializedData) {
14
+ this.id = id;
15
+ this.data = v8.deserialize(new Uint8Array(serializedData));
16
+ }
17
+
18
+ reply(data) {
19
+ return publishMessage(data, this.id);
20
+ }
21
+ }
22
+
23
+ // Ensure that, no matter how often it's received, we have a stable message
24
+ // object.
25
+ const messageCache = new WeakMap();
26
+ async function * receiveMessages(replyTo) {
27
+ for await (const evt of channel.receive()) {
28
+ if (replyTo === undefined && evt.replyTo !== undefined) {
29
+ continue;
30
+ }
31
+
32
+ if (replyTo !== undefined && evt.replyTo !== replyTo) {
33
+ continue;
34
+ }
35
+
36
+ let message = messageCache.get(evt);
37
+ if (message === undefined) {
38
+ message = new ReceivedMessage(evt.messageId, evt.serializedData);
39
+ messageCache.set(evt, message);
40
+ }
41
+
42
+ yield message;
43
+ }
44
+ }
45
+
46
+ function publishMessage(data, replyTo) {
47
+ const id = channel.post([...v8.serialize(data)], replyTo);
48
+
49
+ return {
50
+ id,
51
+ async * replies() {
52
+ yield * receiveMessages(id);
53
+ }
54
+ };
55
+ }
56
+
57
+ return {
58
+ available: channel.available,
59
+ protocol: 'experimental',
60
+
61
+ get currentlyAvailable() {
62
+ return channel.currentlyAvailable;
63
+ },
64
+
65
+ publish(data) {
66
+ return publishMessage(data);
67
+ },
68
+
69
+ async * subscribe() {
70
+ yield * receiveMessages();
71
+ }
72
+ };
73
+ }
74
+
75
+ const supportsSharedWorkers = process.versions.node >= '12.17.0';
76
+
77
+ function registerSharedWorker({
78
+ filename,
79
+ initialData,
80
+ supportedProtocols,
81
+ teardown
82
+ }) {
83
+ if (!options.get().experiments.sharedWorkers) {
84
+ throw new Error('Shared workers are experimental. Opt in to them in your AVA configuration');
85
+ }
86
+
87
+ if (!supportsSharedWorkers) {
88
+ throw new Error('Shared workers require Node.js 12.17 or newer');
89
+ }
90
+
91
+ if (!supportedProtocols.includes('experimental')) {
92
+ throw new Error(`This version of AVA (${pkg.version}) does not support any of the desired shared worker protocols: ${supportedProtocols.join()}`);
93
+ }
94
+
95
+ let worker = workers.get(filename);
96
+ if (worker === undefined) {
97
+ worker = createSharedWorker(filename, initialData, async () => {
98
+ // Run possibly asynchronous teardown functions serially, in reverse
99
+ // order. Any error will crash the worker.
100
+ const teardownFns = workerTeardownFns.get(worker);
101
+ if (teardownFns !== undefined) {
102
+ for await (const fn of [...teardownFns].reverse()) {
103
+ await fn();
104
+ }
105
+ }
106
+ });
107
+ workers.set(filename, worker);
108
+ }
109
+
110
+ if (teardown !== undefined) {
111
+ if (workerTeardownFns.has(worker)) {
112
+ workerTeardownFns.get(worker).push(teardown);
113
+ } else {
114
+ workerTeardownFns.set(worker, [teardown]);
115
+ }
116
+ }
117
+
118
+ return worker;
119
+ }
120
+
121
+ exports.registerSharedWorker = registerSharedWorker;