@jsenv/test 1.7.9 → 1.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/test",
3
- "version": "1.7.9",
3
+ "version": "1.8.1",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -36,12 +36,12 @@
36
36
  "@jsenv/abort": "4.2.4",
37
37
  "@jsenv/ast": "6.0.1",
38
38
  "@jsenv/filesystem": "4.3.2",
39
- "@jsenv/log": "3.4.2",
39
+ "@jsenv/log": "3.4.3",
40
40
  "@jsenv/plugin-supervisor": "1.3.9",
41
41
  "@jsenv/urls": "2.2.1",
42
42
  "@jsenv/url-meta": "8.1.0",
43
43
  "@jsenv/utils": "2.0.1",
44
- "@jsenv/github-check-run": "0.1.3",
44
+ "@jsenv/github-check-run": "0.1.4",
45
45
  "v8-to-istanbul": "9.1.3",
46
46
  "istanbul-lib-coverage": "3.2.2",
47
47
  "istanbul-lib-instrument": "6.0.1",
@@ -1,4 +1,6 @@
1
1
  import { existsSync } from "node:fs";
2
+ import { memoryUsage } from "node:process";
3
+ import { takeCoverage } from "node:v8";
2
4
  import stripAnsi from "strip-ansi";
3
5
  import { Abort, raceProcessTeardownEvents } from "@jsenv/abort";
4
6
  import { URL_META } from "@jsenv/url-meta";
@@ -7,22 +9,36 @@ import {
7
9
  ensureEmptyDirectory,
8
10
  assertAndNormalizeDirectoryUrl,
9
11
  assertAndNormalizeFileUrl,
12
+ writeFileSync,
10
13
  } from "@jsenv/filesystem";
11
- import { createLogger, createDetailedMessage, UNICODE } from "@jsenv/log";
14
+ import {
15
+ createLogger,
16
+ createDetailedMessage,
17
+ UNICODE,
18
+ createLog,
19
+ startSpinner,
20
+ } from "@jsenv/log";
12
21
  import {
13
22
  startGithubCheckRun,
14
23
  readGitHubWorkflowEnv,
15
24
  } from "@jsenv/github-check-run";
16
25
 
17
26
  import { createTeardown } from "../helpers/teardown.js";
27
+ import { createCallOrderer } from "../helpers/call_orderer.js";
28
+ import { reportToCoverage } from "../coverage/report_to_coverage.js";
18
29
  import { generateCoverageJsonFile } from "../coverage/coverage_reporter_json_file.js";
19
30
  import { generateCoverageHtmlDirectory } from "../coverage/coverage_reporter_html_directory.js";
20
31
  import { generateCoverageTextLog } from "../coverage/coverage_reporter_text_log.js";
21
32
  import { assertAndNormalizeWebServer } from "./web_server_param.js";
22
33
  import { executionStepsFromTestPlan } from "./execution_steps.js";
23
- import { executeSteps } from "./execute_steps.js";
24
- import { formatSummary } from "./logs_file_execution.js";
34
+ import {
35
+ createExecutionLog,
36
+ formatSummaryLog,
37
+ formatSummary,
38
+ } from "./logs_file_execution.js";
25
39
  import { githubAnnotationFromError } from "./github_annotation_from_error.js";
40
+ import { run } from "./run.js";
41
+ import { ensureGlobalGc } from "./gc.js";
26
42
 
27
43
  /**
28
44
  * Execute a list of files and log how it goes.
@@ -74,7 +90,7 @@ export const executeTestPlan = async ({
74
90
  githubCheckEnabled = Boolean(process.env.GITHUB_WORKFLOW),
75
91
  githubCheckLogLevel,
76
92
  githubCheckName = "Jsenv tests",
77
- githubCheckTitle,
93
+ githubCheckTitle = "Tests executions",
78
94
  githubCheckToken,
79
95
  githubCheckRepositoryOwner,
80
96
  githubCheckRepositoryName,
@@ -119,10 +135,21 @@ export const executeTestPlan = async ({
119
135
  coverageReportJsonFileUrl,
120
136
  coverageReportHtml = !process.env.CI,
121
137
  coverageReportHtmlDirectoryUrl,
138
+
139
+ beforeExecutionCallback = () => {},
140
+ afterExecutionCallback = () => {},
141
+ afterAllExecutionCallback = () => {},
122
142
  ...rest
123
143
  }) => {
124
144
  const teardown = createTeardown();
125
145
 
146
+ const beforeExecutionCallbackSet = new Set();
147
+ const afterExecutionCallbackSet = new Set();
148
+ const afterAllExecutionCallbackSet = new Set();
149
+ beforeExecutionCallbackSet.add(beforeExecutionCallback);
150
+ afterExecutionCallbackSet.add(afterExecutionCallback);
151
+ afterAllExecutionCallbackSet.add(afterAllExecutionCallback);
152
+
126
153
  const operation = Abort.startOperation();
127
154
  operation.addAbortSignal(signal);
128
155
  if (handleSIGINT) {
@@ -362,15 +389,12 @@ To fix this warning:
362
389
  "**/.jsenv/": null,
