first commit
This commit is contained in:
+15
@@ -0,0 +1,15 @@
|
||||
# Changelog
|
||||
|
||||
## [1.0.1](https://github.com/humanwhocodes/module-importer/compare/v1.0.0...v1.0.1) (2022-08-18)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Ensure CommonJS mode works correctly. ([cf54a0b](https://github.com/humanwhocodes/module-importer/commit/cf54a0b998085066fbe1776dd0b4cacd808cc192)), closes [#6](https://github.com/humanwhocodes/module-importer/issues/6)
|
||||
|
||||
## 1.0.0 (2022-08-17)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Implement ModuleImporter ([3ce4e82](https://www.github.com/humanwhocodes/module-importer/commit/3ce4e820c30c114e787bfed00a0966ac4772f563))
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
# ModuleImporter
|
||||
|
||||
by [Nicholas C. Zakas](https://humanwhocodes.com)
|
||||
|
||||
If you find this useful, please consider supporting my work with a [donation](https://humanwhocodes.com/donate).
|
||||
|
||||
## Description
|
||||
|
||||
A utility for seamlessly importing modules in Node.js regardless if they are CommonJS or ESM format. Under the hood, this uses `import()` and relies on Node.js's CommonJS compatibility to work correctly. This ensures that the correct locations and formats are used for CommonJS so you can call one method and not worry about any compatibility issues.
|
||||
|
||||
The problem with the default `import()` is that it always resolves relative to the file location in which it is called. If you want to resolve from a different location, you need to jump through a few hoops to achieve that. This package makes it easy to both resolve and import modules from any directory.
|
||||
|
||||
## Usage
|
||||
|
||||
### Node.js
|
||||
|
||||
Install using [npm][npm] or [yarn][yarn]:
|
||||
|
||||
```
|
||||
npm install @humanwhocodes/module-importer
|
||||
|
||||
# or
|
||||
|
||||
yarn add @humanwhocodes/module-importer
|
||||
```
|
||||
|
||||
Import into your Node.js project:
|
||||
|
||||
```js
|
||||
// CommonJS
|
||||
const { ModuleImporter } = require("@humanwhocodes/module-importer");
|
||||
|
||||
// ESM
|
||||
import { ModuleImporter } from "@humanwhocodes/module-importer";
|
||||
```
|
||||
|
||||
### Bun
|
||||
|
||||
Install using this command:
|
||||
|
||||
```
|
||||
bun add @humanwhocodes/module-importer
|
||||
```
|
||||
|
||||
Import into your Bun project:
|
||||
|
||||
```js
|
||||
import { ModuleImporter } from "@humanwhocodes/module-importer";
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
After importing, create a new instance of `ModuleImporter` to start emitting events:
|
||||
|
||||
```js
|
||||
// cwd can be omitted to use process.cwd()
|
||||
const importer = new ModuleImporter(cwd);
|
||||
|
||||
// you can resolve the location of any package
|
||||
const location = importer.resolve("./some-file.cjs");
|
||||
|
||||
// you can also import directly
|
||||
const module = importer.import("./some-file.cjs");
|
||||
```
|
||||
|
||||
For both `resolve()` and `import()`, you can pass in package names and filenames.
|
||||
|
||||
## Developer Setup
|
||||
|
||||
1. Fork the repository
|
||||
2. Clone your fork
|
||||
3. Run `npm install` to setup dependencies
|
||||
4. Run `npm test` to run tests
|
||||
|
||||
## License
|
||||
|
||||
Apache 2.0
|
||||
|
||||
[npm]: https://npmjs.com/
|
||||
[yarn]: https://yarnpkg.com/
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
'use strict';
|
||||
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
|
||||
var module$1 = require('module');
|
||||
var url = require('url');
|
||||
var path = require('path');
|
||||
|
||||
/**
|
||||
* @fileoverview Universal module importer
|
||||
*/
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
const __filename$1 = url.fileURLToPath((typeof document === 'undefined' ? new (require('u' + 'rl').URL)('file:' + __filename).href : (document.currentScript && document.currentScript.src || new URL('module-importer.cjs', document.baseURI).href)));
|
||||
const __dirname$1 = path.dirname(__filename$1);
|
||||
const require$1 = module$1.createRequire(__dirname$1 + "/");
|
||||
const { ModuleImporter } = require$1("./module-importer.cjs");
|
||||
|
||||
exports.ModuleImporter = ModuleImporter;
|
||||
Generated
Vendored
+27
@@ -0,0 +1,27 @@
|
||||
export class ModuleImporter {
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {string} [cwd] The current working directory to resolve from.
|
||||
*/
|
||||
constructor(cwd?: string);
|
||||
/**
|
||||
* The base directory from which paths should be resolved.
|
||||
* @type {string}
|
||||
*/
|
||||
cwd: string;
|
||||
/**
|
||||
* Resolves a module based on its name or location.
|
||||
* @param {string} specifier Either an npm package name or
|
||||
* relative file path.
|
||||
* @returns {string|undefined} The location of the import.
|
||||
* @throws {Error} If specifier cannot be located.
|
||||
*/
|
||||
resolve(specifier: string): string | undefined;
|
||||
/**
|
||||
* Imports a module based on its name or location.
|
||||
* @param {string} specifier Either an npm package name or
|
||||
* relative file path.
|
||||
* @returns {Promise<object>} The module's object.
|
||||
*/
|
||||
import(specifier: string): Promise<object>;
|
||||
}
|
||||
Generated
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
export { ModuleImporter };
|
||||
import { ModuleImporter } from "./module-importer.cjs";
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { createRequire } from 'module';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname } from 'path';
|
||||
|
||||
/**
|
||||
* @fileoverview Universal module importer
|
||||
*/
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const require = createRequire(__dirname + "/");
|
||||
const { ModuleImporter } = require("./module-importer.cjs");
|
||||
|
||||
export { ModuleImporter };
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"name": "@humanwhocodes/module-importer",
|
||||
"version": "1.0.1",
|
||||
"description": "Universal module importer for Node.js",
|
||||
"main": "src/module-importer.cjs",
|
||||
"module": "src/module-importer.js",
|
||||
"type": "module",
|
||||
"types": "dist/module-importer.d.ts",
|
||||
"exports": {
|
||||
"require": "./src/module-importer.cjs",
|
||||
"import": "./src/module-importer.js"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"src"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"gitHooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.js": [
|
||||
"eslint --fix"
|
||||
]
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/nzakas"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rollup -c && tsc",
|
||||
"prepare": "npm run build",
|
||||
"lint": "eslint src/ tests/",
|
||||
"test:unit": "c8 mocha tests/module-importer.test.js",
|
||||
"test:build": "node tests/pkg.test.cjs && node tests/pkg.test.mjs",
|
||||
"test": "npm run test:unit && npm run test:build"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/humanwhocodes/module-importer.git"
|
||||
},
|
||||
"keywords": [
|
||||
"modules",
|
||||
"esm",
|
||||
"commonjs"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12.22"
|
||||
},
|
||||
"author": "Nicholas C. Zaks",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.7.6",
|
||||
"c8": "7.12.0",
|
||||
"chai": "4.3.6",
|
||||
"eslint": "8.22.0",
|
||||
"lint-staged": "13.0.3",
|
||||
"mocha": "9.2.2",
|
||||
"rollup": "2.78.0",
|
||||
"typescript": "4.7.4",
|
||||
"yorkie": "2.0.0"
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* @fileoverview Universal module importer
|
||||
*/
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Imports
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
const { createRequire } = require("module");
|
||||
const { pathToFileURL } = require("url");
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
const SLASHES = new Set(["/", "\\"]);
|
||||
|
||||
/**
|
||||
* Normalizes directories to have a trailing slash.
|
||||
* Resolve is pretty finicky -- if the directory name doesn't have
|
||||
* a trailing slash then it tries to look in the parent directory.
|
||||
* i.e., if the directory is "/usr/nzakas/foo" it will start the
|
||||
* search in /usr/nzakas. However, if the directory is "/user/nzakas/foo/",
|
||||
* then it will start the search in /user/nzakas/foo.
|
||||
* @param {string} directory The directory to check.
|
||||
* @returns {string} The normalized directory.
|
||||
*/
|
||||
function normalizeDirectory(directory) {
|
||||
if (!SLASHES.has(directory[directory.length-1])) {
|
||||
return directory + "/";
|
||||
}
|
||||
|
||||
return directory;
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Exports
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Class for importing both CommonJS and ESM modules in Node.js.
|
||||
*/
|
||||
exports.ModuleImporter = class ModuleImporter {
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {string} [cwd] The current working directory to resolve from.
|
||||
*/
|
||||
constructor(cwd = process.cwd()) {
|
||||
|
||||
/**
|
||||
* The base directory from which paths should be resolved.
|
||||
* @type {string}
|
||||
*/
|
||||
this.cwd = normalizeDirectory(cwd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a module based on its name or location.
|
||||
* @param {string} specifier Either an npm package name or
|
||||
* relative file path.
|
||||
* @returns {string|undefined} The location of the import.
|
||||
* @throws {Error} If specifier cannot be located.
|
||||
*/
|
||||
resolve(specifier) {
|
||||
const require = createRequire(this.cwd);
|
||||
return require.resolve(specifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Imports a module based on its name or location.
|
||||
* @param {string} specifier Either an npm package name or
|
||||
* relative file path.
|
||||
* @returns {Promise<object>} The module's object.
|
||||
*/
|
||||
import(specifier) {
|
||||
const location = this.resolve(specifier);
|
||||
return import(pathToFileURL(location).href);
|
||||
}
|
||||
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @fileoverview Universal module importer
|
||||
*/
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Imports
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
import { createRequire } from "module";
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname } from "path";
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const require = createRequire(__dirname + "/");
|
||||
const { ModuleImporter } = require("./module-importer.cjs");
|
||||
|
||||
export { ModuleImporter };
|
||||
+294
@@ -0,0 +1,294 @@
|
||||
# Momoa JSON
|
||||
|
||||
by [Nicholas C. Zakas](https://humanwhocodes.com)
|
||||
|
||||
If you find this useful, please consider supporting my work with a [donation](https://humanwhocodes.com/donate).
|
||||
|
||||
## About
|
||||
|
||||
Momoa is a general purpose JSON utility toolkit, containing:
|
||||
|
||||
* A **tokenizer** that allows you to separate a JSON string into its component parts.
|
||||
* A ECMA-404 compliant **parser** that produces an abstract syntax tree (AST) representing everything inside of a JSON string.
|
||||
* A **traverser** that visits an AST produced by the parser in order.
|
||||
* A **printer** that can convert an AST produced by the parser back into a valid JSON string.
|
||||
|
||||
## Background
|
||||
|
||||
JavaScript defines the `JSON` object with methods for both parsing strings into objects and converting objects into JSON-formatted strings. In most cases, this is exactly what you need and should use without question. However, these methods aren't useful for more fine-grained analysis of JSON structures. For instance, you'll never know if a JSON object contains two properties with the same names because `JSON.parse()` will ignore the first one and return the value of the second. A tool like Momoa comes in handy when you want to know not just the result of JSON parsing, but exactly what is contained in the original JSON string.
|
||||
|
||||
## Usage
|
||||
|
||||
### Node.js
|
||||
|
||||
Install using [npm][npm] or [yarn][yarn]:
|
||||
|
||||
```
|
||||
npm install @humanwhocodes/momoa
|
||||
|
||||
# or
|
||||
|
||||
yarn add @humanwhocodes/momoa
|
||||
```
|
||||
|
||||
Import into your Node.js project:
|
||||
|
||||
```js
|
||||
// CommonJS
|
||||
const { parse } = require("@humanwhocodes/momoa");
|
||||
|
||||
// ESM
|
||||
import { parse } from "@humanwhocodes/momoa";
|
||||
```
|
||||
|
||||
### Deno
|
||||
|
||||
Import into your Deno project:
|
||||
|
||||
```js
|
||||
import { parse } from "https://cdn.skypack.dev/@humanwhocodes/momoa?dts";
|
||||
```
|
||||
|
||||
### Bun
|
||||
|
||||
Install using this command:
|
||||
|
||||
```
|
||||
bun add @humanwhocodes/momoa
|
||||
```
|
||||
|
||||
Import into your Bun project:
|
||||
|
||||
```js
|
||||
import { parse } from "@humanwhocodes/momoa";
|
||||
```
|
||||
|
||||
### Browser
|
||||
|
||||
It's recommended to import the minified version to save bandwidth:
|
||||
|
||||
```js
|
||||
import { parse } from "https://cdn.skypack.dev/@humanwhocodes/momoa?min";
|
||||
```
|
||||
|
||||
However, you can also import the unminified version for debugging purposes:
|
||||
|
||||
```js
|
||||
import { parse } from "https://cdn.skypack.dev/@humanwhocodes/momoa";
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### Parsing
|
||||
|
||||
To parse a JSON string into an AST, use the `parse()` function:
|
||||
|
||||
```js
|
||||
const { parse } = require("@humanwhocodes/momoa");
|
||||
|
||||
const ast = parse(some_json_string);
|
||||
```
|
||||
|
||||
The `parse()` function accepts a second argument, which is an options object that may contain one or more of the following properties:
|
||||
|
||||
* `mode` (default: `"json"`) - specify the parsing mode. Possible options are `"json"`, `"jsonc"` (JSON with comments), and `"json5"`.
|
||||
* `ranges` (default: `false`) - set to `true` if you want each node to also have a `range` property, which is an array containing the start and stop index for the syntax within the source string.
|
||||
* `tokens` - set to `true` to return a `tokens` property on the root node containing all of the tokens used to parse the code. If `mode` is `"jsonc"` or `"json5"`, then the tokens include comment tokens.
|
||||
* `allowTrailingCommas` - set to `true` to allow trailing commas in arrays and objects in `"json"` and `"jsonc"` modes. This option is ignored in JSON5 mode.
|
||||
|
||||
Here's an example of passing options:
|
||||
|
||||
```js
|
||||
const { parse } = require("@humanwhocodes/momoa");
|
||||
|
||||
const ast = parse(some_json_string, {
|
||||
mode: "jsonc",
|
||||
ranges: true,
|
||||
tokens: true
|
||||
});
|
||||
|
||||
// root now has a range array
|
||||
console.dir(ast.range);
|
||||
|
||||
// root now has a tokens array
|
||||
console.dir(ast.tokens);
|
||||
```
|
||||
|
||||
### Tokenizing
|
||||
|
||||
To produce JSON tokens from a string, use the `tokenize()` function:
|
||||
|
||||
```js
|
||||
const { tokenize } = require("@humanwhocodes/momoa");
|
||||
const json = "{\"foo\":\"bar\"}";
|
||||
|
||||
for (const token of tokenize(json)) {
|
||||
console.log("Token type is", token.type);
|
||||
|
||||
const start = token.loc.start.offset;
|
||||
const end = token.loc.end.offset;
|
||||
console.log("Token value is", json.slice(start, end));
|
||||
}
|
||||
```
|
||||
|
||||
The `tokenize()` function accepts a second parameter, which is an options object that may contain one or more of the following properties:
|
||||
|
||||
* `mode` (default: `"json"`) - specify the parsing mode. Possible options are `"json"`, `"jsonc"` (JSON with comments), and `"json5"`.
|
||||
* `ranges` (default: `false`) - set to `true` if you want each token to also have a `range` property, which is an array containing the start and stop index for the syntax within the source string.
|
||||
|
||||
### Traversing
|
||||
|
||||
There are two ways to traverse an AST: iteration and traditional traversal.
|
||||
|
||||
#### Iterating
|
||||
|
||||
Iteration uses a generator function to create an iterator over the AST:
|
||||
|
||||
```js
|
||||
const { parse, iterator } = require("@humanwhocodes/momoa");
|
||||
|
||||
const ast = parse(some_json_string);
|
||||
|
||||
for (const { node, parent, phase } of iterator(ast)) {
|
||||
console.log(node.type);
|
||||
console.log(phase); // "enter" or "exit"
|
||||
}
|
||||
```
|
||||
|
||||
Each step of the iterator returns an object with three properties:
|
||||
|
||||
1. `node` - the node that the traversal is currently visiting
|
||||
1. `parent` - the parent node of `node`
|
||||
1. `phase` - a string indicating the phase of traversal (`"enter"` when first visiting the node, `"exit"` when leaving the node)
|
||||
|
||||
You can also filter the iterator by passing in a filter function. For instance, if you only want steps to be returned in the `"enter"` phase, you can do this:
|
||||
|
||||
```js
|
||||
const { parse, iterator } = require("@humanwhocodes/momoa");
|
||||
|
||||
const ast = parse(some_json_string);
|
||||
|
||||
for (const { node } of iterator(ast, ({ phase }) => phase === "enter")) {
|
||||
console.log(node.type);
|
||||
}
|
||||
```
|
||||
|
||||
#### Traversing
|
||||
|
||||
Traversing uses a function that accepts an object with `enter` and `exit` properties:
|
||||
|
||||
```js
|
||||
const { parse, traverse } = require("@humanwhocodes/momoa");
|
||||
|
||||
const ast = parse(some_json_string);
|
||||
|
||||
traverse(ast, {
|
||||
enter(node, parent) {
|
||||
console.log("Entering", node.type);
|
||||
},
|
||||
exit(node, parent) {
|
||||
console.log("Exiting", node.type);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## Evaluating
|
||||
|
||||
To convert an AST into the JavaScript value it represents, use the `evaluate()` function:
|
||||
|
||||
```js
|
||||
const { parse, evaluate } = require("@humanwhocodes/momoa");
|
||||
|
||||
// same as JSON.parse(some_json_string)
|
||||
const ast = parse(some_json_string);
|
||||
const value = evaluate(ast);
|
||||
```
|
||||
|
||||
In this example, `value` is the same result you would get from calling `JSON.parse(some_json_string)` (`ast` is the intermediate format representing the syntax).
|
||||
|
||||
### Printing
|
||||
|
||||
To convert an AST back into a JSON string, use the `print()` function:
|
||||
|
||||
```js
|
||||
const { parse, print } = require("@humanwhocodes/momoa");
|
||||
|
||||
const ast = parse(some_json_string);
|
||||
const text = print(ast);
|
||||
```
|
||||
|
||||
**Note:** The printed AST will not produce the same result as the original JSON text as the AST does not preserve whitespace.
|
||||
|
||||
You can modify the output of the `print()` function by passing in an object with an `indent` option specifying the number of spaces to use for indentation. When the `indent` option is passed, the text produced will automatically have newlines insert after each `{`, `}`, `[`, `]`, and `,` characters.
|
||||
|
||||
```js
|
||||
const { parse, print } = require("@humanwhocodes/momoa");
|
||||
|
||||
const ast = parse(some_json_string);
|
||||
const text = print(ast, { indent: 4 });
|
||||
```
|
||||
|
||||
### Visitor Keys
|
||||
|
||||
Momoa also exports a map of traversable properties in AST nodes that is helpful if you'd like to traverse an AST manually. This is a map where the keys are the `type` property of each AST node and the values are an array of property names to traverse.
|
||||
|
||||
```js
|
||||
const { visitorKeys } = require("@humanwhocodes/momoa");
|
||||
|
||||
console.log(visitorKeys.get("Document")); // "body"
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
To work on Momoa, you'll need:
|
||||
|
||||
* [Git](https://git-scm.com/)
|
||||
* [Node.js](https://nodejs.org)
|
||||
|
||||
Make sure both are installed by visiting the links and following the instructions to install.
|
||||
|
||||
Now you're ready to clone the repository:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/humanwhocodes/momoa.git
|
||||
```
|
||||
|
||||
Then, enter the directory and install the dependencies:
|
||||
|
||||
```bash
|
||||
cd momoa/js
|
||||
npm install
|
||||
```
|
||||
|
||||
After that, you can run the tests via:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
**Note:** Momoa builds itself into a single file for deployment. The `npm test` command automatically rebuilds Momoa into that single file whenever it is run. If you are testing in a different way, then you may need to manually rebuild using the `npm run build` command.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
This project takes inspiration (but not code) from a number of other projects:
|
||||
|
||||
* [`Esprima`](https://esprima.org) inspired the package interface and AST format.
|
||||
* [`json-to-ast`](https://github.com/vtrushin/json-to-ast) inspired the AST format.
|
||||
* [`parseJson.js`](https://gist.github.com/rgrove/5cc64db4b9ae8c946401b230ba9d2451) inspired me by showing writing a parser isn't all that hard.
|
||||
|
||||
## License
|
||||
|
||||
Apache 2.0
|
||||
|
||||
## Frequently Asked Questions
|
||||
|
||||
### What does "Momoa" even mean?
|
||||
|
||||
Momoa is the last name of American actor [Jason Momoa](https://en.wikipedia.org/wiki/Jason_Momoa). Because "JSON" is pronounced "Jason", I wanted a name that played off of this fact. The most obvious choice would have been something related to [Jason and the Argonauts](https://en.wikipedia.org/wiki/Jason_and_the_Argonauts_(1963_film)), as this movie is referenced in the [JSON specification](https://ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf) directly. However, both "Argo" and "Argonaut" were already used for open source projects. When I did a search for "Jason" online, Jason Momoa was the first result that came up. He always plays badass characters so it seemed to fit.
|
||||
|
||||
### Why support comments in JSON?
|
||||
|
||||
There are a number of programs that allow C-style comments in JSON files, most notably, configuration files for [Visual Studio Code](https://code.visualstudio.com). As there seems to be a need for this functionality, I decided to add it out-of-the-box.
|
||||
|
||||
[npm]: https://npmjs.com/
|
||||
[yarn]: https://yarnpkg.com/
|
||||
+2674
File diff suppressed because one or more lines are too long
+202
@@ -0,0 +1,202 @@
|
||||
export type FilterPredicate = (item: {
|
||||
node: Node;
|
||||
parent?: Node;
|
||||
phase: TraversalPhase;
|
||||
}, index: number, array: Array<{
|
||||
node: Node;
|
||||
parent?: Node;
|
||||
phase: TraversalPhase;
|
||||
}>) => boolean;
|
||||
export type AnyNode = import("./typedefs.cjs").AnyNode;
|
||||
export type JSONValue = import("./typedefs.cjs").JSONValue;
|
||||
export type TokenType = import("./typedefs.cjs").TokenType;
|
||||
export type Location = import("./typedefs.cjs").Location;
|
||||
export type Token = import("./typedefs.cjs").Token;
|
||||
export type Range = import("./typedefs.cjs").Range;
|
||||
export type TokenizeOptions = import("./typedefs.cjs").TokenizeOptions;
|
||||
export type LocationRange = import("./typedefs.cjs").LocationRange;
|
||||
export type NodeParts = import("./typedefs.cjs").NodeParts;
|
||||
export type DocumentNode = import("./typedefs.cjs").DocumentNode;
|
||||
export type StringNode = import("./typedefs.cjs").StringNode;
|
||||
export type NumberNode = import("./typedefs.cjs").NumberNode;
|
||||
export type BooleanNode = import("./typedefs.cjs").BooleanNode;
|
||||
export type MemberNode = import("./typedefs.cjs").MemberNode;
|
||||
export type ObjectNode = import("./typedefs.cjs").ObjectNode;
|
||||
export type ElementNode = import("./typedefs.cjs").ElementNode;
|
||||
export type ArrayNode = import("./typedefs.cjs").ArrayNode;
|
||||
export type NullNode = import("./typedefs.cjs").NullNode;
|
||||
export type ValueNode = import("./typedefs.cjs").ValueNode;
|
||||
export type IdentifierNode = import("./typedefs.cjs").IdentifierNode;
|
||||
export type NaNNode = import("./typedefs.cjs").NaNNode;
|
||||
export type InfinityNode = import("./typedefs.cjs").InfinityNode;
|
||||
export type Sign = import("./typedefs.cjs").Sign;
|
||||
export type Node = import("./typedefs.cjs").Node;
|
||||
export type Mode = import("./typedefs.cjs").Mode;
|
||||
export type ParseOptions = import("./typedefs.cjs").ParseOptions;
|
||||
export type TraversalPhase = import("./typedefs.cjs").TraversalPhase;
|
||||
export type TraversalVisitor = {
|
||||
enter?: (node: Node, parent?: Node) => void;
|
||||
exit?: (node: Node, parent?: Node) => void;
|
||||
};
|
||||
/**
|
||||
* @fileoverview Evaluator for Momoa AST.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
/** @typedef {import("./typedefs.cjs").AnyNode} AnyNode */
|
||||
/** @typedef {import("./typedefs.cjs").JSONValue} JSONValue */
|
||||
/**
|
||||
* Evaluates a Momoa AST node into a JavaScript value.
|
||||
* @param {AnyNode} node The node to interpet.
|
||||
* @returns {JSONValue} The JavaScript value for the node.
|
||||
*/
|
||||
export function evaluate(node: AnyNode): JSONValue;
|
||||
/**
|
||||
* @callback FilterPredicate
|
||||
* @param {{node: Node, parent?: Node, phase: TraversalPhase}} item
|
||||
* @param {number} index
|
||||
* @param {Array<{node: Node, parent?: Node, phase: TraversalPhase}>} array
|
||||
* @returns {boolean}
|
||||
*/
|
||||
/**
|
||||
* Creates an iterator over the given AST.
|
||||
* @param {Node} root The root AST node to traverse.
|
||||
* @param {FilterPredicate} [filter] A filter function to determine which steps to
|
||||
* return;
|
||||
* @returns {IterableIterator<{node: Node, parent?: Node, phase: TraversalPhase}>} An iterator over the AST.
|
||||
*/
|
||||
export function iterator(root: Node, filter?: FilterPredicate): IterableIterator<{
|
||||
node: Node;
|
||||
parent?: Node;
|
||||
phase: TraversalPhase;
|
||||
}>;
|
||||
/**
|
||||
*
|
||||
* @param {string} text The text to parse.
|
||||
* @param {ParseOptions} [options] The options object.
|
||||
* @returns {DocumentNode} The AST representing the parsed JSON.
|
||||
* @throws {Error} When there is a parsing error.
|
||||
*/
|
||||
export function parse(text: string, options?: ParseOptions): DocumentNode;
|
||||
/**
|
||||
* Converts a Momoa AST back into a JSON string.
|
||||
* @param {AnyNode} node The node to print.
|
||||
* @param {Object} options Options for the print.
|
||||
* @param {number} [options.indent=0] The number of spaces to indent each line. If
|
||||
* greater than 0, then newlines and indents will be added to output.
|
||||
* @returns {string} The JSON representation of the AST.
|
||||
*/
|
||||
export function print(node: AnyNode, { indent }?: {
|
||||
indent?: number;
|
||||
}): string;
|
||||
/**
|
||||
* Creates an iterator over the tokens representing the source text.
|
||||
* @param {string} text The source text to tokenize.
|
||||
* @param {TokenizeOptions} [options] Options for doing the tokenization.
|
||||
* @returns {Array<Token>} An iterator over the tokens.
|
||||
*/
|
||||
export function tokenize(text: string, options?: TokenizeOptions): Array<Token>;
|
||||
/**
|
||||
* Traverses an AST from the given node.
|
||||
* @param {Node} root The node to traverse from
|
||||
* @param {TraversalVisitor} visitor An object with an `enter` and `exit` method.
|
||||
*/
|
||||
export function traverse(root: Node, visitor: TraversalVisitor): void;
|
||||
export namespace types {
|
||||
/**
|
||||
* Creates a document node.
|
||||
* @param {ValueNode} body The body of the document.
|
||||
* @param {NodeParts} parts Additional properties for the node.
|
||||
* @returns {DocumentNode} The document node.
|
||||
*/
|
||||
export function document(body: ValueNode, parts?: NodeParts): DocumentNode;
|
||||
/**
|
||||
* Creates a string node.
|
||||
* @param {string} value The value for the string.
|
||||
* @param {NodeParts} parts Additional properties for the node.
|
||||
* @returns {StringNode} The string node.
|
||||
*/
|
||||
export function string(value: string, parts?: NodeParts): StringNode;
|
||||
/**
|
||||
* Creates a number node.
|
||||
* @param {number} value The value for the number.
|
||||
* @param {NodeParts} parts Additional properties for the node.
|
||||
* @returns {NumberNode} The number node.
|
||||
*/
|
||||
export function number(value: number, parts?: NodeParts): NumberNode;
|
||||
/**
|
||||
* Creates a boolean node.
|
||||
* @param {boolean} value The value for the boolean.
|
||||
* @param {NodeParts} parts Additional properties for the node.
|
||||
* @returns {BooleanNode} The boolean node.
|
||||
*/
|
||||
export function boolean(value: boolean, parts?: NodeParts): BooleanNode;
|
||||
/**
|
||||
* Creates a null node.
|
||||
* @param {NodeParts} parts Additional properties for the node.
|
||||
* @returns {NullNode} The null node.
|
||||
*/
|
||||
function _null(parts?: NodeParts): NullNode;
|
||||
export { _null as null };
|
||||
/**
|
||||
* Creates an array node.
|
||||
* @param {Array<ElementNode>} elements The elements to add.
|
||||
* @param {NodeParts} parts Additional properties for the node.
|
||||
* @returns {ArrayNode} The array node.
|
||||
*/
|
||||
export function array(elements: Array<ElementNode>, parts?: NodeParts): ArrayNode;
|
||||
/**
|
||||
* Creates an element node.
|
||||
* @param {ValueNode} value The value for the element.
|
||||
* @param {NodeParts} parts Additional properties for the node.
|
||||
* @returns {ElementNode} The element node.
|
||||
*/
|
||||
export function element(value: ValueNode, parts?: NodeParts): ElementNode;
|
||||
/**
|
||||
* Creates an object node.
|
||||
* @param {Array<MemberNode>} members The members to add.
|
||||
* @param {NodeParts} parts Additional properties for the node.
|
||||
* @returns {ObjectNode} The object node.
|
||||
*/
|
||||
export function object(members: Array<MemberNode>, parts?: NodeParts): ObjectNode;
|
||||
/**
|
||||
* Creates a member node.
|
||||
* @param {StringNode|IdentifierNode} name The name for the member.
|
||||
* @param {ValueNode} value The value for the member.
|
||||
* @param {NodeParts} parts Additional properties for the node.
|
||||
* @returns {MemberNode} The member node.
|
||||
*/
|
||||
export function member(name: StringNode | IdentifierNode, value: ValueNode, parts?: NodeParts): MemberNode;
|
||||
/**
|
||||
* Creates an identifier node.
|
||||
* @param {string} name The name for the identifier.
|
||||
* @param {NodeParts} parts Additional properties for the node.
|
||||
* @returns {IdentifierNode} The identifier node.
|
||||
*/
|
||||
export function identifier(name: string, parts?: NodeParts): IdentifierNode;
|
||||
/**
|
||||
* Creates a NaN node.
|
||||
* @param {Sign} sign The sign for the Infinity.
|
||||
* @param {NodeParts} parts Additional properties for the node.
|
||||
* @returns {NaNNode} The NaN node.
|
||||
*/
|
||||
export function nan(sign?: Sign, parts?: NodeParts): NaNNode;
|
||||
/**
|
||||
* Creates an Infinity node.
|
||||
* @param {Sign} sign The sign for the Infinity.
|
||||
* @param {NodeParts} parts Additional properties for the node.
|
||||
* @returns {InfinityNode} The Infinity node.
|
||||
*/
|
||||
export function infinity(sign?: Sign, parts?: NodeParts): InfinityNode;
|
||||
}
|
||||
/**
|
||||
* @fileoverview Traversal approaches for Momoa JSON AST.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
/** @typedef {import("./typedefs.cjs").TraversalPhase} TraversalPhase */
|
||||
/**
|
||||
* @typedef {Object} TraversalVisitor
|
||||
* @property {(node: Node, parent?: Node) => void} [enter]
|
||||
* @property {(node: Node, parent?: Node) => void} [exit]
|
||||
*/
|
||||
declare const childKeys: Map<string, string[]>;
|
||||
export { childKeys as visitorKeys };
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
export type FilterPredicate = (item: {
|
||||
node: Node;
|
||||
parent?: Node;
|
||||
phase: TraversalPhase;
|
||||
}, index: number, array: Array<{
|
||||
node: Node;
|
||||
parent?: Node;
|
||||
phase: TraversalPhase;
|
||||
}>) => boolean;
|
||||
export type AnyNode = import("./typedefs.js").AnyNode;
|
||||
export type JSONValue = import("./typedefs.js").JSONValue;
|
||||
export type TokenType = import("./typedefs.js").TokenType;
|
||||
export type Location = import("./typedefs.js").Location;
|
||||
export type Token = import("./typedefs.js").Token;
|
||||
export type Range = import("./typedefs.js").Range;
|
||||
export type TokenizeOptions = import("./typedefs.js").TokenizeOptions;
|
||||
export type LocationRange = import("./typedefs.js").LocationRange;
|
||||
export type NodeParts = import("./typedefs.js").NodeParts;
|
||||
export type DocumentNode = import("./typedefs.js").DocumentNode;
|
||||
export type StringNode = import("./typedefs.js").StringNode;
|
||||
export type NumberNode = import("./typedefs.js").NumberNode;
|
||||
export type BooleanNode = import("./typedefs.js").BooleanNode;
|
||||
export type MemberNode = import("./typedefs.js").MemberNode;
|
||||
export type ObjectNode = import("./typedefs.js").ObjectNode;
|
||||
export type ElementNode = import("./typedefs.js").ElementNode;
|
||||
export type ArrayNode = import("./typedefs.js").ArrayNode;
|
||||
export type NullNode = import("./typedefs.js").NullNode;
|
||||
export type ValueNode = import("./typedefs.js").ValueNode;
|
||||
export type IdentifierNode = import("./typedefs.js").IdentifierNode;
|
||||
export type NaNNode = import("./typedefs.js").NaNNode;
|
||||
export type InfinityNode = import("./typedefs.js").InfinityNode;
|
||||
export type Sign = import("./typedefs.js").Sign;
|
||||
export type Node = import("./typedefs.js").Node;
|
||||
export type Mode = import("./typedefs.js").Mode;
|
||||
export type ParseOptions = import("./typedefs.js").ParseOptions;
|
||||
export type TraversalPhase = import("./typedefs.js").TraversalPhase;
|
||||
export type TraversalVisitor = {
|
||||
enter?: (node: Node, parent?: Node) => void;
|
||||
exit?: (node: Node, parent?: Node) => void;
|
||||
};
|
||||
/**
|
||||
* @fileoverview Evaluator for Momoa AST.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
/** @typedef {import("./typedefs.js").AnyNode} AnyNode */
|
||||
/** @typedef {import("./typedefs.js").JSONValue} JSONValue */
|
||||
/**
|
||||
* Evaluates a Momoa AST node into a JavaScript value.
|
||||
* @param {AnyNode} node The node to interpet.
|
||||
* @returns {JSONValue} The JavaScript value for the node.
|
||||
*/
|
||||
export function evaluate(node: AnyNode): JSONValue;
|
||||
/**
|
||||
* @callback FilterPredicate
|
||||
* @param {{node: Node, parent?: Node, phase: TraversalPhase}} item
|
||||
* @param {number} index
|
||||
* @param {Array<{node: Node, parent?: Node, phase: TraversalPhase}>} array
|
||||
* @returns {boolean}
|
||||
*/
|
||||
/**
|
||||
* Creates an iterator over the given AST.
|
||||
* @param {Node} root The root AST node to traverse.
|
||||
* @param {FilterPredicate} [filter] A filter function to determine which steps to
|
||||
* return;
|
||||
* @returns {IterableIterator<{node: Node, parent?: Node, phase: TraversalPhase}>} An iterator over the AST.
|
||||
*/
|
||||
export function iterator(root: Node, filter?: FilterPredicate): IterableIterator<{
|
||||
node: Node;
|
||||
parent?: Node;
|
||||
phase: TraversalPhase;
|
||||
}>;
|
||||
/**
|
||||
*
|
||||
* @param {string} text The text to parse.
|
||||
* @param {ParseOptions} [options] The options object.
|
||||
* @returns {DocumentNode} The AST representing the parsed JSON.
|
||||
* @throws {Error} When there is a parsing error.
|
||||
*/
|
||||
export function parse(text: string, options?: ParseOptions): DocumentNode;
|
||||
/**
|
||||
* Converts a Momoa AST back into a JSON string.
|
||||
* @param {AnyNode} node The node to print.
|
||||
* @param {Object} options Options for the print.
|
||||
* @param {number} [options.indent=0] The number of spaces to indent each line. If
|
||||
* greater than 0, then newlines and indents will be added to output.
|
||||
* @returns {string} The JSON representation of the AST.
|
||||
*/
|
||||
export function print(node: AnyNode, { indent }?: {
|
||||
indent?: number;
|
||||
}): string;
|
||||
/**
|
||||
* Creates an iterator over the tokens representing the source text.
|
||||
* @param {string} text The source text to tokenize.
|
||||
* @param {TokenizeOptions} [options] Options for doing the tokenization.
|
||||
* @returns {Array<Token>} An iterator over the tokens.
|
||||
*/
|
||||
export function tokenize(text: string, options?: TokenizeOptions): Array<Token>;
|
||||
/**
|
||||
* Traverses an AST from the given node.
|
||||
* @param {Node} root The node to traverse from
|
||||
* @param {TraversalVisitor} visitor An object with an `enter` and `exit` method.
|
||||
*/
|
||||
export function traverse(root: Node, visitor: TraversalVisitor): void;
|
||||
export namespace types {
|
||||
/**
|
||||
* Creates a document node.
|
||||
* @param {ValueNode} body The body of the document.
|
||||
* @param {NodeParts} parts Additional properties for the node.
|
||||
* @returns {DocumentNode} The document node.
|
||||
*/
|
||||
export function document(body: ValueNode, parts?: NodeParts): DocumentNode;
|
||||
/**
|
||||
* Creates a string node.
|
||||
* @param {string} value The value for the string.
|
||||
* @param {NodeParts} parts Additional properties for the node.
|
||||
* @returns {StringNode} The string node.
|
||||
*/
|
||||
export function string(value: string, parts?: NodeParts): StringNode;
|
||||
/**
|
||||
* Creates a number node.
|
||||
* @param {number} value The value for the number.
|
||||
* @param {NodeParts} parts Additional properties for the node.
|
||||
* @returns {NumberNode} The number node.
|
||||
*/
|
||||
export function number(value: number, parts?: NodeParts): NumberNode;
|
||||
/**
|
||||
* Creates a boolean node.
|
||||
* @param {boolean} value The value for the boolean.
|
||||
* @param {NodeParts} parts Additional properties for the node.
|
||||
* @returns {BooleanNode} The boolean node.
|
||||
*/
|
||||
export function boolean(value: boolean, parts?: NodeParts): BooleanNode;
|
||||
/**
|
||||
* Creates a null node.
|
||||
* @param {NodeParts} parts Additional properties for the node.
|
||||
* @returns {NullNode} The null node.
|
||||
*/
|
||||
function _null(parts?: NodeParts): NullNode;
|
||||
export { _null as null };
|
||||
/**
|
||||
* Creates an array node.
|
||||
* @param {Array<ElementNode>} elements The elements to add.
|
||||
* @param {NodeParts} parts Additional properties for the node.
|
||||
* @returns {ArrayNode} The array node.
|
||||
*/
|
||||
export function array(elements: Array<ElementNode>, parts?: NodeParts): ArrayNode;
|
||||
/**
|
||||
* Creates an element node.
|
||||
* @param {ValueNode} value The value for the element.
|
||||
* @param {NodeParts} parts Additional properties for the node.
|
||||
* @returns {ElementNode} The element node.
|
||||
*/
|
||||
export function element(value: ValueNode, parts?: NodeParts): ElementNode;
|
||||
/**
|
||||
* Creates an object node.
|
||||
* @param {Array<MemberNode>} members The members to add.
|
||||
* @param {NodeParts} parts Additional properties for the node.
|
||||
* @returns {ObjectNode} The object node.
|
||||
*/
|
||||
export function object(members: Array<MemberNode>, parts?: NodeParts): ObjectNode;
|
||||
/**
|
||||
* Creates a member node.
|
||||
* @param {StringNode|IdentifierNode} name The name for the member.
|
||||
* @param {ValueNode} value The value for the member.
|
||||
* @param {NodeParts} parts Additional properties for the node.
|
||||
* @returns {MemberNode} The member node.
|
||||
*/
|
||||
export function member(name: StringNode | IdentifierNode, value: ValueNode, parts?: NodeParts): MemberNode;
|
||||
/**
|
||||
* Creates an identifier node.
|
||||
* @param {string} name The name for the identifier.
|
||||
* @param {NodeParts} parts Additional properties for the node.
|
||||
* @returns {IdentifierNode} The identifier node.
|
||||
*/
|
||||
export function identifier(name: string, parts?: NodeParts): IdentifierNode;
|
||||
/**
|
||||
* Creates a NaN node.
|
||||
* @param {Sign} sign The sign for the Infinity.
|
||||
* @param {NodeParts} parts Additional properties for the node.
|
||||
* @returns {NaNNode} The NaN node.
|
||||
*/
|
||||
export function nan(sign?: Sign, parts?: NodeParts): NaNNode;
|
||||
/**
|
||||
* Creates an Infinity node.
|
||||
* @param {Sign} sign The sign for the Infinity.
|
||||
* @param {NodeParts} parts Additional properties for the node.
|
||||
* @returns {InfinityNode} The Infinity node.
|
||||
*/
|
||||
export function infinity(sign?: Sign, parts?: NodeParts): InfinityNode;
|
||||
}
|
||||
/**
|
||||
* @fileoverview Traversal approaches for Momoa JSON AST.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
/** @typedef {import("./typedefs.js").TraversalPhase} TraversalPhase */
|
||||
/**
|
||||
* @typedef {Object} TraversalVisitor
|
||||
* @property {(node: Node, parent?: Node) => void} [enter]
|
||||
* @property {(node: Node, parent?: Node) => void} [exit]
|
||||
*/
|
||||
declare const childKeys: Map<string, string[]>;
|
||||
export { childKeys as visitorKeys };
|
||||
+2619
File diff suppressed because one or more lines are too long
+205
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* @fileoverview Type definitions for the Momoa JSON parser.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
/**
|
||||
* The mode that Momoa runs in:
|
||||
* - "json" for regular JSON
|
||||
* - "jsonc" for JSON with C-style comments
|
||||
*/
|
||||
export type Mode = "json" | "jsonc" | "json5";
|
||||
/**
|
||||
* The phase of the traversal step.
|
||||
*/
|
||||
export type TraversalPhase = "enter" | "exit";
|
||||
/**
|
||||
* The type of a JSON5 Infinity or NaN value.
|
||||
* - "+" for positive Infinity or postive NaN
|
||||
* - "-" for negative Infinity or negative NaN
|
||||
* - "" for Infinity or NaN without a sign
|
||||
*/
|
||||
export type Sign = "+" | "-" | "";
|
||||
/**
|
||||
* Tokenization options.
|
||||
*/
|
||||
export interface TokenizeOptions {
|
||||
/**
|
||||
* The mode to tokenize in.
|
||||
*/
|
||||
readonly mode?: Mode;
|
||||
/**
|
||||
* When true, includes the `range` key on each token.
|
||||
*/
|
||||
readonly ranges?: boolean;
|
||||
}
|
||||
/**
|
||||
* Parse options.
|
||||
*/
|
||||
export interface ParseOptions {
|
||||
/**
|
||||
* The mode to parse in.
|
||||
*/
|
||||
readonly mode?: Mode;
|
||||
/**
|
||||
* When true, includes the `range` key on each node and token.
|
||||
*/
|
||||
readonly ranges?: boolean;
|
||||
/**
|
||||
* When true, includes the `tokens` key on the document node containing
|
||||
* all of the tokens used during parsing.
|
||||
*/
|
||||
readonly tokens?: boolean;
|
||||
/**
|
||||
* When true, allows trailing commas in arrays and objects. Defaults to
|
||||
* false for JSON and JSONC modes, and true for JSON5 mode.
|
||||
*/
|
||||
readonly allowTrailingCommas?: boolean;
|
||||
}
|
||||
export interface Node {
|
||||
type: string;
|
||||
loc: LocationRange;
|
||||
range?: Range;
|
||||
}
|
||||
/**
|
||||
* The root node of a JSON document.
|
||||
*/
|
||||
export interface DocumentNode extends Node {
|
||||
type: "Document";
|
||||
body: ValueNode;
|
||||
tokens?: Array<Token>;
|
||||
}
|
||||
export interface NullNode extends Node {
|
||||
type: "Null";
|
||||
}
|
||||
interface LiteralNode<T> extends Node {
|
||||
value: T;
|
||||
}
|
||||
/**
|
||||
* Represents a JSON5 NaN value.
|
||||
*/
|
||||
export interface NaNNode extends Node {
|
||||
type: "NaN";
|
||||
sign: Sign;
|
||||
}
|
||||
/**
|
||||
* Represents a JSON5 Infinity value.
|
||||
*/
|
||||
export interface InfinityNode extends Node {
|
||||
type: "Infinity";
|
||||
sign: Sign;
|
||||
}
|
||||
/**
|
||||
* Represents a JSON identifier.
|
||||
*/
|
||||
export interface IdentifierNode extends Node {
|
||||
type: "Identifier";
|
||||
name: string;
|
||||
}
|
||||
/**
|
||||
* Represents a JSON string.
|
||||
*/
|
||||
export interface StringNode extends LiteralNode<string> {
|
||||
type: "String";
|
||||
}
|
||||
/**
|
||||
* Represents a JSON number.
|
||||
*/
|
||||
export interface NumberNode extends LiteralNode<number> {
|
||||
type: "Number";
|
||||
}
|
||||
/**
|
||||
* Represents a JSON boolean.
|
||||
*/
|
||||
export interface BooleanNode extends LiteralNode<boolean> {
|
||||
type: "Boolean";
|
||||
}
|
||||
/**
|
||||
* Represents an element of a JSON array.
|
||||
*/
|
||||
export interface ElementNode extends Node {
|
||||
type: "Element";
|
||||
value: ValueNode;
|
||||
}
|
||||
/**
|
||||
* Represents a JSON array.
|
||||
*/
|
||||
export interface ArrayNode extends Node {
|
||||
type: "Array";
|
||||
elements: Array<ElementNode>;
|
||||
}
|
||||
/**
|
||||
* Represents a member of a JSON object.
|
||||
*/
|
||||
export interface MemberNode extends Node {
|
||||
type: "Member";
|
||||
name: StringNode | IdentifierNode;
|
||||
value: ValueNode;
|
||||
}
|
||||
/**
|
||||
* Represents a JSON object.
|
||||
*/
|
||||
export interface ObjectNode extends Node {
|
||||
type: "Object";
|
||||
members: Array<MemberNode>;
|
||||
}
|
||||
/**
|
||||
* Any node that represents a JSON value.
|
||||
*/
|
||||
export type ValueNode = ArrayNode | ObjectNode | BooleanNode | StringNode | NumberNode | NullNode | NaNNode | InfinityNode;
|
||||
/**
|
||||
* Any node that represents the container for a JSON value.
|
||||
*/
|
||||
export type ContainerNode = DocumentNode | MemberNode | ElementNode;
|
||||
/**
|
||||
* Any node that represents a JSON5 extension.
|
||||
*/
|
||||
export type JSON5ExtensionNode = NaNNode | InfinityNode | IdentifierNode;
|
||||
/**
|
||||
* Any valid AST node.
|
||||
*/
|
||||
export type AnyNode = ValueNode | ContainerNode | JSON5ExtensionNode;
|
||||
/**
|
||||
* Additional information about an AST node.
|
||||
*/
|
||||
export interface NodeParts {
|
||||
loc?: LocationRange;
|
||||
range?: Range;
|
||||
}
|
||||
/**
|
||||
* Values that can be represented in JSON.
|
||||
*/
|
||||
export type JSONValue = Array<JSONValue> | boolean | number | string | {
|
||||
[property: string]: JSONValue;
|
||||
} | null;
|
||||
/**
|
||||
* A token used to during JSON parsing.
|
||||
*/
|
||||
export interface Token {
|
||||
type: TokenType;
|
||||
loc: LocationRange;
|
||||
range?: Range;
|
||||
}
|
||||
/**
|
||||
* The type of token.
|
||||
*/
|
||||
export type TokenType = "Number" | "String" | "Boolean" | "Colon" | "LBrace" | "RBrace" | "RBracket" | "LBracket" | "Comma" | "Null" | "LineComment" | "BlockComment" | "NaN" | "Infinity" | "Identifier";
|
||||
/**
|
||||
* The start and stop location for a token or node inside the source text.
|
||||
*/
|
||||
export interface LocationRange {
|
||||
start: Location;
|
||||
end: Location;
|
||||
}
|
||||
/**
|
||||
* A cursor location inside the source text.
|
||||
*/
|
||||
export interface Location {
|
||||
line: number;
|
||||
column: number;
|
||||
offset: number;
|
||||
}
|
||||
/**
|
||||
* The start and stop offset for a given node or token inside the source text.
|
||||
*/
|
||||
export type Range = [number, number];
|
||||
export {};
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* @fileoverview Type definitions for the Momoa JSON parser.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
/**
|
||||
* The mode that Momoa runs in:
|
||||
* - "json" for regular JSON
|
||||
* - "jsonc" for JSON with C-style comments
|
||||
*/
|
||||
export type Mode = "json" | "jsonc" | "json5";
|
||||
/**
|
||||
* The phase of the traversal step.
|
||||
*/
|
||||
export type TraversalPhase = "enter" | "exit";
|
||||
/**
|
||||
* The type of a JSON5 Infinity or NaN value.
|
||||
* - "+" for positive Infinity or postive NaN
|
||||
* - "-" for negative Infinity or negative NaN
|
||||
* - "" for Infinity or NaN without a sign
|
||||
*/
|
||||
export type Sign = "+" | "-" | "";
|
||||
/**
|
||||
* Tokenization options.
|
||||
*/
|
||||
export interface TokenizeOptions {
|
||||
/**
|
||||
* The mode to tokenize in.
|
||||
*/
|
||||
readonly mode?: Mode;
|
||||
/**
|
||||
* When true, includes the `range` key on each token.
|
||||
*/
|
||||
readonly ranges?: boolean;
|
||||
}
|
||||
/**
|
||||
* Parse options.
|
||||
*/
|
||||
export interface ParseOptions {
|
||||
/**
|
||||
* The mode to parse in.
|
||||
*/
|
||||
readonly mode?: Mode;
|
||||
/**
|
||||
* When true, includes the `range` key on each node and token.
|
||||
*/
|
||||
readonly ranges?: boolean;
|
||||
/**
|
||||
* When true, includes the `tokens` key on the document node containing
|
||||
* all of the tokens used during parsing.
|
||||
*/
|
||||
readonly tokens?: boolean;
|
||||
/**
|
||||
* When true, allows trailing commas in arrays and objects. Defaults to
|
||||
* false for JSON and JSONC modes, and true for JSON5 mode.
|
||||
*/
|
||||
readonly allowTrailingCommas?: boolean;
|
||||
}
|
||||
export interface Node {
|
||||
type: string;
|
||||
loc: LocationRange;
|
||||
range?: Range;
|
||||
}
|
||||
/**
|
||||
* The root node of a JSON document.
|
||||
*/
|
||||
export interface DocumentNode extends Node {
|
||||
type: "Document";
|
||||
body: ValueNode;
|
||||
tokens?: Array<Token>;
|
||||
}
|
||||
export interface NullNode extends Node {
|
||||
type: "Null";
|
||||
}
|
||||
interface LiteralNode<T> extends Node {
|
||||
value: T;
|
||||
}
|
||||
/**
|
||||
* Represents a JSON5 NaN value.
|
||||
*/
|
||||
export interface NaNNode extends Node {
|
||||
type: "NaN";
|
||||
sign: Sign;
|
||||
}
|
||||
/**
|
||||
* Represents a JSON5 Infinity value.
|
||||
*/
|
||||
export interface InfinityNode extends Node {
|
||||
type: "Infinity";
|
||||
sign: Sign;
|
||||
}
|
||||
/**
|
||||
* Represents a JSON identifier.
|
||||
*/
|
||||
export interface IdentifierNode extends Node {
|
||||
type: "Identifier";
|
||||
name: string;
|
||||
}
|
||||
/**
|
||||
* Represents a JSON string.
|
||||
*/
|
||||
export interface StringNode extends LiteralNode<string> {
|
||||
type: "String";
|
||||
}
|
||||
/**
|
||||
* Represents a JSON number.
|
||||
*/
|
||||
export interface NumberNode extends LiteralNode<number> {
|
||||
type: "Number";
|
||||
}
|
||||
/**
|
||||
* Represents a JSON boolean.
|
||||
*/
|
||||
export interface BooleanNode extends LiteralNode<boolean> {
|
||||
type: "Boolean";
|
||||
}
|
||||
/**
|
||||
* Represents an element of a JSON array.
|
||||
*/
|
||||
export interface ElementNode extends Node {
|
||||
type: "Element";
|
||||
value: ValueNode;
|
||||
}
|
||||
/**
|
||||
* Represents a JSON array.
|
||||
*/
|
||||
export interface ArrayNode extends Node {
|
||||
type: "Array";
|
||||
elements: Array<ElementNode>;
|
||||
}
|
||||
/**
|
||||
* Represents a member of a JSON object.
|
||||
*/
|
||||
export interface MemberNode extends Node {
|
||||
type: "Member";
|
||||
name: StringNode | IdentifierNode;
|
||||
value: ValueNode;
|
||||
}
|
||||
/**
|
||||
* Represents a JSON object.
|
||||
*/
|
||||
export interface ObjectNode extends Node {
|
||||
type: "Object";
|
||||
members: Array<MemberNode>;
|
||||
}
|
||||
/**
|
||||
* Any node that represents a JSON value.
|
||||
*/
|
||||
export type ValueNode = ArrayNode | ObjectNode | BooleanNode | StringNode | NumberNode | NullNode | NaNNode | InfinityNode;
|
||||
/**
|
||||
* Any node that represents the container for a JSON value.
|
||||
*/
|
||||
export type ContainerNode = DocumentNode | MemberNode | ElementNode;
|
||||
/**
|
||||
* Any node that represents a JSON5 extension.
|
||||
*/
|
||||
export type JSON5ExtensionNode = NaNNode | InfinityNode | IdentifierNode;
|
||||
/**
|
||||
* Any valid AST node.
|
||||
*/
|
||||
export type AnyNode = ValueNode | ContainerNode | JSON5ExtensionNode;
|
||||
/**
|
||||
* Additional information about an AST node.
|
||||
*/
|
||||
export interface NodeParts {
|
||||
loc?: LocationRange;
|
||||
range?: Range;
|
||||
}
|
||||
/**
|
||||
* Values that can be represented in JSON.
|
||||
*/
|
||||
export type JSONValue = Array<JSONValue> | boolean | number | string | {
|
||||
[property: string]: JSONValue;
|
||||
} | null;
|
||||
/**
|
||||
* A token used to during JSON parsing.
|
||||
*/
|
||||
export interface Token {
|
||||
type: TokenType;
|
||||
loc: LocationRange;
|
||||
range?: Range;
|
||||
}
|
||||
/**
|
||||
* The type of token.
|
||||
*/
|
||||
export type TokenType = "Number" | "String" | "Boolean" | "Colon" | "LBrace" | "RBrace" | "RBracket" | "LBracket" | "Comma" | "Null" | "LineComment" | "BlockComment" | "NaN" | "Infinity" | "Identifier";
|
||||
/**
|
||||
* The start and stop location for a token or node inside the source text.
|
||||
*/
|
||||
export interface LocationRange {
|
||||
start: Location;
|
||||
end: Location;
|
||||
}
|
||||
/**
|
||||
* A cursor location inside the source text.
|
||||
*/
|
||||
export interface Location {
|
||||
line: number;
|
||||
column: number;
|
||||
offset: number;
|
||||
}
|
||||
/**
|
||||
* The start and stop offset for a given node or token inside the source text.
|
||||
*/
|
||||
export type Range = [number, number];
|
||||
export {};
|
||||
+268
@@ -0,0 +1,268 @@
|
||||
/**
|
||||
* @fileoverview Type definitions for the Momoa JSON parser.
|
||||
* @author Nicholas C. Zakas
|
||||
*/
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Options
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* The mode that Momoa runs in:
|
||||
* - "json" for regular JSON
|
||||
* - "jsonc" for JSON with C-style comments
|
||||
*/
|
||||
export type Mode = "json" | "jsonc" | "json5";
|
||||
|
||||
/**
|
||||
* The phase of the traversal step.
|
||||
*/
|
||||
export type TraversalPhase = "enter" | "exit";
|
||||
|
||||
/**
|
||||
* The type of a JSON5 Infinity or NaN value.
|
||||
* - "+" for positive Infinity or postive NaN
|
||||
* - "-" for negative Infinity or negative NaN
|
||||
* - "" for Infinity or NaN without a sign
|
||||
*/
|
||||
export type Sign = "+" | "-" | "";
|
||||
|
||||
/**
|
||||
* Tokenization options.
|
||||
*/
|
||||
export interface TokenizeOptions {
|
||||
|
||||
/**
|
||||
* The mode to tokenize in.
|
||||
*/
|
||||
readonly mode?: Mode;
|
||||
|
||||
/**
|
||||
* When true, includes the `range` key on each token.
|
||||
*/
|
||||
readonly ranges?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse options.
|
||||
*/
|
||||
export interface ParseOptions {
|
||||
|
||||
/**
|
||||
* The mode to parse in.
|
||||
*/
|
||||
readonly mode?: Mode;
|
||||
|
||||
/**
|
||||
* When true, includes the `range` key on each node and token.
|
||||
*/
|
||||
readonly ranges?: boolean;
|
||||
|
||||
/**
|
||||
* When true, includes the `tokens` key on the document node containing
|
||||
* all of the tokens used during parsing.
|
||||
*/
|
||||
readonly tokens?: boolean;
|
||||
|
||||
/**
|
||||
* When true, allows trailing commas in arrays and objects. Defaults to
|
||||
* false for JSON and JSONC modes, and true for JSON5 mode.
|
||||
*/
|
||||
readonly allowTrailingCommas?: boolean;
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Nodes
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
export interface Node {
|
||||
type: string;
|
||||
loc: LocationRange;
|
||||
range?: Range;
|
||||
}
|
||||
|
||||
/**
|
||||
* The root node of a JSON document.
|
||||
*/
|
||||
export interface DocumentNode extends Node {
|
||||
type: "Document";
|
||||
body: ValueNode;
|
||||
tokens?: Array<Token>;
|
||||
}
|
||||
|
||||
export interface NullNode extends Node {
|
||||
type: "Null";
|
||||
}
|
||||
|
||||
interface LiteralNode<T> extends Node {
|
||||
value: T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a JSON5 NaN value.
|
||||
*/
|
||||
export interface NaNNode extends Node {
|
||||
type: "NaN";
|
||||
sign: Sign;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a JSON5 Infinity value.
|
||||
*/
|
||||
export interface InfinityNode extends Node {
|
||||
type: "Infinity";
|
||||
sign: Sign;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a JSON identifier.
|
||||
*/
|
||||
export interface IdentifierNode extends Node {
|
||||
type: "Identifier";
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a JSON string.
|
||||
*/
|
||||
export interface StringNode extends LiteralNode<string> {
|
||||
type: "String";
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a JSON number.
|
||||
*/
|
||||
export interface NumberNode extends LiteralNode<number> {
|
||||
type: "Number";
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a JSON boolean.
|
||||
*/
|
||||
export interface BooleanNode extends LiteralNode<boolean> {
|
||||
type: "Boolean";
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an element of a JSON array.
|
||||
*/
|
||||
export interface ElementNode extends Node {
|
||||
type: "Element";
|
||||
value: ValueNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a JSON array.
|
||||
*/
|
||||
export interface ArrayNode extends Node {
|
||||
type: "Array";
|
||||
elements: Array<ElementNode>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a member of a JSON object.
|
||||
*/
|
||||
export interface MemberNode extends Node {
|
||||
type: "Member";
|
||||
name: StringNode | IdentifierNode;
|
||||
value: ValueNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a JSON object.
|
||||
*/
|
||||
export interface ObjectNode extends Node {
|
||||
type: "Object";
|
||||
members: Array<MemberNode>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Any node that represents a JSON value.
|
||||
*/
|
||||
export type ValueNode = ArrayNode | ObjectNode |
|
||||
BooleanNode | StringNode | NumberNode | NullNode |
|
||||
NaNNode | InfinityNode;
|
||||
|
||||
/**
|
||||
* Any node that represents the container for a JSON value.
|
||||
*/
|
||||
export type ContainerNode = DocumentNode | MemberNode | ElementNode;
|
||||
|
||||
/**
|
||||
* Any node that represents a JSON5 extension.
|
||||
*/
|
||||
export type JSON5ExtensionNode = NaNNode | InfinityNode | IdentifierNode;
|
||||
|
||||
/**
|
||||
* Any valid AST node.
|
||||
*/
|
||||
export type AnyNode = ValueNode | ContainerNode | JSON5ExtensionNode;
|
||||
|
||||
/**
|
||||
* Additional information about an AST node.
|
||||
*/
|
||||
export interface NodeParts {
|
||||
loc?: LocationRange;
|
||||
range?: Range;
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Values
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Values that can be represented in JSON.
|
||||
*/
|
||||
export type JSONValue =
|
||||
| Array<JSONValue>
|
||||
| boolean
|
||||
| number
|
||||
| string
|
||||
| { [property: string]: JSONValue }
|
||||
| null;
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Tokens
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* A token used to during JSON parsing.
|
||||
*/
|
||||
export interface Token {
|
||||
type: TokenType;
|
||||
loc: LocationRange;
|
||||
range?: Range;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of token.
|
||||
*/
|
||||
export type TokenType = "Number" | "String" | "Boolean" | "Colon" | "LBrace" |
|
||||
"RBrace" | "RBracket" | "LBracket" | "Comma" | "Null" | "LineComment" |
|
||||
"BlockComment" | "NaN" | "Infinity" | "Identifier";
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Location Related
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* The start and stop location for a token or node inside the source text.
|
||||
*/
|
||||
export interface LocationRange {
|
||||
start: Location;
|
||||
end: Location;
|
||||
}
|
||||
|
||||
/**
|
||||
* A cursor location inside the source text.
|
||||
*/
|
||||
export interface Location {
|
||||
line: number;
|
||||
column: number;
|
||||
offset: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* The start and stop offset for a given node or token inside the source text.
|
||||
*/
|
||||
export type Range = [number, number];
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"name": "@humanwhocodes/momoa",
|
||||
"version": "3.3.10",
|
||||
"description": "JSON AST parser, tokenizer, printer, traverser.",
|
||||
"author": "Nicholas C. Zakas",
|
||||
"type": "module",
|
||||
"main": "dist/momoa.cjs",
|
||||
"module": "dist/momoa.js",
|
||||
"types": "dist/momoa.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"require": {
|
||||
"types": "./dist/momoa.d.cts",
|
||||
"default": "./dist/momoa.cjs"
|
||||
},
|
||||
"import": {
|
||||
"types": "./dist/momoa.d.ts",
|
||||
"default": "./dist/momoa.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/humanwhocodes/momoa.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/humanwhocodes/momoa/issues"
|
||||
},
|
||||
"homepage": "https://github.com/humanwhocodes/momoa#readme",
|
||||
"scripts": {
|
||||
"build": "rollup -c && npm run fixup && tsc -p tsconfig.build.json && npm run copy-dts && npm run build-dcts",
|
||||
"copy-dts": "node -e \"fs.copyFileSync('dist/momoa.d.ts', 'dist/momoa.d.cts')\"",
|
||||
"build-dcts": "node tools/update-cts-references.js",
|
||||
"fixup": "node tools/strip-typedef-aliases.js",
|
||||
"lint": "eslint *.js src/*.js tests/*.js",
|
||||
"perf": "npm run build && node tools/perf.js",
|
||||
"regen": "npm run build && node tools/regenerate-test-data.js",
|
||||
"prepare": "npm run build",
|
||||
"pretest": "npm run build",
|
||||
"test": "mocha tests/*.test.js && npm run test:types",
|
||||
"test:types": "tsc --noEmit --project tests/types/tsconfig.json",
|
||||
"test:attw": "attw --pack"
|
||||
},
|
||||
"keywords": [
|
||||
"json",
|
||||
"ast",
|
||||
"json tree",
|
||||
"abstract syntax tree"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@arethetypeswrong/cli": "^0.17.4",
|
||||
"beautify-benchmark": "0.2.4",
|
||||
"benchmark": "2.1.4",
|
||||
"chai": "^4.3.7",
|
||||
"eslint": "8.57.1",
|
||||
"esm": "3.2.25",
|
||||
"json-to-ast": "2.1.0",
|
||||
"json5": "^2.2.3",
|
||||
"mocha": "^11.0.0",
|
||||
"npm-run-all2": "^7.0.0",
|
||||
"rollup": "^4.19.0",
|
||||
"rollup-plugin-copy": "^3.4.0",
|
||||
"rollup-plugin-dts": "^6.1.1",
|
||||
"sinon": "^19.0.0",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
# Retry utility
|
||||
|
||||
by [Nicholas C. Zakas](https://humanwhocodes.com)
|
||||
|
||||
If you find this useful, please consider supporting my work with a [donation](https://humanwhocodes.com/donate) or [nominate me](https://stars.github.com/nominate/) for a GitHub Star.
|
||||
|
||||
## Description
|
||||
|
||||
A utility for retrying failed async JavaScript calls based on the error returned.
|
||||
|
||||
## Usage
|
||||
|
||||
### Node.js
|
||||
|
||||
Install using [npm][npm] or [yarn][yarn]:
|
||||
|
||||
```
|
||||
npm install @humanwhocodes/retry
|
||||
|
||||
# or
|
||||
|
||||
yarn add @humanwhocodes/retry
|
||||
```
|
||||
|
||||
Import into your Node.js project:
|
||||
|
||||
```js
|
||||
// CommonJS
|
||||
const { Retrier } = require("@humanwhocodes/retry");
|
||||
|
||||
// ESM
|
||||
import { Retrier } from "@humanwhocodes/retry";
|
||||
```
|
||||
|
||||
### Deno
|
||||
|
||||
Install using [JSR](https://jsr.io):
|
||||
|
||||
```shell
|
||||
deno add @humanwhocodes/retry
|
||||
|
||||
#or
|
||||
|
||||
jsr add @humanwhocodes/retry
|
||||
```
|
||||
|
||||
Then import into your Deno project:
|
||||
|
||||
```js
|
||||
import { Retrier } from "@humanwhocodes/retry";
|
||||
```
|
||||
|
||||
### Bun
|
||||
|
||||
Install using this command:
|
||||
|
||||
```
|
||||
bun add @humanwhocodes/retry
|
||||
```
|
||||
|
||||
Import into your Bun project:
|
||||
|
||||
```js
|
||||
import { Retrier } from "@humanwhocodes/retry";
|
||||
```
|
||||
|
||||
### Browser
|
||||
|
||||
It's recommended to import the minified version to save bandwidth:
|
||||
|
||||
```js
|
||||
import { Retrier } from "https://cdn.skypack.dev/@humanwhocodes/retry?min";
|
||||
```
|
||||
|
||||
However, you can also import the unminified version for debugging purposes:
|
||||
|
||||
```js
|
||||
import { Retrier } from "https://cdn.skypack.dev/@humanwhocodes/retry";
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
After importing, create a new instance of `Retrier` and specify the function to run on the error. This function should return `true` if you want the call retried and `false` if not.
|
||||
|
||||
```js
|
||||
// this instance will retry if the specific error code is found
|
||||
const retrier = new Retrier(error => {
|
||||
return error.code === "ENFILE" || error.code === "EMFILE";
|
||||
});
|
||||
```
|
||||
|
||||
Then, call the `retry()` method around the function you'd like to retry, such as:
|
||||
|
||||
```js
|
||||
import fs from "fs/promises";
|
||||
|
||||
const retrier = new Retrier(error => {
|
||||
return error.code === "ENFILE" || error.code === "EMFILE";
|
||||
});
|
||||
|
||||
const text = await retrier.retry(() => fs.readFile("README.md", "utf8"));
|
||||
```
|
||||
|
||||
The `retry()` method will either pass through the result on success or wait and retry on failure. Any error that isn't caught by the retrier is automatically rejected so the end result is a transparent passing through of both success and failure.
|
||||
|
||||
### Setting a Timeout
|
||||
|
||||
You can control how long a task will attempt to retry before giving up by passing the `timeout` option to the `Retrier` constructor. By default, the timeout is one minute.
|
||||
|
||||
```js
|
||||
import fs from "fs/promises";
|
||||
|
||||
const retrier = new Retrier(error => {
|
||||
return error.code === "ENFILE" || error.code === "EMFILE";
|
||||
}, { timeout: 100_000 });
|
||||
|
||||
const text = await retrier.retry(() => fs.readFile("README.md", "utf8"));
|
||||
```
|
||||
|
||||
When a call times out, it rejects the first error that was received from calling the function.
|
||||
|
||||
### Setting a Concurrency Limit
|
||||
|
||||
When processing a large number of function calls, you can limit the number of concurrent function calls by passing the `concurrency` option to the `Retrier` constructor. By default, `concurrency` is 1000.
|
||||
|
||||
```js
|
||||
import fs from "fs/promises";
|
||||
|
||||
const retrier = new Retrier(error => {
|
||||
return error.code === "ENFILE" || error.code === "EMFILE";
|
||||
}, { concurrency: 100 });
|
||||
|
||||
const filenames = getFilenames();
|
||||
const contents = await Promise.all(
|
||||
filenames.map(filename => retrier.retry(() => fs.readFile(filename, "utf8"))
|
||||
);
|
||||
```
|
||||
|
||||
### Aborting with `AbortSignal`
|
||||
|
||||
You can also pass an `AbortSignal` to cancel a retry:
|
||||
|
||||
```js
|
||||
import fs from "fs/promises";
|
||||
|
||||
const controller = new AbortController();
|
||||
const retrier = new Retrier(error => {
|
||||
return error.code === "ENFILE" || error.code === "EMFILE";
|
||||
});
|
||||
|
||||
const text = await retrier.retry(
|
||||
() => fs.readFile("README.md", "utf8"),
|
||||
{ signal: controller.signal }
|
||||
);
|
||||
```
|
||||
|
||||
## Developer Setup
|
||||
|
||||
1. Fork the repository
|
||||
2. Clone your fork
|
||||
3. Run `npm install` to setup dependencies
|
||||
4. Run `npm test` to run tests
|
||||
|
||||
### Debug Output
|
||||
|
||||
Enable debugging output by setting the `DEBUG` environment variable to `"@hwc/retry"` before running.
|
||||
|
||||
## License
|
||||
|
||||
Apache 2.0
|
||||
|
||||
## Prior Art
|
||||
|
||||
This utility is inspired by, and contains code from [`graceful-fs`](https://github.com/isaacs/node-graceful-fs).
|
||||
|
||||
[npm]: https://npmjs.com/
|
||||
[yarn]: https://yarnpkg.com/
|
||||
+478
@@ -0,0 +1,478 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* @fileoverview A utility for retrying failed async method calls.
|
||||
*/
|
||||
|
||||
/* global setTimeout, clearTimeout */
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Constants
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
const MAX_TASK_TIMEOUT = 60000;
|
||||
const MAX_TASK_DELAY = 100;
|
||||
const MAX_CONCURRENCY = 1000;
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Logs a message to the console if the DEBUG environment variable is set.
|
||||
* @param {string} message The message to log.
|
||||
* @returns {void}
|
||||
*/
|
||||
function debug(message) {
|
||||
if (globalThis?.process?.env.DEBUG === "@hwc/retry") {
|
||||
console.debug(message);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* The following logic has been extracted from graceful-fs.
|
||||
*
|
||||
* The ISC License
|
||||
*
|
||||
* Copyright (c) 2011-2023 Isaac Z. Schlueter, Ben Noordhuis, and Contributors
|
||||
*
|
||||
* Permission to use, copy, modify, and/or distribute this software for any
|
||||
* purpose with or without fee is hereby granted, provided that the above
|
||||
* copyright notice and this permission notice appear in all copies.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||
* IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Checks if it is time to retry a task based on the timestamp and last attempt time.
|
||||
* @param {RetryTask} task The task to check.
|
||||
* @param {number} maxDelay The maximum delay for the queue.
|
||||
* @returns {boolean} true if it is time to retry, false otherwise.
|
||||
*/
|
||||
function isTimeToRetry(task, maxDelay) {
|
||||
const timeSinceLastAttempt = Date.now() - task.lastAttempt;
|
||||
const timeSinceStart = Math.max(task.lastAttempt - task.timestamp, 1);
|
||||
const desiredDelay = Math.min(timeSinceStart * 1.2, maxDelay);
|
||||
|
||||
return timeSinceLastAttempt >= desiredDelay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if it is time to bail out based on the given timestamp.
|
||||
* @param {RetryTask} task The task to check.
|
||||
* @param {number} timeout The timeout for the queue.
|
||||
* @returns {boolean} true if it is time to bail, false otherwise.
|
||||
*/
|
||||
function isTimeToBail(task, timeout) {
|
||||
return task.age > timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new promise with resolve and reject functions.
|
||||
* @returns {{promise:Promise<any>, resolve:(value:any) => any, reject: (value:any) => any}} A new promise.
|
||||
*/
|
||||
function createPromise() {
|
||||
if (Promise.withResolvers) {
|
||||
return Promise.withResolvers();
|
||||
}
|
||||
|
||||
let resolve, reject;
|
||||
|
||||
const promise = new Promise((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
|
||||
if (resolve === undefined || reject === undefined) {
|
||||
throw new Error("Promise executor did not initialize resolve or reject.");
|
||||
}
|
||||
|
||||
return { promise, resolve, reject };
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A class to represent a task in the retry queue.
|
||||
*/
|
||||
class RetryTask {
|
||||
|
||||
/**
|
||||
* The unique ID for the task.
|
||||
* @type {string}
|
||||
*/
|
||||
id = Math.random().toString(36).slice(2);
|
||||
|
||||
/**
|
||||
* The function to call.
|
||||
* @type {Function}
|
||||
*/
|
||||
fn;
|
||||
|
||||
/**
|
||||
* The error that was thrown.
|
||||
* @type {Error}
|
||||
*/
|
||||
error;
|
||||
|
||||
/**
|
||||
* The timestamp of the task.
|
||||
* @type {number}
|
||||
*/
|
||||
timestamp = Date.now();
|
||||
|
||||
/**
|
||||
* The timestamp of the last attempt.
|
||||
* @type {number}
|
||||
*/
|
||||
lastAttempt = this.timestamp;
|
||||
|
||||
/**
|
||||
* The resolve function for the promise.
|
||||
* @type {Function}
|
||||
*/
|
||||
resolve;
|
||||
|
||||
/**
|
||||
* The reject function for the promise.
|
||||
* @type {Function}
|
||||
*/
|
||||
reject;
|
||||
|
||||
/**
|
||||
* The AbortSignal to monitor for cancellation.
|
||||
* @type {AbortSignal|undefined}
|
||||
*/
|
||||
signal;
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {Function} fn The function to call.
|
||||
* @param {Error} error The error that was thrown.
|
||||
* @param {Function} resolve The resolve function for the promise.
|
||||
* @param {Function} reject The reject function for the promise.
|
||||
* @param {AbortSignal|undefined} signal The AbortSignal to monitor for cancellation.
|
||||
*/
|
||||
constructor(fn, error, resolve, reject, signal) {
|
||||
this.fn = fn;
|
||||
this.error = error;
|
||||
this.timestamp = Date.now();
|
||||
this.lastAttempt = Date.now();
|
||||
this.resolve = resolve;
|
||||
this.reject = reject;
|
||||
this.signal = signal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the age of the task.
|
||||
* @returns {number} The age of the task in milliseconds.
|
||||
* @readonly
|
||||
*/
|
||||
get age() {
|
||||
return Date.now() - this.timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Exports
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* A class that manages a queue of retry jobs.
|
||||
*/
|
||||
class Retrier {
|
||||
|
||||
/**
|
||||
* Represents the queue for processing tasks.
|
||||
* @type {Array<RetryTask>}
|
||||
*/
|
||||
#retrying = [];
|
||||
|
||||
/**
|
||||
* Represents the queue for pending tasks.
|
||||
* @type {Array<Function>}
|
||||
*/
|
||||
#pending = [];
|
||||
|
||||
/**
|
||||
* The number of tasks currently being processed.
|
||||
* @type {number}
|
||||
*/
|
||||
#working = 0;
|
||||
|
||||
/**
|
||||
* The timeout for the queue.
|
||||
* @type {number}
|
||||
*/
|
||||
#timeout;
|
||||
|
||||
/**
|
||||
* The maximum delay for the queue.
|
||||
* @type {number}
|
||||
*/
|
||||
#maxDelay;
|
||||
|
||||
/**
|
||||
* The setTimeout() timer ID.
|
||||
* @type {NodeJS.Timeout|undefined}
|
||||
*/
|
||||
#timerId;
|
||||
|
||||
/**
|
||||
* The function to call.
|
||||
* @type {Function}
|
||||
*/
|
||||
#check;
|
||||
|
||||
/**
|
||||
* The maximum number of concurrent tasks.
|
||||
* @type {number}
|
||||
*/
|
||||
#concurrency;
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {Function} check The function to call.
|
||||
* @param {object} [options] The options for the instance.
|
||||
* @param {number} [options.timeout] The timeout for the queue.
|
||||
* @param {number} [options.maxDelay] The maximum delay for the queue.
|
||||
* @param {number} [options.concurrency] The maximum number of concurrent tasks.
|
||||
*/
|
||||
constructor(check, { timeout = MAX_TASK_TIMEOUT, maxDelay = MAX_TASK_DELAY, concurrency = MAX_CONCURRENCY } = {}) {
|
||||
|
||||
if (typeof check !== "function") {
|
||||
throw new Error("Missing function to check errors");
|
||||
}
|
||||
|
||||
this.#check = check;
|
||||
this.#timeout = timeout;
|
||||
this.#maxDelay = maxDelay;
|
||||
this.#concurrency = concurrency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of tasks waiting to be retried.
|
||||
* @returns {number} The number of tasks in the retry queue.
|
||||
*/
|
||||
get retrying() {
|
||||
return this.#retrying.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of tasks waiting to be processed in the pending queue.
|
||||
* @returns {number} The number of tasks in the pending queue.
|
||||
*/
|
||||
get pending() {
|
||||
return this.#pending.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of tasks currently being processed.
|
||||
* @returns {number} The number of tasks currently being processed.
|
||||
*/
|
||||
get working() {
|
||||
return this.#working;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the function and retries if it fails.
|
||||
* @param {Function} fn The function to call.
|
||||
* @param {Object} options The options for the job.
|
||||
* @param {AbortSignal} [options.signal] The AbortSignal to monitor for cancellation.
|
||||
* @param {Promise<any>} options.promise The promise to return when the function settles.
|
||||
* @param {Function} options.resolve The resolve function for the promise.
|
||||
* @param {Function} options.reject The reject function for the promise.
|
||||
* @returns {Promise<any>} A promise that resolves when the function is
|
||||
* called successfully.
|
||||
*/
|
||||
#call(fn, { signal, promise, resolve, reject }) {
|
||||
|
||||
let result;
|
||||
|
||||
try {
|
||||
result = fn();
|
||||
} catch (/** @type {any} */ error) {
|
||||
reject(new Error(`Synchronous error: ${error.message}`, { cause: error }));
|
||||
return promise;
|
||||
}
|
||||
|
||||
// if the result is not a promise then reject an error
|
||||
if (!result || typeof result.then !== "function") {
|
||||
reject(new Error("Result is not a promise."));
|
||||
return promise;
|
||||
}
|
||||
|
||||
this.#working++;
|
||||
promise.finally(() => {
|
||||
this.#working--;
|
||||
this.#processPending();
|
||||
})
|
||||
// `promise.finally` creates a new promise that may be rejected, so it must be handled.
|
||||
.catch(() => { });
|
||||
|
||||
// call the original function and catch any ENFILE or EMFILE errors
|
||||
Promise.resolve(result)
|
||||
.then(value => {
|
||||
debug("Function called successfully without retry.");
|
||||
resolve(value);
|
||||
})
|
||||
.catch(error => {
|
||||
if (!this.#check(error)) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
const task = new RetryTask(fn, error, resolve, reject, signal);
|
||||
|
||||
debug(`Function failed, queuing for retry with task ${task.id}.`);
|
||||
this.#retrying.push(task);
|
||||
|
||||
signal?.addEventListener("abort", () => {
|
||||
debug(`Task ${task.id} was aborted due to AbortSignal.`);
|
||||
reject(signal.reason);
|
||||
});
|
||||
|
||||
this.#processQueue();
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new retry job to the queue.
|
||||
* @template {(...args: unknown[]) => Promise<unknown>} Func
|
||||
* @template {Awaited<ReturnType<Func>>} RetVal
|
||||
* @param {Func} fn The function to call.
|
||||
* @param {object} [options] The options for the job.
|
||||
* @param {AbortSignal} [options.signal] The AbortSignal to monitor for cancellation.
|
||||
* @returns {Promise<RetVal>} A promise that resolves when the queue is processed.
|
||||
*/
|
||||
retry(fn, { signal } = {}) {
|
||||
|
||||
signal?.throwIfAborted();
|
||||
|
||||
const { promise, resolve, reject } = createPromise();
|
||||
|
||||
this.#pending.push(() => this.#call(fn, { signal, promise, resolve, reject }));
|
||||
this.#processPending();
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Processes the pending queue and the retry queue.
|
||||
* @returns {void}
|
||||
*/
|
||||
#processAll() {
|
||||
if (this.pending) {
|
||||
this.#processPending();
|
||||
}
|
||||
|
||||
if (this.retrying) {
|
||||
this.#processQueue();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the pending queue to see which tasks can be started.
|
||||
* @returns {void}
|
||||
*/
|
||||
#processPending() {
|
||||
|
||||
debug(`Processing pending tasks: ${this.pending} pending, ${this.working} working.`);
|
||||
|
||||
const available = this.#concurrency - this.working;
|
||||
|
||||
if (available <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const count = Math.min(this.pending, available);
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const task = this.#pending.shift();
|
||||
task?.();
|
||||
}
|
||||
|
||||
debug(`Processed pending tasks: ${this.pending} pending, ${this.working} working.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the queue.
|
||||
* @returns {void}
|
||||
*/
|
||||
#processQueue() {
|
||||
// clear any timer because we're going to check right now
|
||||
clearTimeout(this.#timerId);
|
||||
this.#timerId = undefined;
|
||||
|
||||
debug(`Processing retry queue: ${this.retrying} retrying, ${this.working} working.`);
|
||||
|
||||
const processAgain = () => {
|
||||
this.#timerId = setTimeout(() => this.#processAll(), 0);
|
||||
};
|
||||
|
||||
// if there's nothing in the queue, we're done
|
||||
const task = this.#retrying.shift();
|
||||
if (!task) {
|
||||
debug("Queue is empty, exiting.");
|
||||
|
||||
if (this.pending) {
|
||||
processAgain();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// if it's time to bail, then bail
|
||||
if (isTimeToBail(task, this.#timeout)) {
|
||||
debug(`Task ${task.id} was abandoned due to timeout.`);
|
||||
task.reject(task.error);
|
||||
processAgain();
|
||||
return;
|
||||
}
|
||||
|
||||
// if it's not time to retry, then wait and try again
|
||||
if (!isTimeToRetry(task, this.#maxDelay)) {
|
||||
debug(`Task ${task.id} is not ready to retry, skipping.`);
|
||||
this.#retrying.push(task);
|
||||
processAgain();
|
||||
return;
|
||||
}
|
||||
|
||||
// otherwise, try again
|
||||
task.lastAttempt = Date.now();
|
||||
|
||||
// Promise.resolve needed in case it's a thenable but not a Promise
|
||||
Promise.resolve(task.fn())
|
||||
// @ts-ignore because we know it's any
|
||||
.then(result => {
|
||||
debug(`Task ${task.id} succeeded after ${task.age}ms.`);
|
||||
task.resolve(result);
|
||||
})
|
||||
|
||||
// @ts-ignore because we know it's any
|
||||
.catch(error => {
|
||||
if (!this.#check(error)) {
|
||||
debug(`Task ${task.id} failed with non-retryable error: ${error.message}.`);
|
||||
task.reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
// update the task timestamp and push to back of queue to try again
|
||||
task.lastAttempt = Date.now();
|
||||
this.#retrying.push(task);
|
||||
debug(`Task ${task.id} failed, requeueing to try again.`);
|
||||
})
|
||||
.finally(() => {
|
||||
this.#processAll();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
exports.Retrier = Retrier;
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* A class that manages a queue of retry jobs.
|
||||
*/
|
||||
export class Retrier {
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {Function} check The function to call.
|
||||
* @param {object} [options] The options for the instance.
|
||||
* @param {number} [options.timeout] The timeout for the queue.
|
||||
* @param {number} [options.maxDelay] The maximum delay for the queue.
|
||||
* @param {number} [options.concurrency] The maximum number of concurrent tasks.
|
||||
*/
|
||||
constructor(check: Function, { timeout, maxDelay, concurrency }?: {
|
||||
timeout?: number | undefined;
|
||||
maxDelay?: number | undefined;
|
||||
concurrency?: number | undefined;
|
||||
} | undefined);
|
||||
/**
|
||||
* Gets the number of tasks waiting to be retried.
|
||||
* @returns {number} The number of tasks in the retry queue.
|
||||
*/
|
||||
get retrying(): number;
|
||||
/**
|
||||
* Gets the number of tasks waiting to be processed in the pending queue.
|
||||
* @returns {number} The number of tasks in the pending queue.
|
||||
*/
|
||||
get pending(): number;
|
||||
/**
|
||||
* Gets the number of tasks currently being processed.
|
||||
* @returns {number} The number of tasks currently being processed.
|
||||
*/
|
||||
get working(): number;
|
||||
/**
|
||||
* Adds a new retry job to the queue.
|
||||
* @template {(...args: unknown[]) => Promise<unknown>} Func
|
||||
* @template {Awaited<ReturnType<Func>>} RetVal
|
||||
* @param {Func} fn The function to call.
|
||||
* @param {object} [options] The options for the job.
|
||||
* @param {AbortSignal} [options.signal] The AbortSignal to monitor for cancellation.
|
||||
* @returns {Promise<RetVal>} A promise that resolves when the queue is processed.
|
||||
*/
|
||||
retry<Func extends (...args: unknown[]) => Promise<unknown>, RetVal extends Awaited<ReturnType<Func>>>(fn: Func, { signal }?: {
|
||||
signal?: AbortSignal | undefined;
|
||||
} | undefined): Promise<RetVal>;
|
||||
#private;
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* A class that manages a queue of retry jobs.
|
||||
*/
|
||||
export class Retrier {
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {Function} check The function to call.
|
||||
* @param {object} [options] The options for the instance.
|
||||
* @param {number} [options.timeout] The timeout for the queue.
|
||||
* @param {number} [options.maxDelay] The maximum delay for the queue.
|
||||
* @param {number} [options.concurrency] The maximum number of concurrent tasks.
|
||||
*/
|
||||
constructor(check: Function, { timeout, maxDelay, concurrency }?: {
|
||||
timeout?: number | undefined;
|
||||
maxDelay?: number | undefined;
|
||||
concurrency?: number | undefined;
|
||||
} | undefined);
|
||||
/**
|
||||
* Gets the number of tasks waiting to be retried.
|
||||
* @returns {number} The number of tasks in the retry queue.
|
||||
*/
|
||||
get retrying(): number;
|
||||
/**
|
||||
* Gets the number of tasks waiting to be processed in the pending queue.
|
||||
* @returns {number} The number of tasks in the pending queue.
|
||||
*/
|
||||
get pending(): number;
|
||||
/**
|
||||
* Gets the number of tasks currently being processed.
|
||||
* @returns {number} The number of tasks currently being processed.
|
||||
*/
|
||||
get working(): number;
|
||||
/**
|
||||
* Adds a new retry job to the queue.
|
||||
* @template {(...args: unknown[]) => Promise<unknown>} Func
|
||||
* @template {Awaited<ReturnType<Func>>} RetVal
|
||||
* @param {Func} fn The function to call.
|
||||
* @param {object} [options] The options for the job.
|
||||
* @param {AbortSignal} [options.signal] The AbortSignal to monitor for cancellation.
|
||||
* @returns {Promise<RetVal>} A promise that resolves when the queue is processed.
|
||||
*/
|
||||
retry<Func extends (...args: unknown[]) => Promise<unknown>, RetVal extends Awaited<ReturnType<Func>>>(fn: Func, { signal }?: {
|
||||
signal?: AbortSignal | undefined;
|
||||
} | undefined): Promise<RetVal>;
|
||||
#private;
|
||||
}
|
||||
+477
@@ -0,0 +1,477 @@
|
||||
// @ts-self-types="./retrier.d.ts"
|
||||
/**
|
||||
* @fileoverview A utility for retrying failed async method calls.
|
||||
*/
|
||||
|
||||
/* global setTimeout, clearTimeout */
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Constants
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
const MAX_TASK_TIMEOUT = 60000;
|
||||
const MAX_TASK_DELAY = 100;
|
||||
const MAX_CONCURRENCY = 1000;
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Logs a message to the console if the DEBUG environment variable is set.
|
||||
* @param {string} message The message to log.
|
||||
* @returns {void}
|
||||
*/
|
||||
function debug(message) {
|
||||
if (globalThis?.process?.env.DEBUG === "@hwc/retry") {
|
||||
console.debug(message);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* The following logic has been extracted from graceful-fs.
|
||||
*
|
||||
* The ISC License
|
||||
*
|
||||
* Copyright (c) 2011-2023 Isaac Z. Schlueter, Ben Noordhuis, and Contributors
|
||||
*
|
||||
* Permission to use, copy, modify, and/or distribute this software for any
|
||||
* purpose with or without fee is hereby granted, provided that the above
|
||||
* copyright notice and this permission notice appear in all copies.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||
* IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Checks if it is time to retry a task based on the timestamp and last attempt time.
|
||||
* @param {RetryTask} task The task to check.
|
||||
* @param {number} maxDelay The maximum delay for the queue.
|
||||
* @returns {boolean} true if it is time to retry, false otherwise.
|
||||
*/
|
||||
function isTimeToRetry(task, maxDelay) {
|
||||
const timeSinceLastAttempt = Date.now() - task.lastAttempt;
|
||||
const timeSinceStart = Math.max(task.lastAttempt - task.timestamp, 1);
|
||||
const desiredDelay = Math.min(timeSinceStart * 1.2, maxDelay);
|
||||
|
||||
return timeSinceLastAttempt >= desiredDelay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if it is time to bail out based on the given timestamp.
|
||||
* @param {RetryTask} task The task to check.
|
||||
* @param {number} timeout The timeout for the queue.
|
||||
* @returns {boolean} true if it is time to bail, false otherwise.
|
||||
*/
|
||||
function isTimeToBail(task, timeout) {
|
||||
return task.age > timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new promise with resolve and reject functions.
|
||||
* @returns {{promise:Promise<any>, resolve:(value:any) => any, reject: (value:any) => any}} A new promise.
|
||||
*/
|
||||
function createPromise() {
|
||||
if (Promise.withResolvers) {
|
||||
return Promise.withResolvers();
|
||||
}
|
||||
|
||||
let resolve, reject;
|
||||
|
||||
const promise = new Promise((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
|
||||
if (resolve === undefined || reject === undefined) {
|
||||
throw new Error("Promise executor did not initialize resolve or reject.");
|
||||
}
|
||||
|
||||
return { promise, resolve, reject };
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A class to represent a task in the retry queue.
|
||||
*/
|
||||
class RetryTask {
|
||||
|
||||
/**
|
||||
* The unique ID for the task.
|
||||
* @type {string}
|
||||
*/
|
||||
id = Math.random().toString(36).slice(2);
|
||||
|
||||
/**
|
||||
* The function to call.
|
||||
* @type {Function}
|
||||
*/
|
||||
fn;
|
||||
|
||||
/**
|
||||
* The error that was thrown.
|
||||
* @type {Error}
|
||||
*/
|
||||
error;
|
||||
|
||||
/**
|
||||
* The timestamp of the task.
|
||||
* @type {number}
|
||||
*/
|
||||
timestamp = Date.now();
|
||||
|
||||
/**
|
||||
* The timestamp of the last attempt.
|
||||
* @type {number}
|
||||
*/
|
||||
lastAttempt = this.timestamp;
|
||||
|
||||
/**
|
||||
* The resolve function for the promise.
|
||||
* @type {Function}
|
||||
*/
|
||||
resolve;
|
||||
|
||||
/**
|
||||
* The reject function for the promise.
|
||||
* @type {Function}
|
||||
*/
|
||||
reject;
|
||||
|
||||
/**
|
||||
* The AbortSignal to monitor for cancellation.
|
||||
* @type {AbortSignal|undefined}
|
||||
*/
|
||||
signal;
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {Function} fn The function to call.
|
||||
* @param {Error} error The error that was thrown.
|
||||
* @param {Function} resolve The resolve function for the promise.
|
||||
* @param {Function} reject The reject function for the promise.
|
||||
* @param {AbortSignal|undefined} signal The AbortSignal to monitor for cancellation.
|
||||
*/
|
||||
constructor(fn, error, resolve, reject, signal) {
|
||||
this.fn = fn;
|
||||
this.error = error;
|
||||
this.timestamp = Date.now();
|
||||
this.lastAttempt = Date.now();
|
||||
this.resolve = resolve;
|
||||
this.reject = reject;
|
||||
this.signal = signal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the age of the task.
|
||||
* @returns {number} The age of the task in milliseconds.
|
||||
* @readonly
|
||||
*/
|
||||
get age() {
|
||||
return Date.now() - this.timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Exports
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* A class that manages a queue of retry jobs.
|
||||
*/
|
||||
class Retrier {
|
||||
|
||||
/**
|
||||
* Represents the queue for processing tasks.
|
||||
* @type {Array<RetryTask>}
|
||||
*/
|
||||
#retrying = [];
|
||||
|
||||
/**
|
||||
* Represents the queue for pending tasks.
|
||||
* @type {Array<Function>}
|
||||
*/
|
||||
#pending = [];
|
||||
|
||||
/**
|
||||
* The number of tasks currently being processed.
|
||||
* @type {number}
|
||||
*/
|
||||
#working = 0;
|
||||
|
||||
/**
|
||||
* The timeout for the queue.
|
||||
* @type {number}
|
||||
*/
|
||||
#timeout;
|
||||
|
||||
/**
|
||||
* The maximum delay for the queue.
|
||||
* @type {number}
|
||||
*/
|
||||
#maxDelay;
|
||||
|
||||
/**
|
||||
* The setTimeout() timer ID.
|
||||
* @type {NodeJS.Timeout|undefined}
|
||||
*/
|
||||
#timerId;
|
||||
|
||||
/**
|
||||
* The function to call.
|
||||
* @type {Function}
|
||||
*/
|
||||
#check;
|
||||
|
||||
/**
|
||||
* The maximum number of concurrent tasks.
|
||||
* @type {number}
|
||||
*/
|
||||
#concurrency;
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {Function} check The function to call.
|
||||
* @param {object} [options] The options for the instance.
|
||||
* @param {number} [options.timeout] The timeout for the queue.
|
||||
* @param {number} [options.maxDelay] The maximum delay for the queue.
|
||||
* @param {number} [options.concurrency] The maximum number of concurrent tasks.
|
||||
*/
|
||||
constructor(check, { timeout = MAX_TASK_TIMEOUT, maxDelay = MAX_TASK_DELAY, concurrency = MAX_CONCURRENCY } = {}) {
|
||||
|
||||
if (typeof check !== "function") {
|
||||
throw new Error("Missing function to check errors");
|
||||
}
|
||||
|
||||
this.#check = check;
|
||||
this.#timeout = timeout;
|
||||
this.#maxDelay = maxDelay;
|
||||
this.#concurrency = concurrency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of tasks waiting to be retried.
|
||||
* @returns {number} The number of tasks in the retry queue.
|
||||
*/
|
||||
get retrying() {
|
||||
return this.#retrying.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of tasks waiting to be processed in the pending queue.
|
||||
* @returns {number} The number of tasks in the pending queue.
|
||||
*/
|
||||
get pending() {
|
||||
return this.#pending.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of tasks currently being processed.
|
||||
* @returns {number} The number of tasks currently being processed.
|
||||
*/
|
||||
get working() {
|
||||
return this.#working;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the function and retries if it fails.
|
||||
* @param {Function} fn The function to call.
|
||||
* @param {Object} options The options for the job.
|
||||
* @param {AbortSignal} [options.signal] The AbortSignal to monitor for cancellation.
|
||||
* @param {Promise<any>} options.promise The promise to return when the function settles.
|
||||
* @param {Function} options.resolve The resolve function for the promise.
|
||||
* @param {Function} options.reject The reject function for the promise.
|
||||
* @returns {Promise<any>} A promise that resolves when the function is
|
||||
* called successfully.
|
||||
*/
|
||||
#call(fn, { signal, promise, resolve, reject }) {
|
||||
|
||||
let result;
|
||||
|
||||
try {
|
||||
result = fn();
|
||||
} catch (/** @type {any} */ error) {
|
||||
reject(new Error(`Synchronous error: ${error.message}`, { cause: error }));
|
||||
return promise;
|
||||
}
|
||||
|
||||
// if the result is not a promise then reject an error
|
||||
if (!result || typeof result.then !== "function") {
|
||||
reject(new Error("Result is not a promise."));
|
||||
return promise;
|
||||
}
|
||||
|
||||
this.#working++;
|
||||
promise.finally(() => {
|
||||
this.#working--;
|
||||
this.#processPending();
|
||||
})
|
||||
// `promise.finally` creates a new promise that may be rejected, so it must be handled.
|
||||
.catch(() => { });
|
||||
|
||||
// call the original function and catch any ENFILE or EMFILE errors
|
||||
Promise.resolve(result)
|
||||
.then(value => {
|
||||
debug("Function called successfully without retry.");
|
||||
resolve(value);
|
||||
})
|
||||
.catch(error => {
|
||||
if (!this.#check(error)) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
const task = new RetryTask(fn, error, resolve, reject, signal);
|
||||
|
||||
debug(`Function failed, queuing for retry with task ${task.id}.`);
|
||||
this.#retrying.push(task);
|
||||
|
||||
signal?.addEventListener("abort", () => {
|
||||
debug(`Task ${task.id} was aborted due to AbortSignal.`);
|
||||
reject(signal.reason);
|
||||
});
|
||||
|
||||
this.#processQueue();
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new retry job to the queue.
|
||||
* @template {(...args: unknown[]) => Promise<unknown>} Func
|
||||
* @template {Awaited<ReturnType<Func>>} RetVal
|
||||
* @param {Func} fn The function to call.
|
||||
* @param {object} [options] The options for the job.
|
||||
* @param {AbortSignal} [options.signal] The AbortSignal to monitor for cancellation.
|
||||
* @returns {Promise<RetVal>} A promise that resolves when the queue is processed.
|
||||
*/
|
||||
retry(fn, { signal } = {}) {
|
||||
|
||||
signal?.throwIfAborted();
|
||||
|
||||
const { promise, resolve, reject } = createPromise();
|
||||
|
||||
this.#pending.push(() => this.#call(fn, { signal, promise, resolve, reject }));
|
||||
this.#processPending();
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Processes the pending queue and the retry queue.
|
||||
* @returns {void}
|
||||
*/
|
||||
#processAll() {
|
||||
if (this.pending) {
|
||||
this.#processPending();
|
||||
}
|
||||
|
||||
if (this.retrying) {
|
||||
this.#processQueue();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the pending queue to see which tasks can be started.
|
||||
* @returns {void}
|
||||
*/
|
||||
#processPending() {
|
||||
|
||||
debug(`Processing pending tasks: ${this.pending} pending, ${this.working} working.`);
|
||||
|
||||
const available = this.#concurrency - this.working;
|
||||
|
||||
if (available <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const count = Math.min(this.pending, available);
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const task = this.#pending.shift();
|
||||
task?.();
|
||||
}
|
||||
|
||||
debug(`Processed pending tasks: ${this.pending} pending, ${this.working} working.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the queue.
|
||||
* @returns {void}
|
||||
*/
|
||||
#processQueue() {
|
||||
// clear any timer because we're going to check right now
|
||||
clearTimeout(this.#timerId);
|
||||
this.#timerId = undefined;
|
||||
|
||||
debug(`Processing retry queue: ${this.retrying} retrying, ${this.working} working.`);
|
||||
|
||||
const processAgain = () => {
|
||||
this.#timerId = setTimeout(() => this.#processAll(), 0);
|
||||
};
|
||||
|
||||
// if there's nothing in the queue, we're done
|
||||
const task = this.#retrying.shift();
|
||||
if (!task) {
|
||||
debug("Queue is empty, exiting.");
|
||||
|
||||
if (this.pending) {
|
||||
processAgain();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// if it's time to bail, then bail
|
||||
if (isTimeToBail(task, this.#timeout)) {
|
||||
debug(`Task ${task.id} was abandoned due to timeout.`);
|
||||
task.reject(task.error);
|
||||
processAgain();
|
||||
return;
|
||||
}
|
||||
|
||||
// if it's not time to retry, then wait and try again
|
||||
if (!isTimeToRetry(task, this.#maxDelay)) {
|
||||
debug(`Task ${task.id} is not ready to retry, skipping.`);
|
||||
this.#retrying.push(task);
|
||||
processAgain();
|
||||
return;
|
||||
}
|
||||
|
||||
// otherwise, try again
|
||||
task.lastAttempt = Date.now();
|
||||
|
||||
// Promise.resolve needed in case it's a thenable but not a Promise
|
||||
Promise.resolve(task.fn())
|
||||
// @ts-ignore because we know it's any
|
||||
.then(result => {
|
||||
debug(`Task ${task.id} succeeded after ${task.age}ms.`);
|
||||
task.resolve(result);
|
||||
})
|
||||
|
||||
// @ts-ignore because we know it's any
|
||||
.catch(error => {
|
||||
if (!this.#check(error)) {
|
||||
debug(`Task ${task.id} failed with non-retryable error: ${error.message}.`);
|
||||
task.reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
// update the task timestamp and push to back of queue to try again
|
||||
task.lastAttempt = Date.now();
|
||||
this.#retrying.push(task);
|
||||
debug(`Task ${task.id} failed, requeueing to try again.`);
|
||||
})
|
||||
.finally(() => {
|
||||
this.#processAll();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { Retrier };
|
||||
+1
@@ -0,0 +1 @@
|
||||
function e(e){"@hwc/retry"===globalThis?.process?.env.DEBUG&&console.debug(e)}class RetryTask{id=Math.random().toString(36).slice(2);fn;error;timestamp=Date.now();lastAttempt=this.timestamp;resolve;reject;signal;constructor(e,t,r,i,s){this.fn=e,this.error=t,this.timestamp=Date.now(),this.lastAttempt=Date.now(),this.resolve=r,this.reject=i,this.signal=s}get age(){return Date.now()-this.timestamp}}class Retrier{#e=[];#t=[];#r=0;#i;#s;#n;#o;#c;constructor(e,{timeout:t=6e4,maxDelay:r=100,concurrency:i=1e3}={}){if("function"!=typeof e)throw new Error("Missing function to check errors");this.#o=e,this.#i=t,this.#s=r,this.#c=i}get retrying(){return this.#e.length}get pending(){return this.#t.length}get working(){return this.#r}#a(t,{signal:r,promise:i,resolve:s,reject:n}){let o;try{o=t()}catch(e){return n(new Error(`Synchronous error: ${e.message}`,{cause:e})),i}return o&&"function"==typeof o.then?(this.#r++,i.finally((()=>{this.#r--,this.#h()})).catch((()=>{})),Promise.resolve(o).then((t=>{e("Function called successfully without retry."),s(t)})).catch((i=>{if(!this.#o(i))return void n(i);const o=new RetryTask(t,i,s,n,r);e(`Function failed, queuing for retry with task ${o.id}.`),this.#e.push(o),r?.addEventListener("abort",(()=>{e(`Task ${o.id} was aborted due to AbortSignal.`),n(r.reason)})),this.#g()})),i):(n(new Error("Result is not a promise.")),i)}retry(e,{signal:t}={}){t?.throwIfAborted();const{promise:r,resolve:i,reject:s}=function(){if(Promise.withResolvers)return Promise.withResolvers();let e,t;const r=new Promise(((r,i)=>{e=r,t=i}));if(void 0===e||void 0===t)throw new Error("Promise executor did not initialize resolve or reject.");return{promise:r,resolve:e,reject:t}}();return this.#t.push((()=>this.#a(e,{signal:t,promise:r,resolve:i,reject:s}))),this.#h(),r}#u(){this.pending&&this.#h(),this.retrying&&this.#g()}#h(){e(`Processing pending tasks: ${this.pending} pending, ${this.working} working.`);const t=this.#c-this.working;if(t<=0)return;const r=Math.min(this.pending,t);for(let e=0;e<r;e++){const e=this.#t.shift();e?.()}e(`Processed pending tasks: ${this.pending} pending, ${this.working} working.`)}#g(){clearTimeout(this.#n),this.#n=void 0,e(`Processing retry queue: ${this.retrying} retrying, ${this.working} working.`);const t=()=>{this.#n=setTimeout((()=>this.#u()),0)},r=this.#e.shift();return r?function(e,t){return e.age>t}(r,this.#i)?(e(`Task ${r.id} was abandoned due to timeout.`),r.reject(r.error),void t()):function(e,t){const r=Date.now()-e.lastAttempt,i=Math.max(e.lastAttempt-e.timestamp,1);return r>=Math.min(1.2*i,t)}(r,this.#s)?(r.lastAttempt=Date.now(),void Promise.resolve(r.fn()).then((t=>{e(`Task ${r.id} succeeded after ${r.age}ms.`),r.resolve(t)})).catch((t=>{if(!this.#o(t))return e(`Task ${r.id} failed with non-retryable error: ${t.message}.`),void r.reject(t);r.lastAttempt=Date.now(),this.#e.push(r),e(`Task ${r.id} failed, requeueing to try again.`)})).finally((()=>{this.#u()}))):(e(`Task ${r.id} is not ready to retry, skipping.`),this.#e.push(r),void t()):(e("Queue is empty, exiting."),void(this.pending&&t()))}}export{Retrier};
|
||||
+476
@@ -0,0 +1,476 @@
|
||||
/**
|
||||
* @fileoverview A utility for retrying failed async method calls.
|
||||
*/
|
||||
|
||||
/* global setTimeout, clearTimeout */
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Constants
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
const MAX_TASK_TIMEOUT = 60000;
|
||||
const MAX_TASK_DELAY = 100;
|
||||
const MAX_CONCURRENCY = 1000;
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Logs a message to the console if the DEBUG environment variable is set.
|
||||
* @param {string} message The message to log.
|
||||
* @returns {void}
|
||||
*/
|
||||
function debug(message) {
|
||||
if (globalThis?.process?.env.DEBUG === "@hwc/retry") {
|
||||
console.debug(message);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* The following logic has been extracted from graceful-fs.
|
||||
*
|
||||
* The ISC License
|
||||
*
|
||||
* Copyright (c) 2011-2023 Isaac Z. Schlueter, Ben Noordhuis, and Contributors
|
||||
*
|
||||
* Permission to use, copy, modify, and/or distribute this software for any
|
||||
* purpose with or without fee is hereby granted, provided that the above
|
||||
* copyright notice and this permission notice appear in all copies.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
|
||||
* IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Checks if it is time to retry a task based on the timestamp and last attempt time.
|
||||
* @param {RetryTask} task The task to check.
|
||||
* @param {number} maxDelay The maximum delay for the queue.
|
||||
* @returns {boolean} true if it is time to retry, false otherwise.
|
||||
*/
|
||||
function isTimeToRetry(task, maxDelay) {
|
||||
const timeSinceLastAttempt = Date.now() - task.lastAttempt;
|
||||
const timeSinceStart = Math.max(task.lastAttempt - task.timestamp, 1);
|
||||
const desiredDelay = Math.min(timeSinceStart * 1.2, maxDelay);
|
||||
|
||||
return timeSinceLastAttempt >= desiredDelay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if it is time to bail out based on the given timestamp.
|
||||
* @param {RetryTask} task The task to check.
|
||||
* @param {number} timeout The timeout for the queue.
|
||||
* @returns {boolean} true if it is time to bail, false otherwise.
|
||||
*/
|
||||
function isTimeToBail(task, timeout) {
|
||||
return task.age > timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new promise with resolve and reject functions.
|
||||
* @returns {{promise:Promise<any>, resolve:(value:any) => any, reject: (value:any) => any}} A new promise.
|
||||
*/
|
||||
function createPromise() {
|
||||
if (Promise.withResolvers) {
|
||||
return Promise.withResolvers();
|
||||
}
|
||||
|
||||
let resolve, reject;
|
||||
|
||||
const promise = new Promise((res, rej) => {
|
||||
resolve = res;
|
||||
reject = rej;
|
||||
});
|
||||
|
||||
if (resolve === undefined || reject === undefined) {
|
||||
throw new Error("Promise executor did not initialize resolve or reject.");
|
||||
}
|
||||
|
||||
return { promise, resolve, reject };
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A class to represent a task in the retry queue.
|
||||
*/
|
||||
class RetryTask {
|
||||
|
||||
/**
|
||||
* The unique ID for the task.
|
||||
* @type {string}
|
||||
*/
|
||||
id = Math.random().toString(36).slice(2);
|
||||
|
||||
/**
|
||||
* The function to call.
|
||||
* @type {Function}
|
||||
*/
|
||||
fn;
|
||||
|
||||
/**
|
||||
* The error that was thrown.
|
||||
* @type {Error}
|
||||
*/
|
||||
error;
|
||||
|
||||
/**
|
||||
* The timestamp of the task.
|
||||
* @type {number}
|
||||
*/
|
||||
timestamp = Date.now();
|
||||
|
||||
/**
|
||||
* The timestamp of the last attempt.
|
||||
* @type {number}
|
||||
*/
|
||||
lastAttempt = this.timestamp;
|
||||
|
||||
/**
|
||||
* The resolve function for the promise.
|
||||
* @type {Function}
|
||||
*/
|
||||
resolve;
|
||||
|
||||
/**
|
||||
* The reject function for the promise.
|
||||
* @type {Function}
|
||||
*/
|
||||
reject;
|
||||
|
||||
/**
|
||||
* The AbortSignal to monitor for cancellation.
|
||||
* @type {AbortSignal|undefined}
|
||||
*/
|
||||
signal;
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {Function} fn The function to call.
|
||||
* @param {Error} error The error that was thrown.
|
||||
* @param {Function} resolve The resolve function for the promise.
|
||||
* @param {Function} reject The reject function for the promise.
|
||||
* @param {AbortSignal|undefined} signal The AbortSignal to monitor for cancellation.
|
||||
*/
|
||||
constructor(fn, error, resolve, reject, signal) {
|
||||
this.fn = fn;
|
||||
this.error = error;
|
||||
this.timestamp = Date.now();
|
||||
this.lastAttempt = Date.now();
|
||||
this.resolve = resolve;
|
||||
this.reject = reject;
|
||||
this.signal = signal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the age of the task.
|
||||
* @returns {number} The age of the task in milliseconds.
|
||||
* @readonly
|
||||
*/
|
||||
get age() {
|
||||
return Date.now() - this.timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Exports
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* A class that manages a queue of retry jobs.
|
||||
*/
|
||||
class Retrier {
|
||||
|
||||
/**
|
||||
* Represents the queue for processing tasks.
|
||||
* @type {Array<RetryTask>}
|
||||
*/
|
||||
#retrying = [];
|
||||
|
||||
/**
|
||||
* Represents the queue for pending tasks.
|
||||
* @type {Array<Function>}
|
||||
*/
|
||||
#pending = [];
|
||||
|
||||
/**
|
||||
* The number of tasks currently being processed.
|
||||
* @type {number}
|
||||
*/
|
||||
#working = 0;
|
||||
|
||||
/**
|
||||
* The timeout for the queue.
|
||||
* @type {number}
|
||||
*/
|
||||
#timeout;
|
||||
|
||||
/**
|
||||
* The maximum delay for the queue.
|
||||
* @type {number}
|
||||
*/
|
||||
#maxDelay;
|
||||
|
||||
/**
|
||||
* The setTimeout() timer ID.
|
||||
* @type {NodeJS.Timeout|undefined}
|
||||
*/
|
||||
#timerId;
|
||||
|
||||
/**
|
||||
* The function to call.
|
||||
* @type {Function}
|
||||
*/
|
||||
#check;
|
||||
|
||||
/**
|
||||
* The maximum number of concurrent tasks.
|
||||
* @type {number}
|
||||
*/
|
||||
#concurrency;
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
* @param {Function} check The function to call.
|
||||
* @param {object} [options] The options for the instance.
|
||||
* @param {number} [options.timeout] The timeout for the queue.
|
||||
* @param {number} [options.maxDelay] The maximum delay for the queue.
|
||||
* @param {number} [options.concurrency] The maximum number of concurrent tasks.
|
||||
*/
|
||||
constructor(check, { timeout = MAX_TASK_TIMEOUT, maxDelay = MAX_TASK_DELAY, concurrency = MAX_CONCURRENCY } = {}) {
|
||||
|
||||
if (typeof check !== "function") {
|
||||
throw new Error("Missing function to check errors");
|
||||
}
|
||||
|
||||
this.#check = check;
|
||||
this.#timeout = timeout;
|
||||
this.#maxDelay = maxDelay;
|
||||
this.#concurrency = concurrency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of tasks waiting to be retried.
|
||||
* @returns {number} The number of tasks in the retry queue.
|
||||
*/
|
||||
get retrying() {
|
||||
return this.#retrying.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of tasks waiting to be processed in the pending queue.
|
||||
* @returns {number} The number of tasks in the pending queue.
|
||||
*/
|
||||
get pending() {
|
||||
return this.#pending.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of tasks currently being processed.
|
||||
* @returns {number} The number of tasks currently being processed.
|
||||
*/
|
||||
get working() {
|
||||
return this.#working;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the function and retries if it fails.
|
||||
* @param {Function} fn The function to call.
|
||||
* @param {Object} options The options for the job.
|
||||
* @param {AbortSignal} [options.signal] The AbortSignal to monitor for cancellation.
|
||||
* @param {Promise<any>} options.promise The promise to return when the function settles.
|
||||
* @param {Function} options.resolve The resolve function for the promise.
|
||||
* @param {Function} options.reject The reject function for the promise.
|
||||
* @returns {Promise<any>} A promise that resolves when the function is
|
||||
* called successfully.
|
||||
*/
|
||||
#call(fn, { signal, promise, resolve, reject }) {
|
||||
|
||||
let result;
|
||||
|
||||
try {
|
||||
result = fn();
|
||||
} catch (/** @type {any} */ error) {
|
||||
reject(new Error(`Synchronous error: ${error.message}`, { cause: error }));
|
||||
return promise;
|
||||
}
|
||||
|
||||
// if the result is not a promise then reject an error
|
||||
if (!result || typeof result.then !== "function") {
|
||||
reject(new Error("Result is not a promise."));
|
||||
return promise;
|
||||
}
|
||||
|
||||
this.#working++;
|
||||
promise.finally(() => {
|
||||
this.#working--;
|
||||
this.#processPending();
|
||||
})
|
||||
// `promise.finally` creates a new promise that may be rejected, so it must be handled.
|
||||
.catch(() => { });
|
||||
|
||||
// call the original function and catch any ENFILE or EMFILE errors
|
||||
Promise.resolve(result)
|
||||
.then(value => {
|
||||
debug("Function called successfully without retry.");
|
||||
resolve(value);
|
||||
})
|
||||
.catch(error => {
|
||||
if (!this.#check(error)) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
const task = new RetryTask(fn, error, resolve, reject, signal);
|
||||
|
||||
debug(`Function failed, queuing for retry with task ${task.id}.`);
|
||||
this.#retrying.push(task);
|
||||
|
||||
signal?.addEventListener("abort", () => {
|
||||
debug(`Task ${task.id} was aborted due to AbortSignal.`);
|
||||
reject(signal.reason);
|
||||
});
|
||||
|
||||
this.#processQueue();
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new retry job to the queue.
|
||||
* @template {(...args: unknown[]) => Promise<unknown>} Func
|
||||
* @template {Awaited<ReturnType<Func>>} RetVal
|
||||
* @param {Func} fn The function to call.
|
||||
* @param {object} [options] The options for the job.
|
||||
* @param {AbortSignal} [options.signal] The AbortSignal to monitor for cancellation.
|
||||
* @returns {Promise<RetVal>} A promise that resolves when the queue is processed.
|
||||
*/
|
||||
retry(fn, { signal } = {}) {
|
||||
|
||||
signal?.throwIfAborted();
|
||||
|
||||
const { promise, resolve, reject } = createPromise();
|
||||
|
||||
this.#pending.push(() => this.#call(fn, { signal, promise, resolve, reject }));
|
||||
this.#processPending();
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Processes the pending queue and the retry queue.
|
||||
* @returns {void}
|
||||
*/
|
||||
#processAll() {
|
||||
if (this.pending) {
|
||||
this.#processPending();
|
||||
}
|
||||
|
||||
if (this.retrying) {
|
||||
this.#processQueue();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the pending queue to see which tasks can be started.
|
||||
* @returns {void}
|
||||
*/
|
||||
#processPending() {
|
||||
|
||||
debug(`Processing pending tasks: ${this.pending} pending, ${this.working} working.`);
|
||||
|
||||
const available = this.#concurrency - this.working;
|
||||
|
||||
if (available <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const count = Math.min(this.pending, available);
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const task = this.#pending.shift();
|
||||
task?.();
|
||||
}
|
||||
|
||||
debug(`Processed pending tasks: ${this.pending} pending, ${this.working} working.`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the queue.
|
||||
* @returns {void}
|
||||
*/
|
||||
#processQueue() {
|
||||
// clear any timer because we're going to check right now
|
||||
clearTimeout(this.#timerId);
|
||||
this.#timerId = undefined;
|
||||
|
||||
debug(`Processing retry queue: ${this.retrying} retrying, ${this.working} working.`);
|
||||
|
||||
const processAgain = () => {
|
||||
this.#timerId = setTimeout(() => this.#processAll(), 0);
|
||||
};
|
||||
|
||||
// if there's nothing in the queue, we're done
|
||||
const task = this.#retrying.shift();
|
||||
if (!task) {
|
||||
debug("Queue is empty, exiting.");
|
||||
|
||||
if (this.pending) {
|
||||
processAgain();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// if it's time to bail, then bail
|
||||
if (isTimeToBail(task, this.#timeout)) {
|
||||
debug(`Task ${task.id} was abandoned due to timeout.`);
|
||||
task.reject(task.error);
|
||||
processAgain();
|
||||
return;
|
||||
}
|
||||
|
||||
// if it's not time to retry, then wait and try again
|
||||
if (!isTimeToRetry(task, this.#maxDelay)) {
|
||||
debug(`Task ${task.id} is not ready to retry, skipping.`);
|
||||
this.#retrying.push(task);
|
||||
processAgain();
|
||||
return;
|
||||
}
|
||||
|
||||
// otherwise, try again
|
||||
task.lastAttempt = Date.now();
|
||||
|
||||
// Promise.resolve needed in case it's a thenable but not a Promise
|
||||
Promise.resolve(task.fn())
|
||||
// @ts-ignore because we know it's any
|
||||
.then(result => {
|
||||
debug(`Task ${task.id} succeeded after ${task.age}ms.`);
|
||||
task.resolve(result);
|
||||
})
|
||||
|
||||
// @ts-ignore because we know it's any
|
||||
.catch(error => {
|
||||
if (!this.#check(error)) {
|
||||
debug(`Task ${task.id} failed with non-retryable error: ${error.message}.`);
|
||||
task.reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
// update the task timestamp and push to back of queue to try again
|
||||
task.lastAttempt = Date.now();
|
||||
this.#retrying.push(task);
|
||||
debug(`Task ${task.id} failed, requeueing to try again.`);
|
||||
})
|
||||
.finally(() => {
|
||||
this.#processAll();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { Retrier };
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"name": "@humanwhocodes/retry",
|
||||
"version": "0.4.3",
|
||||
"description": "A utility to retry failed async methods.",
|
||||
"type": "module",
|
||||
"main": "dist/retrier.cjs",
|
||||
"module": "dist/retrier.js",
|
||||
"types": "dist/retrier.d.ts",
|
||||
"exports": {
|
||||
"require": {
|
||||
"types": "./dist/retrier.d.cts",
|
||||
"default": "./dist/retrier.cjs"
|
||||
},
|
||||
"import": {
|
||||
"types": "./dist/retrier.d.ts",
|
||||
"default": "./dist/retrier.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18.18"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"gitHooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.js": [
|
||||
"eslint --fix"
|
||||
]
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/nzakas"
|
||||
},
|
||||
"scripts": {
|
||||
"build:cts-types": "node -e \"fs.copyFileSync('dist/retrier.d.ts', 'dist/retrier.d.cts')\"",
|
||||
"build": "rollup -c && tsc && npm run build:cts-types",
|
||||
"prepare": "npm run build",
|
||||
"lint": "eslint src/ tests/",
|
||||
"pretest": "npm run build",
|
||||
"test:unit": "mocha tests/retrier.test.js",
|
||||
"test:build": "node tests/pkg.test.cjs && node tests/pkg.test.mjs",
|
||||
"test:jsr": "npx jsr@latest publish --dry-run",
|
||||
"test:emfile": "node tools/check-emfile-handling.js",
|
||||
"test": "npm run test:unit && npm run test:build"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/humanwhocodes/retry.git"
|
||||
},
|
||||
"keywords": [
|
||||
"nodejs",
|
||||
"retry",
|
||||
"async",
|
||||
"promises"
|
||||
],
|
||||
"author": "Nicholas C. Zaks",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^8.49.0",
|
||||
"@rollup/plugin-terser": "0.4.4",
|
||||
"@tsconfig/node16": "^16.1.1",
|
||||
"@types/mocha": "^10.0.3",
|
||||
"@types/node": "20.12.6",
|
||||
"eslint": "^8.21.0",
|
||||
"lint-staged": "15.2.1",
|
||||
"mocha": "^10.3.0",
|
||||
"rollup": "3.29.4",
|
||||
"typescript": "5.4.4",
|
||||
"yorkie": "2.0.0"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user