363
390
  };
364
391
  logger.debug(`Generate executions`);
365
- const executionSteps = await executionStepsFromTestPlan({
392
+ let executionSteps = await executionStepsFromTestPlan({
366
393
  signal,
367
394
  testPlan,
368
395
  rootDirectoryUrl,
369
396
  });
370
397
  logger.debug(`${executionSteps.length} executions planned`);
371
- let beforeExecutionCallback;
372
- let afterExecutionCallback;
373
- let afterAllExecutionCallback = () => {};
374
398
  if (githubCheckEnabled) {
375
399
  const githubCheckRun = await startGithubCheckRun({
376
400
  logLevel: githubCheckLogLevel,
@@ -379,11 +403,11 @@ To fix this warning:
379
403
  repositoryName: githubCheckRepositoryName,
380
404
  commitSha: githubCheckCommitSha,
381
405
  checkName: githubCheckName,
382
- checkTitle: `Tests executions`,
406
+ checkTitle: githubCheckTitle,
383
407
  checkSummary: `${executionSteps.length} files will be executed`,
384
408
  });
385
409
  const annotations = [];
386
- afterExecutionCallback = (afterExecutionInfo) => {
410
+ afterExecutionCallbackSet.add((afterExecutionInfo) => {
387
411
  const { executionResult } = afterExecutionInfo;
388
412
  const { errors = [] } = executionResult;
389
413
  for (const error of errors) {
@@ -393,79 +417,338 @@ To fix this warning:
393
417
  });
394
418
  annotations.push(annotation);
395
419
  }
396
- };
397
- afterAllExecutionCallback = async ({ testPlanSummary }) => {
420
+ });
421
+ afterAllExecutionCallbackSet.add(async (returnValue) => {
422
+ const { summary } = returnValue;
398
423
  const title = "Jsenv test results";
399
- const summary = stripAnsi(formatSummary(testPlanSummary));
400
- if (
401
- testPlanSummary.counters.total !== testPlanSummary.counters.completed
402
- ) {
424
+ const summaryText = stripAnsi(formatSummary(summary));
425
+ if (summary.counters.total !== summary.counters.completed) {
403
426
  await githubCheckRun.fail({
404
427
  title,
405
- summary,
428
+ summary: summaryText,
406
429
  annotations,
407
430
  });
408
431
  return;
409
432
  }
410
433
  await githubCheckRun.pass({
411
434
  title,
412
- summary,
435
+ summary: summaryText,
413
436
  annotations,
414
437
  });
415
- };
438
+ });
416
439
  }
417
440
 
418
- const result = await executeSteps(executionSteps, {
419
- signal,
420
- teardown,
421
- logger,
422
- logRefresh,
423
- logSummary,
424
- logRuntime,
425
- logEachDuration,
426
- logTimeUsage,
427
- logMemoryHeapUsage,
428
- logFileRelativeUrl,
429
- logShortForCompletedExecutions,
430
- logMergeForCompletedExecutions,
431
- rootDirectoryUrl,
432
- webServer,
433
-
434
- maxExecutionsInParallel,
435
- defaultMsAllocatedPerExecution,
436
- failFast,
437
- keepRunning,
438
- cooldownBetweenExecutions,
439
- gcBetweenExecutions,
440
-
441
- githubCheckEnabled,
442
- githubCheckName,
443
- githubCheckTitle,
444
- githubCheckToken,
445
- githubCheckRepositoryOwner,
446
- githubCheckRepositoryName,
447
- githubCheckCommitSha,
448
-
449
- coverageEnabled,
450
- coverageConfig,
451
- coverageIncludeMissing,
452
- coverageMethodForBrowsers,
453
- coverageMethodForNodeJs,
454
- coverageV8ConflictWarning,
455
- coverageTempDirectoryUrl,
456
-
457
- beforeExecutionCallback,
458
- afterExecutionCallback,
459
- });
441
+ executionSteps = executionSteps.filter(
442
+ (executionStep) => !executionStep.runtime?.disabled,
443
+ );
444
+
445
+ const returnValue = {
446
+ aborted: false,
447
+ summary: null,
448
+ report: null,
449
+ coverage: null,
450
+ };
451
+ const report = {};
452
+ const callbacks = [];
453
+
454
+ const multipleExecutionsOperation = Abort.startOperation();
455
+ multipleExecutionsOperation.addAbortSignal(signal);
456
+ const failFastAbortController = new AbortController();
457
+ if (failFast) {
458
+ multipleExecutionsOperation.addAbortSignal(failFastAbortController.signal);
459
+ }
460
+
461
+ try {
462
+ if (gcBetweenExecutions) {
463
+ ensureGlobalGc();
464
+ }
465
+
466
+ if (coverageEnabled) {
467
+ // when runned multiple times, we don't want to keep previous files in this directory
468
+ await ensureEmptyDirectory(coverageTempDirectoryUrl);
469
+ callbacks.push(async () => {
470
+ if (multipleExecutionsOperation.signal.aborted) {
471
+ // don't try to do the coverage stuff
472
+ return;
473
+ }
474
+ try {
475
+ if (coverageMethodForNodeJs === "NODE_V8_COVERAGE") {
476
+ takeCoverage();
477
+ // conceptually we don't need coverage anymore so it would be
478
+ // good to call v8.stopCoverage()
479
+ // but it logs a strange message about "result is not an object"
480
+ }
481
+ const coverage = await reportToCoverage(report, {
482
+ signal: multipleExecutionsOperation.signal,
483
+ logger,
484
+ rootDirectoryUrl,
485
+ coverageConfig,
486
+ coverageIncludeMissing,
487
+ coverageMethodForNodeJs,
488
+ coverageV8ConflictWarning,
489
+ });
490
+ returnValue.coverage = coverage;
491
+ } catch (e) {
492
+ if (Abort.isAbortError(e)) {
493
+ return;
494
+ }
495
+ throw e;
496
+ }
497
+ });
498
+ }
499
+
500
+ const runtimeParams = {
501
+ rootDirectoryUrl,
502
+ webServer,
503
+
504
+ coverageEnabled,
505
+ coverageConfig,
506
+ coverageMethodForBrowsers,
507
+ coverageMethodForNodeJs,
508
+ isTestPlan: true,
509
+ teardown,
510
+ };
511
+
512
+ if (logMergeForCompletedExecutions && !process.stdout.isTTY) {
513
+ logMergeForCompletedExecutions = false;
514
+ logger.debug(
515
+ `Force logMergeForCompletedExecutions to false because process.stdout.isTTY is false`,
516
+ );
517
+ }
518
+ const debugLogsEnabled = logger.levels.debug;
519
+ const executionLogsEnabled = logger.levels.info;
520
+ const executionSpinner =
521
+ logRefresh &&
522
+ maxExecutionsInParallel === 1 &&
523
+ !debugLogsEnabled &&
524
+ executionLogsEnabled &&
525
+ process.stdout.isTTY &&
526
+ // if there is an error during execution npm will mess up the output
527
+ // (happens when npm runs several command in a workspace)
528
+ // so we enable spinner only when !process.exitCode (no error so far)
529
+ process.exitCode !== 1;
530
+
531
+ const startMs = Date.now();
532
+ let rawOutput = "";
533
+ let executionLog = createLog({ newLine: "" });
534
+ const counters = {
535
+ total: executionSteps.length,
536
+ aborted: 0,
537
+ timedout: 0,
538
+ failed: 0,
539
+ completed: 0,
540
+ done: 0,
541
+ };
542
+
543
+ const callWhenPreviousExecutionAreDone = createCallOrderer();
544
+
545
+ await executeInParallel({
546
+ multipleExecutionsOperation,
547
+ maxExecutionsInParallel,
548
+ cooldownBetweenExecutions,
549
+ executionSteps,
550
+ start: async (paramsFromStep) => {
551
+ const executionIndex = executionSteps.indexOf(paramsFromStep);
552
+ const { executionName, fileRelativeUrl, runtime } = paramsFromStep;
553
+ const runtimeType = runtime.type;
554
+ const runtimeName = runtime.name;
555
+ const runtimeVersion = runtime.version;
556
+ const executionParams = {
557
+ measurePerformance: false,
558
+ collectPerformance: false,
559
+ collectConsole: true,
560
+ allocatedMs: defaultMsAllocatedPerExecution,
561
+ ...paramsFromStep,
562
+ runtimeParams: {
563
+ fileRelativeUrl,
564
+ ...paramsFromStep.runtimeParams,
565
+ },
566
+ };
567
+ const beforeExecutionInfo = {
568
+ fileRelativeUrl,
569
+ runtimeType,
570
+ runtimeName,
571
+ runtimeVersion,
572
+ executionIndex,
573
+ executionParams,
574
+ startMs: Date.now(),
575
+ executionResult: {
576
+ status: "executing",
577
+ },
578
+ counters,
579
+ timeEllapsed: Date.now() - startMs,
580
+ memoryHeap: memoryUsage().heapUsed,
581
+ };
582
+ if (typeof executionParams.allocatedMs === "function") {
583
+ executionParams.allocatedMs =
584
+ executionParams.allocatedMs(beforeExecutionInfo);
585
+ }
586
+ let spinner;
587
+ if (executionSpinner) {
588
+ spinner = startSpinner({
589
+ log: executionLog,
590
+ render: () => {
591
+ return createExecutionLog(beforeExecutionInfo, {
592
+ logRuntime,
593
+ logEachDuration,
594
+ logTimeUsage,
595
+ logMemoryHeapUsage,
596
+ });
597
+ },
598
+ });
599
+ }
600
+ for (const beforeExecutionCallback of beforeExecutionCallbackSet) {
601
+ beforeExecutionCallback(beforeExecutionInfo);
602
+ }
603
+ const fileUrl = `${rootDirectoryUrl}${fileRelativeUrl}`;
604
+ let executionResult;
605
+ if (existsSync(new URL(fileUrl))) {
606
+ executionResult = await run({
607
+ signal: multipleExecutionsOperation.signal,
608
+ logger,
609
+ allocatedMs: executionParams.allocatedMs,
610
+ keepRunning,
611
+ mirrorConsole: false, // might be executed in parallel: log would be a mess to read
612
+ collectConsole: executionParams.collectConsole,
613
+ coverageEnabled,
614
+ coverageTempDirectoryUrl,
615
+ runtime: executionParams.runtime,
616
+ runtimeParams: {
617
+ ...runtimeParams,
618
+ ...executionParams.runtimeParams,
619
+ },
620
+ });
621
+ } else {
622
+ executionResult = {
623
+ status: "failed",
624
+ errors: [
625
+ new Error(
626
+ `No file at ${fileRelativeUrl} for execution "${executionName}"`,
627
+ ),
628
+ ],
629
+ };
630
+ }
631
+ const fileReport = report[fileRelativeUrl];
632
+ if (fileReport) {
633
+ fileReport[executionName] = executionResult;
634
+ } else {
635
+ report[fileRelativeUrl] = {
636
+ [executionName]: executionResult,
637
+ };
638
+ }
639
+
640
+ const afterExecutionInfo = {
641
+ ...beforeExecutionInfo,
642
+ runtimeVersion: runtime.version,
643
+ endMs: Date.now(),
644
+ executionResult,
645
+ };
646
+
647
+ if (gcBetweenExecutions) {
648
+ global.gc();
649
+ }
650
+
651
+ const timeEllapsed = Date.now() - startMs;
652
+ const memoryHeap = memoryUsage().heapUsed;
653
+ callWhenPreviousExecutionAreDone(executionIndex, () => {
654
+ counters.done++;
655
+ if (executionResult.status === "aborted") {
656
+ counters.aborted++;
657
+ } else if (executionResult.status === "timedout") {
658
+ counters.timedout++;
659
+ } else if (executionResult.status === "failed") {
660
+ counters.failed++;
661
+ } else if (executionResult.status === "completed") {
662
+ counters.completed++;
663
+ }
664
+
665
+ if (executionLogsEnabled) {
666
+ // replace spinner with this execution result
667
+ if (spinner) {
668
+ spinner.stop();
669
+ spinner = null;
670
+ }
671
+
672
+ const log = createExecutionLog(afterExecutionInfo, {
673
+ logShortForCompletedExecutions,
674
+ logRuntime,
675
+ logEachDuration,
676
+ ...(logTimeUsage ? { timeEllapsed } : {}),
677
+ ...(logMemoryHeapUsage ? { memoryHeap } : {}),
678
+ });
679
+
680
+ executionLog.write(log);
681
+ rawOutput += stripAnsi(log);
682
+ const canOverwriteLog = canOverwriteLogGetter({
683
+ logMergeForCompletedExecutions,
684
+ executionResult,
685
+ });
686
+ if (canOverwriteLog) {
687
+ // nothing to do, we reuse the current executionLog object
688
+ } else {
689
+ executionLog.destroy();
690
+ executionLog = createLog({ newLine: "" });
691
+ }
692
+ const isLastExecutionLog =
693
+ executionIndex === executionSteps.length - 1;
694
+ if (isLastExecutionLog && logger.levels.info) {
695
+ executionLog.write("\n");
696
+ }
697
+ }
698
+ });
699
+ for (const afterExecutionCallback of afterExecutionCallbackSet) {
700
+ afterExecutionCallback(afterExecutionInfo);
701
+ }
702
+ const cancelRemaining =
703
+ failFast &&
704
+ executionResult.status !== "completed" &&
705
+ counters.done < counters.total;
706
+ if (cancelRemaining) {
707
+ logger.info(`"failFast" enabled -> cancel remaining executions`);
708
+ failFastAbortController.abort();
709
+ }
710
+ },
711
+ });
712
+ if (!keepRunning) {
713
+ logger.debug("trigger test plan teardown");
714
+ await teardown.trigger();
715
+ }
716
+
717
+ counters.cancelled = counters.total - counters.done;
718
+ const summary = {
719
+ counters,
720
+ // when execution is aborted, the remaining executions are "cancelled"
721
+ duration: Date.now() - startMs,
722
+ };
723
+ if (logSummary) {
724
+ const summaryLog = formatSummaryLog(summary);
725
+ rawOutput += stripAnsi(summaryLog);
726
+ logger.info(summaryLog);
727
+ }
728
+ if (summary.counters.total !== summary.counters.completed) {
729
+ const logFileUrl = new URL(logFileRelativeUrl, rootDirectoryUrl).href;
730
+ writeFileSync(logFileUrl, rawOutput);
731
+ logger.info(`-> ${urlToFileSystemPath(logFileUrl)}`);
732
+ }
733
+ returnValue.aborted = multipleExecutionsOperation.signal.aborted;
734
+ returnValue.summary = summary;
735
+ returnValue.report = report;
736
+ for (const callback of callbacks) {
737
+ await callback();
738
+ }
739
+ } finally {
740
+ await multipleExecutionsOperation.end();
741
+ }
460
742
 
461
743
  const hasFailed =
462
- result.planSummary.counters.total !== result.planSummary.counters.completed;
744
+ returnValue.summary.counters.total !==
745
+ returnValue.summary.counters.completed;
463
746
  if (updateProcessExitCode && hasFailed) {
464
747
  process.exitCode = 1;
465
748
  }
466
- const planCoverage = result.planCoverage;
749
+ const coverage = returnValue.coverage;
467
750
  // planCoverage can be null when execution is aborted
468
- if (planCoverage) {
751
+ if (coverage) {
469
752
  const promises = [];
470
753
  // keep this one first because it does ensureEmptyDirectory
471
754
  // and in case coverage json file gets written in the same directory
@@ -477,7 +760,7 @@ To fix this warning:
477
760
  `-> ${urlToFileSystemPath(htmlCoverageDirectoryIndexFileUrl)}`,
478
761
  );
479
762
  promises.push(
480
- generateCoverageHtmlDirectory(planCoverage, {
763
+ generateCoverageHtmlDirectory(coverage, {
481
764
  rootDirectoryUrl,
482
765
  coverageHtmlDirectoryRelativeUrl: urlToRelativeUrl(
483
766
  coverageReportHtmlDirectoryUrl,
@@ -491,7 +774,7 @@ To fix this warning:
491
774
  if (coverageEnabled && coverageReportJson) {
492
775
  promises.push(
493
776
  generateCoverageJsonFile({
494
- coverage: result.planCoverage,
777
+ coverage,
495
778
  coverageJsonFileUrl: coverageReportJsonFileUrl,
496
779
  logger,
497
780
  }),
@@ -499,7 +782,7 @@ To fix this warning:
499
782
  }
500
783
  if (coverageEnabled && coverageReportTextLog) {
501
784
  promises.push(
502
- generateCoverageTextLog(result.planCoverage, {
785
+ generateCoverageTextLog(coverage, {
503
786
  coverageReportSkipEmpty,
504
787
  coverageReportSkipFull,
505
788
  }),
@@ -508,12 +791,81 @@ To fix this warning:
508
791
  await Promise.all(promises);
509
792
  }
510
793
 
511
- const returnValue = {
512
- testPlanAborted: result.aborted,
513
- testPlanSummary: result.planSummary,
514
- testPlanReport: result.planReport,
515
- testPlanCoverage: planCoverage,
516
- };
517
- await afterAllExecutionCallback(returnValue);
794
+ for (const afterAllExecutionCallback of afterAllExecutionCallbackSet) {
795
+ await afterAllExecutionCallback(returnValue);
796
+ }
518
797
  return returnValue;
519
798
  };
799
+
800
+ const canOverwriteLogGetter = ({
801
+ logMergeForCompletedExecutions,
802
+ executionResult,
803
+ }) => {
804
+ if (!logMergeForCompletedExecutions) {
805
+ return false;
806
+ }
807
+ if (executionResult.status === "aborted") {
808
+ return true;
809
+ }
810
+ if (executionResult.status !== "completed") {
811
+ return false;
812
+ }
813
+ const { consoleCalls = [] } = executionResult;
814
+ if (consoleCalls.length > 0) {
815
+ return false;
816
+ }
817
+ return true;
818
+ };
819
+
820
+ const executeInParallel = async ({
821
+ multipleExecutionsOperation,
822
+ maxExecutionsInParallel,
823
+ cooldownBetweenExecutions,
824
+ executionSteps,
825
+ start,
826
+ }) => {
827
+ const executionResults = [];
828
+ let progressionIndex = 0;
829
+ let remainingExecutionCount = executionSteps.length;
830
+
831
+ const nextChunk = async () => {
832
+ if (multipleExecutionsOperation.signal.aborted) {
833
+ return;
834
+ }
835
+ const outputPromiseArray = [];
836
+ let previousExecPromise = Promise.resolve();
837
+ while (
838
+ remainingExecutionCount > 0 &&
839
+ outputPromiseArray.length < maxExecutionsInParallel
840
+ ) {
841
+ remainingExecutionCount--;
842
+ const outputPromise = executeOne(progressionIndex, previousExecPromise);
843
+ previousExecPromise = outputPromise;
844
+ progressionIndex++;
845
+ outputPromiseArray.push(outputPromise);
846
+ }
847
+ if (outputPromiseArray.length) {
848
+ await Promise.all(outputPromiseArray);
849
+ if (remainingExecutionCount > 0) {
850
+ await nextChunk();
851
+ }
852
+ }
853
+ };
854
+
855
+ const executeOne = async (index, previousExecPromise) => {
856
+ const input = executionSteps[index];
857
+ const output = await start(input, previousExecPromise);
858
+ if (!multipleExecutionsOperation.signal.aborted) {
859
+ executionResults[index] = output;
860
+ }
861
+ if (cooldownBetweenExecutions) {
862
+ await new Promise((resolve) =>
863
+ setTimeout(resolve, cooldownBetweenExecutions),
864
+ );
865
+ }
866
+ };
867
+
868
+ await nextChunk();
869
+
870
+ return executionResults;
871
+ };
@@ -0,0 +1,39 @@
1
+ export const createCallOrderer = () => {
2
+ const queue = [];
3
+ const callWhenPreviousExecutionAreDone = (executionIndex, callback) => {
4
+ if (queue[executionIndex]) {
5
+ throw new Error(`${executionIndex} already used`);
6
+ }
7
+
8
+ let allBeforeAreDone = true;
9
+ if (executionIndex > 0) {
10
+ let beforeIndex = executionIndex - 1;
11
+ do {
12
+ const value = queue[beforeIndex];
13
+ if (!value) {
14
+ allBeforeAreDone = false;
15
+ break;
16
+ }
17
+ } while (beforeIndex--);
18
+ }
19
+ if (!allBeforeAreDone) {
20
+ queue[executionIndex] = callback;
21
+ return;
22
+ }
23
+ queue[executionIndex] = true;
24
+ callback();
25
+ let afterIndex = executionIndex + 1;
26
+ while (afterIndex < queue.length) {
27
+ const value = queue[afterIndex];
28
+ if (value === undefined) {
29
+ break;
30
+ }
31
+ if (typeof value === "function") {
32
+ queue[afterIndex] = true;
33
+ value();
34
+ }
35
+ afterIndex++;
36
+ }
37
+ };
38
+ return callWhenPreviousExecutionAreDone;
39
+ };