Compare commits
1 Commits
better-xen
...
gen-signed
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48c8d25774 |
37
.eslintrc.js
37
.eslintrc.js
@@ -1,7 +1,5 @@
|
||||
'use strict'
|
||||
|
||||
module.exports = {
|
||||
extends: ['plugin:eslint-comments/recommended', 'plugin:n/recommended', 'standard', 'standard-jsx', 'prettier'],
|
||||
extends: ['plugin:eslint-comments/recommended', 'standard', 'standard-jsx', 'prettier'],
|
||||
globals: {
|
||||
__DEV__: true,
|
||||
$Dict: true,
|
||||
@@ -17,42 +15,11 @@ module.exports = {
|
||||
{
|
||||
files: ['cli.{,c,m}js', '*-cli.{,c,m}js', '**/*cli*/**/*.{,c,m}js'],
|
||||
rules: {
|
||||
'n/no-process-exit': 'off',
|
||||
'no-console': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.mjs'],
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.{spec,test}.{,c,m}js'],
|
||||
rules: {
|
||||
'n/no-unpublished-require': 'off',
|
||||
'n/no-unpublished-import': 'off',
|
||||
'n/no-unsupported-features/node-builtins': [
|
||||
'error',
|
||||
{
|
||||
version: '>=16',
|
||||
},
|
||||
],
|
||||
'n/no-unsupported-features/es-syntax': [
|
||||
'error',
|
||||
{
|
||||
version: '>=16',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
parserOptions: {
|
||||
ecmaVersion: 13,
|
||||
sourceType: 'script',
|
||||
},
|
||||
|
||||
rules: {
|
||||
// disabled because XAPI objects are using camel case
|
||||
camelcase: ['off'],
|
||||
@@ -67,7 +34,5 @@ module.exports = {
|
||||
'lines-between-class-members': 'off',
|
||||
|
||||
'no-console': ['error', { allow: ['warn', 'error'] }],
|
||||
|
||||
strict: 'error',
|
||||
},
|
||||
}
|
||||
|
||||
16
.flowconfig
Normal file
16
.flowconfig
Normal file
@@ -0,0 +1,16 @@
|
||||
[ignore]
|
||||
<PROJECT_ROOT>/node_modules/.*
|
||||
|
||||
[include]
|
||||
|
||||
[libs]
|
||||
|
||||
[lints]
|
||||
|
||||
[options]
|
||||
esproposal.decorators=ignore
|
||||
esproposal.optional_chaining=enable
|
||||
include_warnings=true
|
||||
module.use_strict=true
|
||||
|
||||
[strict]
|
||||
27
.github/ISSUE_TEMPLATE/bug_report.md
vendored
27
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -4,30 +4,14 @@ about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: 'status: triaging :triangular_flag_on_post:, type: bug :bug:'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
1. ⚠️ **If you don't follow this template, the issue will be closed**.
|
||||
2. ⚠️ **If your issue can't be easily reproduced, please report it [on the forum first](https://xcp-ng.org/forum/category/12/xen-orchestra)**.
|
||||
|
||||
Are you using XOA or XO from the sources?
|
||||
|
||||
If XOA:
|
||||
|
||||
- which release channel? (`stable` vs `latest`)
|
||||
- please consider creating a support ticket in [your dedicated support area](https://xen-orchestra.com/#!/member/support)
|
||||
|
||||
If XO from the sources:
|
||||
|
||||
- Provide **your commit number**. If it's older than a week, we won't investigate
|
||||
- Don't forget to [read this first](https://xen-orchestra.com/docs/community.html)
|
||||
- As well as follow [this guide](https://xen-orchestra.com/docs/community.html#report-a-bug)
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
@@ -39,10 +23,11 @@ A clear and concise description of what you expected to happen.
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Environment (please provide the following information):**
|
||||
|
||||
- Node: [e.g. 16.12.1]
|
||||
- hypervisor: [e.g. XCP-ng 8.2.0]
|
||||
**Desktop (please complete the following information):**
|
||||
- Node: [e.g. 16.12.1]
|
||||
- xo-server: [e.g. 5.82.3]
|
||||
- xo-web: [e.g. 5.87.0]
|
||||
- hypervisor: [e.g. XCP-ng 8.2.0]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/feature_request.md
vendored
1
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -4,6 +4,7 @@ about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
|
||||
12
.github/workflows/push.yml
vendored
12
.github/workflows/push.yml
vendored
@@ -1,12 +0,0 @@
|
||||
name: CI
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Build docker image
|
||||
run: docker-compose -f docker/docker-compose.dev.yml build
|
||||
- name: Create the container and start the tests
|
||||
run: docker-compose -f docker/docker-compose.dev.yml up --exit-code-from xo
|
||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
/_book/
|
||||
/coverage/
|
||||
/node_modules/
|
||||
/lerna-debug.log
|
||||
/lerna-debug.log.*
|
||||
@@ -10,6 +11,10 @@
|
||||
/packages/*/dist/
|
||||
/packages/*/node_modules/
|
||||
|
||||
/@xen-orchestra/proxy/src/app/mixins/index.mjs
|
||||
|
||||
/packages/vhd-cli/src/commands/index.js
|
||||
|
||||
/packages/xen-api/examples/node_modules/
|
||||
/packages/xen-api/plot.dat
|
||||
|
||||
@@ -30,8 +35,3 @@ pnpm-debug.log.*
|
||||
yarn-error.log
|
||||
yarn-error.log.*
|
||||
.env
|
||||
|
||||
# code coverage
|
||||
.nyc_output/
|
||||
coverage/
|
||||
.turbo/
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
. "$(dirname -- "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
||||
23
.travis.yml
Normal file
23
.travis.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- 14
|
||||
|
||||
# Use containers.
|
||||
# http://docs.travis-ci.com/user/workers/container-based-infrastructure/
|
||||
sudo: false
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- qemu-utils
|
||||
- blktap-utils
|
||||
- vmdk-stream-converter
|
||||
|
||||
before_install:
|
||||
- curl -o- -L https://yarnpkg.com/install.sh | bash
|
||||
- export PATH="$HOME/.yarn/bin:$PATH"
|
||||
|
||||
cache:
|
||||
yarn: true
|
||||
|
||||
script:
|
||||
- yarn run travis-tests
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/async-each):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/async-each
|
||||
```
|
||||
> npm install --save @vates/async-each
|
||||
```
|
||||
|
||||
## Usage
|
||||
@@ -32,7 +32,7 @@ Returns a promise wich rejects as soon as a call to `iteratee` throws or a promi
|
||||
|
||||
`opts` is an object that can contains the following options:
|
||||
|
||||
- `concurrency`: a number which indicates the maximum number of parallel call to `iteratee`, defaults to `10`. The value `0` means no concurrency limit.
|
||||
- `concurrency`: a number which indicates the maximum number of parallel call to `iteratee`, defaults to `1`
|
||||
- `signal`: an abort signal to stop the iteration
|
||||
- `stopOnError`: wether to stop iteration of first error, or wait for all calls to finish and throw an `AggregateError`, defaults to `true`
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ Returns a promise wich rejects as soon as a call to `iteratee` throws or a promi
|
||||
|
||||
`opts` is an object that can contains the following options:
|
||||
|
||||
- `concurrency`: a number which indicates the maximum number of parallel call to `iteratee`, defaults to `10`. The value `0` means no concurrency limit.
|
||||
- `concurrency`: a number which indicates the maximum number of parallel call to `iteratee`, defaults to `1`
|
||||
- `signal`: an abort signal to stop the iteration
|
||||
- `stopOnError`: wether to stop iteration of first error, or wait for all calls to finish and throw an `AggregateError`, defaults to `true`
|
||||
|
||||
@@ -9,16 +9,7 @@ class AggregateError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template Item
|
||||
* @param {Iterable<Item>} iterable
|
||||
* @param {(item: Item, index: number, iterable: Iterable<Item>) => Promise<void>} iteratee
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
exports.asyncEach = function asyncEach(iterable, iteratee, { concurrency = 10, signal, stopOnError = true } = {}) {
|
||||
if (concurrency === 0) {
|
||||
concurrency = Infinity
|
||||
}
|
||||
exports.asyncEach = function asyncEach(iterable, iteratee, { concurrency = 1, signal, stopOnError = true } = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const it = (iterable[Symbol.iterator] || iterable[Symbol.asyncIterator]).call(iterable)
|
||||
const errors = []
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
'use strict'
|
||||
|
||||
const { describe, it, beforeEach } = require('test')
|
||||
const assert = require('assert').strict
|
||||
const { spy } = require('sinon')
|
||||
/* eslint-env jest */
|
||||
|
||||
const { asyncEach } = require('./')
|
||||
|
||||
@@ -36,18 +34,12 @@ describe('asyncEach', () => {
|
||||
})
|
||||
|
||||
it('works', async () => {
|
||||
const iteratee = spy(async () => {})
|
||||
const iteratee = jest.fn(async () => {})
|
||||
|
||||
await asyncEach.call(thisArg, iterable, iteratee, { concurrency: 1 })
|
||||
await asyncEach.call(thisArg, iterable, iteratee)
|
||||
|
||||
assert.deepStrictEqual(
|
||||
iteratee.thisValues,
|
||||
Array.from(values, () => thisArg)
|
||||
)
|
||||
assert.deepStrictEqual(
|
||||
iteratee.args,
|
||||
Array.from(values, (value, index) => [value, index, iterable])
|
||||
)
|
||||
expect(iteratee.mock.instances).toEqual(Array.from(values, () => thisArg))
|
||||
expect(iteratee.mock.calls).toEqual(Array.from(values, (value, index) => [value, index, iterable]))
|
||||
})
|
||||
;[1, 2, 4].forEach(concurrency => {
|
||||
it('respects a concurrency of ' + concurrency, async () => {
|
||||
@@ -57,7 +49,7 @@ describe('asyncEach', () => {
|
||||
values,
|
||||
async () => {
|
||||
++running
|
||||
assert.deepStrictEqual(running <= concurrency, true)
|
||||
expect(running).toBeLessThanOrEqual(concurrency)
|
||||
await randomDelay()
|
||||
--running
|
||||
},
|
||||
@@ -67,52 +59,40 @@ describe('asyncEach', () => {
|
||||
})
|
||||
|
||||
it('stops on first error when stopOnError is true', async () => {
|
||||
const tracker = new assert.CallTracker()
|
||||
|
||||
const error = new Error()
|
||||
const iteratee = tracker.calls((_, i) => {
|
||||
const iteratee = jest.fn((_, i) => {
|
||||
if (i === 1) {
|
||||
throw error
|
||||
}
|
||||
}, 2)
|
||||
assert.deepStrictEqual(
|
||||
await rejectionOf(asyncEach(iterable, iteratee, { concurrency: 1, stopOnError: true })),
|
||||
error
|
||||
)
|
||||
})
|
||||
|
||||
tracker.verify()
|
||||
expect(await rejectionOf(asyncEach(iterable, iteratee, { stopOnError: true }))).toBe(error)
|
||||
expect(iteratee).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('rejects AggregateError when stopOnError is false', async () => {
|
||||
const errors = []
|
||||
const iteratee = spy(() => {
|
||||
const iteratee = jest.fn(() => {
|
||||
const error = new Error()
|
||||
errors.push(error)
|
||||
throw error
|
||||
})
|
||||
|
||||
const error = await rejectionOf(asyncEach(iterable, iteratee, { stopOnError: false }))
|
||||
assert.deepStrictEqual(error.errors, errors)
|
||||
assert.deepStrictEqual(
|
||||
iteratee.args,
|
||||
Array.from(values, (value, index) => [value, index, iterable])
|
||||
)
|
||||
expect(error.errors).toEqual(errors)
|
||||
expect(iteratee.mock.calls).toEqual(Array.from(values, (value, index) => [value, index, iterable]))
|
||||
})
|
||||
|
||||
it('can be interrupted with an AbortSignal', async () => {
|
||||
const tracker = new assert.CallTracker()
|
||||
|
||||
const ac = new AbortController()
|
||||
const iteratee = tracker.calls((_, i) => {
|
||||
const iteratee = jest.fn((_, i) => {
|
||||
if (i === 1) {
|
||||
ac.abort()
|
||||
}
|
||||
}, 2)
|
||||
await assert.rejects(asyncEach(iterable, iteratee, { concurrency: 1, signal: ac.signal }), {
|
||||
message: 'asyncEach aborted',
|
||||
})
|
||||
|
||||
tracker.verify()
|
||||
await expect(asyncEach(iterable, iteratee, { signal: ac.signal })).rejects.toThrow('asyncEach aborted')
|
||||
expect(iteratee).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
})
|
||||
)
|
||||
@@ -24,17 +24,11 @@
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"license": "ISC",
|
||||
"version": "1.0.0",
|
||||
"version": "0.1.0",
|
||||
"engines": {
|
||||
"node": ">=8.10"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public",
|
||||
"test": "node--test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"sinon": "^15.0.1",
|
||||
"tap": "^16.3.0",
|
||||
"test": "^3.2.1"
|
||||
"postversion": "npm publish --access public"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
Node does not cache queries to `dns.lookup`, which can lead application doing a lot of connections to have perf issues and to saturate Node threads pool.
|
||||
|
||||
This library attempts to mitigate these problems by providing a version of this function with a version short cache, applied on both errors and results.
|
||||
|
||||
> Limitation: `verbatim: false` option is not supported.
|
||||
|
||||
It has exactly the same API as the native method and can be used directly:
|
||||
|
||||
```js
|
||||
import { createCachedLookup } from '@vates/cached-dns.lookup'
|
||||
|
||||
const lookup = createCachedLookup()
|
||||
|
||||
lookup('example.net', { all: true, family: 0 }, (error, result) => {
|
||||
if (error != null) {
|
||||
return console.warn(error)
|
||||
}
|
||||
console.log(result)
|
||||
})
|
||||
```
|
||||
|
||||
Or it can be used to replace the native implementation and speed up the whole app:
|
||||
|
||||
```js
|
||||
// assign our cached implementation to dns.lookup
|
||||
const restore = createCachedLookup().patchGlobal()
|
||||
|
||||
// to restore the previous implementation
|
||||
restore()
|
||||
```
|
||||
@@ -1 +0,0 @@
|
||||
../../scripts/npmignore
|
||||
@@ -1,63 +0,0 @@
|
||||
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
|
||||
|
||||
# @vates/cached-dns.lookup
|
||||
|
||||
[](https://npmjs.org/package/@vates/cached-dns.lookup)  [](https://bundlephobia.com/result?p=@vates/cached-dns.lookup) [](https://npmjs.org/package/@vates/cached-dns.lookup)
|
||||
|
||||
> Cached implementation of dns.lookup
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/cached-dns.lookup):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/cached-dns.lookup
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Node does not cache queries to `dns.lookup`, which can lead application doing a lot of connections to have perf issues and to saturate Node threads pool.
|
||||
|
||||
This library attempts to mitigate these problems by providing a version of this function with a version short cache, applied on both errors and results.
|
||||
|
||||
> Limitation: `verbatim: false` option is not supported.
|
||||
|
||||
It has exactly the same API as the native method and can be used directly:
|
||||
|
||||
```js
|
||||
import { createCachedLookup } from '@vates/cached-dns.lookup'
|
||||
|
||||
const lookup = createCachedLookup()
|
||||
|
||||
lookup('example.net', { all: true, family: 0 }, (error, result) => {
|
||||
if (error != null) {
|
||||
return console.warn(error)
|
||||
}
|
||||
console.log(result)
|
||||
})
|
||||
```
|
||||
|
||||
Or it can be used to replace the native implementation and speed up the whole app:
|
||||
|
||||
```js
|
||||
// assign our cached implementation to dns.lookup
|
||||
const restore = createCachedLookup().patchGlobal()
|
||||
|
||||
// to restore the previous implementation
|
||||
restore()
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are _very_ welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)
|
||||
@@ -1,72 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const assert = require('assert')
|
||||
const dns = require('dns')
|
||||
const LRU = require('lru-cache')
|
||||
|
||||
function reportResults(all, results, callback) {
|
||||
if (all) {
|
||||
callback(null, results)
|
||||
} else {
|
||||
const first = results[0]
|
||||
callback(null, first.address, first.family)
|
||||
}
|
||||
}
|
||||
|
||||
exports.createCachedLookup = function createCachedLookup({ lookup = dns.lookup } = {}) {
|
||||
const cache = new LRU({
|
||||
max: 500,
|
||||
|
||||
// 1 minute: long enough to be effective, short enough so there is no need to bother with DNS TTLs
|
||||
ttl: 60e3,
|
||||
})
|
||||
|
||||
function cachedLookup(hostname, options, callback) {
|
||||
let all = false
|
||||
let family = 0
|
||||
if (typeof options === 'function') {
|
||||
callback = options
|
||||
} else if (typeof options === 'number') {
|
||||
family = options
|
||||
} else if (options != null) {
|
||||
assert.notStrictEqual(options.verbatim, false, 'not supported by this implementation')
|
||||
;({ all = all, family = family } = options)
|
||||
}
|
||||
|
||||
// cache by family option because there will be an error if there is no
|
||||
// entries for the requestion family so we cannot easily cache all families
|
||||
// and filter on reporting back
|
||||
const key = hostname + '/' + family
|
||||
|
||||
const result = cache.get(key)
|
||||
if (result !== undefined) {
|
||||
setImmediate(reportResults, all, result, callback)
|
||||
} else {
|
||||
lookup(hostname, { all: true, family, verbatim: true }, function onLookup(error, results) {
|
||||
// errors are not cached because this will delay recovery after DNS/network issues
|
||||
//
|
||||
// there are no reliable way to detect if the error is real or simply
|
||||
// that there are no results for the requested hostname
|
||||
//
|
||||
// there should be much fewer errors than success, therefore it should
|
||||
// not be a big deal to not cache them
|
||||
if (error != null) {
|
||||
return callback(error)
|
||||
}
|
||||
|
||||
cache.set(key, results)
|
||||
reportResults(all, results, callback)
|
||||
})
|
||||
}
|
||||
}
|
||||
cachedLookup.patchGlobal = function patchGlobal() {
|
||||
const previous = dns.lookup
|
||||
dns.lookup = cachedLookup
|
||||
return function restoreGlobal() {
|
||||
assert.strictEqual(dns.lookup, cachedLookup)
|
||||
dns.lookup = previous
|
||||
}
|
||||
}
|
||||
|
||||
return cachedLookup
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"dependencies": {
|
||||
"lru-cache": "^7.0.4"
|
||||
},
|
||||
"private": false,
|
||||
"name": "@vates/cached-dns.lookup",
|
||||
"description": "Cached implementation of dns.lookup",
|
||||
"keywords": [
|
||||
"cache",
|
||||
"dns",
|
||||
"lookup"
|
||||
],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/cached-dns.lookup",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"directory": "@vates/cached-dns.lookup",
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Vates SAS",
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"license": "ISC",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public"
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/coalesce-calls):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/coalesce-calls
|
||||
```
|
||||
> npm install --save @vates/coalesce-calls
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
'use strict'
|
||||
|
||||
exports.coalesceCalls = function (fn) {
|
||||
let promise
|
||||
const clean = () => {
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
'use strict'
|
||||
|
||||
const { describe, it } = require('test')
|
||||
const assert = require('assert')
|
||||
/* eslint-env jest */
|
||||
|
||||
const { coalesceCalls } = require('./')
|
||||
|
||||
@@ -24,13 +21,13 @@ describe('coalesceCalls', () => {
|
||||
const promise2 = fn(defer2.promise)
|
||||
|
||||
defer1.resolve('foo')
|
||||
assert.strictEqual(await promise1, 'foo')
|
||||
assert.strictEqual(await promise2, 'foo')
|
||||
expect(await promise1).toBe('foo')
|
||||
expect(await promise2).toBe('foo')
|
||||
|
||||
const defer3 = pDefer()
|
||||
const promise3 = fn(defer3.promise)
|
||||
|
||||
defer3.resolve('bar')
|
||||
assert.strictEqual(await promise3, 'bar')
|
||||
expect(await promise3).toBe('bar')
|
||||
})
|
||||
})
|
||||
@@ -30,10 +30,6 @@
|
||||
"node": ">=8.10"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public",
|
||||
"test": "node--test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"test": "^3.2.1"
|
||||
"postversion": "npm publish --access public"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/compose):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/compose
|
||||
```
|
||||
> npm install --save @vates/compose
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
'use strict'
|
||||
|
||||
const { describe, it } = require('test')
|
||||
const assert = require('node:assert').strict
|
||||
/* eslint-env jest */
|
||||
|
||||
const { compose } = require('./')
|
||||
|
||||
@@ -10,42 +7,43 @@ const mul3 = x => x * 3
|
||||
|
||||
describe('compose()', () => {
|
||||
it('throws when no functions is passed', () => {
|
||||
assert.throws(() => compose(), TypeError)
|
||||
assert.throws(() => compose([]), TypeError)
|
||||
expect(() => compose()).toThrow(TypeError)
|
||||
expect(() => compose([])).toThrow(TypeError)
|
||||
})
|
||||
|
||||
it('applies from left to right', () => {
|
||||
assert.strictEqual(compose(add2, mul3)(5), 21)
|
||||
expect(compose(add2, mul3)(5)).toBe(21)
|
||||
})
|
||||
|
||||
it('accepts functions in an array', () => {
|
||||
assert.strictEqual(compose([add2, mul3])(5), 21)
|
||||
expect(compose([add2, mul3])(5)).toBe(21)
|
||||
})
|
||||
|
||||
it('can apply from right to left', () => {
|
||||
assert.strictEqual(compose({ right: true }, add2, mul3)(5), 17)
|
||||
expect(compose({ right: true }, add2, mul3)(5)).toBe(17)
|
||||
})
|
||||
|
||||
it('accepts options with functions in an array', () => {
|
||||
assert.strictEqual(compose({ right: true }, [add2, mul3])(5), 17)
|
||||
expect(compose({ right: true }, [add2, mul3])(5)).toBe(17)
|
||||
})
|
||||
|
||||
it('can compose async functions', async () => {
|
||||
assert.strictEqual(
|
||||
expect(
|
||||
await compose(
|
||||
{ async: true },
|
||||
async x => x + 2,
|
||||
async x => x * 3
|
||||
)(5),
|
||||
21
|
||||
)
|
||||
)(5)
|
||||
).toBe(21)
|
||||
})
|
||||
|
||||
it('forwards all args to first function', () => {
|
||||
expect.assertions(1)
|
||||
|
||||
const expectedArgs = [Math.random(), Math.random()]
|
||||
compose(
|
||||
(...args) => {
|
||||
assert.deepEqual(args, expectedArgs)
|
||||
expect(args).toEqual(expectedArgs)
|
||||
},
|
||||
// add a second function to avoid the one function special case
|
||||
Function.prototype
|
||||
@@ -53,13 +51,15 @@ describe('compose()', () => {
|
||||
})
|
||||
|
||||
it('forwards context to all functions', () => {
|
||||
expect.assertions(2)
|
||||
|
||||
const expectedThis = {}
|
||||
compose(
|
||||
function () {
|
||||
assert.strictEqual(this, expectedThis)
|
||||
expect(this).toBe(expectedThis)
|
||||
},
|
||||
function () {
|
||||
assert.strictEqual(this, expectedThis)
|
||||
expect(this).toBe(expectedThis)
|
||||
}
|
||||
).call(expectedThis)
|
||||
})
|
||||
@@ -19,10 +19,6 @@
|
||||
"node": ">=7.6"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public",
|
||||
"test": "node--test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"test": "^3.2.1"
|
||||
"postversion": "npm publish --access public"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
### `decorateWith(fn, ...args)`
|
||||
|
||||
Creates a new ([legacy](https://babeljs.io/docs/en/babel-plugin-syntax-decorators#legacy)) method decorator from a function decorator, for instance, allows using Lodash's functions as decorators:
|
||||
|
||||
```js
|
||||
import { decorateWith } from '@vates/decorate-with'
|
||||
|
||||
class Foo {
|
||||
@decorateWith(lodash.debounce, 150)
|
||||
bar() {
|
||||
// body
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `decorateClass(class, map)`
|
||||
|
||||
Decorates a number of accessors and methods directly, without using the decorator syntax:
|
||||
|
||||
```js
|
||||
import { decorateClass } from '@vates/decorate-with'
|
||||
|
||||
class Foo {
|
||||
get bar() {
|
||||
// body
|
||||
}
|
||||
|
||||
set bar(value) {
|
||||
// body
|
||||
}
|
||||
|
||||
baz() {
|
||||
// body
|
||||
}
|
||||
}
|
||||
|
||||
decorateClass(Foo, {
|
||||
// getter and/or setter
|
||||
bar: {
|
||||
// without arguments
|
||||
get: lodash.memoize,
|
||||
|
||||
// with arguments
|
||||
set: [lodash.debounce, 150],
|
||||
},
|
||||
|
||||
// method (with or without arguments)
|
||||
baz: lodash.curry,
|
||||
})
|
||||
```
|
||||
|
||||
The decorated class is returned, so you can export it directly.
|
||||
|
||||
To apply multiple transforms to an accessor/method, you can either call `decorateClass` multiple times or use [`@vates/compose`](https://www.npmjs.com/package/@vates/compose):
|
||||
|
||||
```js
|
||||
decorateClass(Foo, {
|
||||
baz: compose([
|
||||
[lodash.debounce, 150]
|
||||
lodash.curry,
|
||||
])
|
||||
})
|
||||
```
|
||||
|
||||
### `perInstance(fn, ...args)`
|
||||
|
||||
Helper to decorate the method by instance instead of for the whole class.
|
||||
|
||||
This is often necessary for caching or deduplicating calls.
|
||||
|
||||
```js
|
||||
import { perInstance } from '@vates/decorateWith'
|
||||
|
||||
class Foo {
|
||||
@decorateWith(perInstance, lodash.memoize)
|
||||
bar() {
|
||||
// body
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Because it's a normal function, it can also be used with `decorateClass`, with `compose` or even by itself.
|
||||
|
||||
### `decorateMethodsWith(class, map)`
|
||||
|
||||
> Deprecated alias for [`decorateClass(class, map)`](#decorateclassclass-map).
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/decorate-with):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/decorate-with
|
||||
```
|
||||
> npm install --save @vates/decorate-with
|
||||
```
|
||||
|
||||
## Usage
|
||||
@@ -31,19 +31,15 @@ class Foo {
|
||||
}
|
||||
```
|
||||
|
||||
### `decorateClass(class, map)`
|
||||
### `decorateMethodsWith(class, map)`
|
||||
|
||||
Decorates a number of accessors and methods directly, without using the decorator syntax:
|
||||
Decorates a number of methods directly, without using the decorator syntax:
|
||||
|
||||
```js
|
||||
import { decorateClass } from '@vates/decorate-with'
|
||||
import { decorateMethodsWith } from '@vates/decorate-with'
|
||||
|
||||
class Foo {
|
||||
get bar() {
|
||||
// body
|
||||
}
|
||||
|
||||
set bar(value) {
|
||||
bar() {
|
||||
// body
|
||||
}
|
||||
|
||||
@@ -52,57 +48,28 @@ class Foo {
|
||||
}
|
||||
}
|
||||
|
||||
decorateClass(Foo, {
|
||||
// getter and/or setter
|
||||
bar: {
|
||||
// without arguments
|
||||
get: lodash.memoize,
|
||||
decorateMethodsWith(Foo, {
|
||||
// without arguments
|
||||
bar: lodash.curry,
|
||||
|
||||
// with arguments
|
||||
set: [lodash.debounce, 150],
|
||||
},
|
||||
|
||||
// method (with or without arguments)
|
||||
baz: lodash.curry,
|
||||
// with arguments
|
||||
baz: [lodash.debounce, 150],
|
||||
})
|
||||
```
|
||||
|
||||
The decorated class is returned, so you can export it directly.
|
||||
|
||||
To apply multiple transforms to an accessor/method, you can either call `decorateClass` multiple times or use [`@vates/compose`](https://www.npmjs.com/package/@vates/compose):
|
||||
To apply multiple transforms to a method, you can either call `decorateMethodsWith` multiple times or use [`@vates/compose`](https://www.npmjs.com/package/@vates/compose):
|
||||
|
||||
```js
|
||||
decorateClass(Foo, {
|
||||
baz: compose([
|
||||
decorateMethodsWith(Foo, {
|
||||
bar: compose([
|
||||
[lodash.debounce, 150]
|
||||
lodash.curry,
|
||||
])
|
||||
})
|
||||
```
|
||||
|
||||
### `perInstance(fn, ...args)`
|
||||
|
||||
Helper to decorate the method by instance instead of for the whole class.
|
||||
|
||||
This is often necessary for caching or deduplicating calls.
|
||||
|
||||
```js
|
||||
import { perInstance } from '@vates/decorateWith'
|
||||
|
||||
class Foo {
|
||||
@decorateWith(perInstance, lodash.memoize)
|
||||
bar() {
|
||||
// body
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Because it's a normal function, it can also be used with `decorateClass`, with `compose` or even by itself.
|
||||
|
||||
### `decorateMethodsWith(class, map)`
|
||||
|
||||
> Deprecated alias for [`decorateClass(class, map)`](#decorateclassclass-map).
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are _very_ welcomed, either on the documentation or on
|
||||
|
||||
53
@vates/decorate-with/USAGE.md
Normal file
53
@vates/decorate-with/USAGE.md
Normal file
@@ -0,0 +1,53 @@
|
||||
### `decorateWith(fn, ...args)`
|
||||
|
||||
Creates a new ([legacy](https://babeljs.io/docs/en/babel-plugin-syntax-decorators#legacy)) method decorator from a function decorator, for instance, allows using Lodash's functions as decorators:
|
||||
|
||||
```js
|
||||
import { decorateWith } from '@vates/decorate-with'
|
||||
|
||||
class Foo {
|
||||
@decorateWith(lodash.debounce, 150)
|
||||
bar() {
|
||||
// body
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `decorateMethodsWith(class, map)`
|
||||
|
||||
Decorates a number of methods directly, without using the decorator syntax:
|
||||
|
||||
```js
|
||||
import { decorateMethodsWith } from '@vates/decorate-with'
|
||||
|
||||
class Foo {
|
||||
bar() {
|
||||
// body
|
||||
}
|
||||
|
||||
baz() {
|
||||
// body
|
||||
}
|
||||
}
|
||||
|
||||
decorateMethodsWith(Foo, {
|
||||
// without arguments
|
||||
bar: lodash.curry,
|
||||
|
||||
// with arguments
|
||||
baz: [lodash.debounce, 150],
|
||||
})
|
||||
```
|
||||
|
||||
The decorated class is returned, so you can export it directly.
|
||||
|
||||
To apply multiple transforms to a method, you can either call `decorateMethodsWith` multiple times or use [`@vates/compose`](https://www.npmjs.com/package/@vates/compose):
|
||||
|
||||
```js
|
||||
decorateMethodsWith(Foo, {
|
||||
bar: compose([
|
||||
[lodash.debounce, 150]
|
||||
lodash.curry,
|
||||
])
|
||||
})
|
||||
```
|
||||
@@ -1,5 +1,3 @@
|
||||
'use strict'
|
||||
|
||||
exports.decorateWith = function decorateWith(fn, ...args) {
|
||||
return (target, name, descriptor) => ({
|
||||
...descriptor,
|
||||
@@ -9,40 +7,15 @@ exports.decorateWith = function decorateWith(fn, ...args) {
|
||||
|
||||
const { getOwnPropertyDescriptor, defineProperty } = Object
|
||||
|
||||
function applyDecorator(decorator, value) {
|
||||
return typeof decorator === 'function' ? decorator(value) : decorator[0](value, ...decorator.slice(1))
|
||||
}
|
||||
|
||||
exports.decorateClass = exports.decorateMethodsWith = function decorateClass(klass, map) {
|
||||
exports.decorateMethodsWith = function decorateMethodsWith(klass, map) {
|
||||
const { prototype } = klass
|
||||
for (const name of Object.keys(map)) {
|
||||
const decorator = map[name]
|
||||
const descriptor = getOwnPropertyDescriptor(prototype, name)
|
||||
if (typeof decorator === 'function' || Array.isArray(decorator)) {
|
||||
descriptor.value = applyDecorator(decorator, descriptor.value)
|
||||
} else {
|
||||
const { get, set } = decorator
|
||||
if (get !== undefined) {
|
||||
descriptor.get = applyDecorator(get, descriptor.get)
|
||||
}
|
||||
if (set !== undefined) {
|
||||
descriptor.set = applyDecorator(set, descriptor.set)
|
||||
}
|
||||
}
|
||||
const { value } = descriptor
|
||||
|
||||
const decorator = map[name]
|
||||
descriptor.value = typeof decorator === 'function' ? decorator(value) : decorator[0](value, ...decorator.slice(1))
|
||||
defineProperty(prototype, name, descriptor)
|
||||
}
|
||||
return klass
|
||||
}
|
||||
|
||||
exports.perInstance = function perInstance(fn, decorator, ...args) {
|
||||
const map = new WeakMap()
|
||||
return function () {
|
||||
let decorated = map.get(this)
|
||||
if (decorated === undefined) {
|
||||
decorated = decorator(fn, ...args)
|
||||
map.set(this, decorated)
|
||||
}
|
||||
return decorated.apply(this, arguments)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,152 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const assert = require('assert')
|
||||
const { describe, it } = require('test')
|
||||
|
||||
const { decorateClass, decorateWith, decorateMethodsWith, perInstance } = require('./')
|
||||
|
||||
const identity = _ => _
|
||||
|
||||
describe('decorateWith', () => {
|
||||
it('works', () => {
|
||||
const expectedArgs = [Math.random(), Math.random()]
|
||||
const expectedFn = Function.prototype
|
||||
const newFn = () => {}
|
||||
|
||||
const decorator = decorateWith(function wrapper(fn, ...args) {
|
||||
assert.deepStrictEqual(fn, expectedFn)
|
||||
assert.deepStrictEqual(args, expectedArgs)
|
||||
|
||||
return newFn
|
||||
}, ...expectedArgs)
|
||||
|
||||
const descriptor = {
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
value: expectedFn,
|
||||
writable: true,
|
||||
}
|
||||
assert.deepStrictEqual(decorator({}, 'foo', descriptor), {
|
||||
...descriptor,
|
||||
value: newFn,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('decorateClass', () => {
|
||||
it('works', () => {
|
||||
class C {
|
||||
foo() {}
|
||||
bar() {}
|
||||
get baz() {}
|
||||
// eslint-disable-next-line accessor-pairs
|
||||
set qux(_) {}
|
||||
}
|
||||
|
||||
const expectedArgs = [Math.random(), Math.random()]
|
||||
const P = C.prototype
|
||||
|
||||
const descriptors = Object.getOwnPropertyDescriptors(P)
|
||||
|
||||
const newFoo = () => {}
|
||||
const newBar = () => {}
|
||||
const newGetBaz = () => {}
|
||||
const newSetQux = _ => {}
|
||||
|
||||
decorateClass(C, {
|
||||
foo(fn) {
|
||||
assert.strictEqual(arguments.length, 1)
|
||||
assert.strictEqual(fn, P.foo)
|
||||
return newFoo
|
||||
},
|
||||
bar: [
|
||||
function (fn, ...args) {
|
||||
assert.strictEqual(fn, P.bar)
|
||||
assert.deepStrictEqual(args, expectedArgs)
|
||||
return newBar
|
||||
},
|
||||
...expectedArgs,
|
||||
],
|
||||
baz: {
|
||||
get(fn) {
|
||||
assert.strictEqual(arguments.length, 1)
|
||||
assert.strictEqual(fn, descriptors.baz.get)
|
||||
return newGetBaz
|
||||
},
|
||||
},
|
||||
qux: {
|
||||
set: [
|
||||
function (fn, ...args) {
|
||||
assert.strictEqual(fn, descriptors.qux.set)
|
||||
assert.deepStrictEqual(args, expectedArgs)
|
||||
return newSetQux
|
||||
},
|
||||
...expectedArgs,
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const newDescriptors = Object.getOwnPropertyDescriptors(P)
|
||||
assert.deepStrictEqual(newDescriptors.foo, { ...descriptors.foo, value: newFoo })
|
||||
assert.deepStrictEqual(newDescriptors.bar, { ...descriptors.bar, value: newBar })
|
||||
assert.deepStrictEqual(newDescriptors.baz, { ...descriptors.baz, get: newGetBaz })
|
||||
assert.deepStrictEqual(newDescriptors.qux, { ...descriptors.qux, set: newSetQux })
|
||||
})
|
||||
|
||||
it('throws if using an accessor decorator for a method', function () {
|
||||
assert.throws(() =>
|
||||
decorateClass(
|
||||
class {
|
||||
foo() {}
|
||||
},
|
||||
{ foo: { get: identity, set: identity } }
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
it('throws if using a method decorator for an accessor', function () {
|
||||
assert.throws(() =>
|
||||
decorateClass(
|
||||
class {
|
||||
get foo() {}
|
||||
},
|
||||
{ foo: identity }
|
||||
)
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('decorateMethodsWith is an alias of decorateClass', function () {
|
||||
assert.strictEqual(decorateMethodsWith, decorateClass)
|
||||
})
|
||||
|
||||
describe('perInstance', () => {
|
||||
it('works', () => {
|
||||
let calls = 0
|
||||
|
||||
const expectedArgs = [Math.random(), Math.random()]
|
||||
const expectedFn = Function.prototype
|
||||
function wrapper(fn, ...args) {
|
||||
assert.strictEqual(fn, expectedFn)
|
||||
assert.deepStrictEqual(args, expectedArgs)
|
||||
const i = ++calls
|
||||
return () => i
|
||||
}
|
||||
|
||||
const wrapped = perInstance(expectedFn, wrapper, ...expectedArgs)
|
||||
|
||||
// decorator is not called before decorated called
|
||||
assert.strictEqual(calls, 0)
|
||||
|
||||
const o1 = {}
|
||||
const o2 = {}
|
||||
|
||||
assert.strictEqual(wrapped.call(o1), 1)
|
||||
|
||||
// the same decorated function is returned for the same instance
|
||||
assert.strictEqual(wrapped.call(o1), 1)
|
||||
|
||||
// a new decorated function is returned for another instance
|
||||
assert.strictEqual(wrapped.call(o2), 2)
|
||||
})
|
||||
})
|
||||
@@ -20,15 +20,11 @@
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"license": "ISC",
|
||||
"version": "2.0.0",
|
||||
"version": "0.1.0",
|
||||
"engines": {
|
||||
"node": ">=8.10"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public",
|
||||
"test": "node--test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"test": "^3.2.1"
|
||||
"postversion": "npm publish --access public"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/disposable):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/disposable
|
||||
```
|
||||
> npm install --save @vates/disposable
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
'use strict'
|
||||
|
||||
const { asyncMap } = require('@xen-orchestra/async-map')
|
||||
const { createLogger } = require('@xen-orchestra/log')
|
||||
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
'use strict'
|
||||
|
||||
const { describe, it } = require('test')
|
||||
const { useFakeTimers, spy, assert } = require('sinon')
|
||||
/* eslint-env jest */
|
||||
|
||||
const { createDebounceResource } = require('./debounceResource')
|
||||
|
||||
const clock = useFakeTimers()
|
||||
jest.useFakeTimers()
|
||||
|
||||
describe('debounceResource()', () => {
|
||||
it('calls the resource disposer after 10 seconds', async () => {
|
||||
const debounceResource = createDebounceResource()
|
||||
const delay = 10e3
|
||||
const dispose = spy()
|
||||
const dispose = jest.fn()
|
||||
|
||||
const resource = await debounceResource(
|
||||
Promise.resolve({
|
||||
@@ -23,10 +20,10 @@ describe('debounceResource()', () => {
|
||||
|
||||
resource.dispose()
|
||||
|
||||
assert.notCalled(dispose)
|
||||
expect(dispose).not.toBeCalled()
|
||||
|
||||
clock.tick(delay)
|
||||
jest.advanceTimersByTime(delay)
|
||||
|
||||
assert.called(dispose)
|
||||
expect(dispose).toBeCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,3 @@
|
||||
'use strict'
|
||||
|
||||
const ensureArray = require('ensure-array')
|
||||
const { MultiKeyMap } = require('@vates/multi-key-map')
|
||||
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
'use strict'
|
||||
|
||||
const { describe, it } = require('test')
|
||||
const { spy, assert } = require('sinon')
|
||||
/* eslint-env jest */
|
||||
|
||||
const { deduped } = require('./deduped')
|
||||
|
||||
describe('deduped()', () => {
|
||||
it('calls the resource function only once', async () => {
|
||||
const value = {}
|
||||
const getResource = spy(async () => ({
|
||||
const getResource = jest.fn(async () => ({
|
||||
value,
|
||||
dispose: Function.prototype,
|
||||
}))
|
||||
@@ -18,13 +15,13 @@ describe('deduped()', () => {
|
||||
const { value: v1 } = await dedupedGetResource()
|
||||
const { value: v2 } = await dedupedGetResource()
|
||||
|
||||
assert.calledOnce(getResource)
|
||||
assert.match(v1, value)
|
||||
assert.match(v2, value)
|
||||
expect(getResource).toHaveBeenCalledTimes(1)
|
||||
expect(v1).toBe(value)
|
||||
expect(v2).toBe(value)
|
||||
})
|
||||
|
||||
it('only disposes the source disposable when its all copies dispose', async () => {
|
||||
const dispose = spy()
|
||||
const dispose = jest.fn()
|
||||
const getResource = async () => ({
|
||||
value: '',
|
||||
dispose,
|
||||
@@ -37,35 +34,35 @@ describe('deduped()', () => {
|
||||
|
||||
d1()
|
||||
|
||||
assert.notCalled(dispose)
|
||||
expect(dispose).not.toHaveBeenCalled()
|
||||
|
||||
d2()
|
||||
|
||||
assert.calledOnce(dispose)
|
||||
expect(dispose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('works with sync factory', () => {
|
||||
const value = {}
|
||||
const dispose = spy()
|
||||
const dispose = jest.fn()
|
||||
const dedupedGetResource = deduped(() => ({ value, dispose }))
|
||||
|
||||
const d1 = dedupedGetResource()
|
||||
assert.match(d1.value, value)
|
||||
expect(d1.value).toBe(value)
|
||||
|
||||
const d2 = dedupedGetResource()
|
||||
assert.match(d2.value, value)
|
||||
expect(d2.value).toBe(value)
|
||||
|
||||
d1.dispose()
|
||||
|
||||
assert.notCalled(dispose)
|
||||
expect(dispose).not.toHaveBeenCalled()
|
||||
|
||||
d2.dispose()
|
||||
|
||||
assert.calledOnce(dispose)
|
||||
expect(dispose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('no race condition on dispose before async acquisition', async () => {
|
||||
const dispose = spy()
|
||||
const dispose = jest.fn()
|
||||
const dedupedGetResource = deduped(async () => ({ value: 42, dispose }))
|
||||
|
||||
const d1 = await dedupedGetResource()
|
||||
@@ -74,6 +71,6 @@ describe('deduped()', () => {
|
||||
|
||||
d1.dispose()
|
||||
|
||||
assert.notCalled(dispose)
|
||||
expect(dispose).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -14,22 +14,17 @@
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"license": "ISC",
|
||||
"version": "0.1.4",
|
||||
"version": "0.1.1",
|
||||
"engines": {
|
||||
"node": ">=8.10"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public",
|
||||
"test": "node--test"
|
||||
"postversion": "npm publish --access public"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vates/multi-key-map": "^0.1.0",
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"@xen-orchestra/log": "^0.6.0",
|
||||
"@xen-orchestra/log": "^0.3.0",
|
||||
"ensure-array": "^1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"sinon": "^15.0.1",
|
||||
"test": "^3.2.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
> This library is compatible with Node's `EventEmitter` and web browsers' `EventTarget` APIs.
|
||||
|
||||
### API
|
||||
|
||||
```js
|
||||
import { EventListenersManager } from '@vates/event-listeners-manager'
|
||||
|
||||
const events = new EventListenersManager(emitter)
|
||||
|
||||
// adding listeners
|
||||
events.add('foo', onFoo).add('bar', onBar).on('baz', onBaz)
|
||||
|
||||
// removing a specific listener
|
||||
events.remove('foo', onFoo)
|
||||
|
||||
// removing all listeners for a specific event
|
||||
events.removeAll('foo')
|
||||
|
||||
// removing all listeners
|
||||
events.removeAll()
|
||||
```
|
||||
|
||||
### Typical use case
|
||||
|
||||
> Removing all listeners when no longer necessary.
|
||||
|
||||
Manually:
|
||||
|
||||
```js
|
||||
const onFoo = () => {}
|
||||
const onBar = () => {}
|
||||
const onBaz = () => {}
|
||||
emitter.on('foo', onFoo).on('bar', onBar).on('baz', onBaz)
|
||||
|
||||
// CODE LOGIC
|
||||
|
||||
emitter.off('foo', onFoo).off('bar', onBar).off('baz', onBaz)
|
||||
```
|
||||
|
||||
With this library:
|
||||
|
||||
```js
|
||||
const events = new EventListenersManager(emitter)
|
||||
|
||||
events.add('foo', () => {})).add('bar', () => {})).add('baz', () => {}))
|
||||
|
||||
// CODE LOGIC
|
||||
|
||||
events.removeAll()
|
||||
```
|
||||
@@ -1 +0,0 @@
|
||||
../../scripts/npmignore
|
||||
@@ -1,81 +0,0 @@
|
||||
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
|
||||
|
||||
# @vates/event-listeners-manager
|
||||
|
||||
[](https://npmjs.org/package/@vates/event-listeners-manager)  [](https://bundlephobia.com/result?p=@vates/event-listeners-manager) [](https://npmjs.org/package/@vates/event-listeners-manager)
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/event-listeners-manager):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/event-listeners-manager
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
> This library is compatible with Node's `EventEmitter` and web browsers' `EventTarget` APIs.
|
||||
|
||||
### API
|
||||
|
||||
```js
|
||||
import { EventListenersManager } from '@vates/event-listeners-manager'
|
||||
|
||||
const events = new EventListenersManager(emitter)
|
||||
|
||||
// adding listeners
|
||||
events.add('foo', onFoo).add('bar', onBar).on('baz', onBaz)
|
||||
|
||||
// removing a specific listener
|
||||
events.remove('foo', onFoo)
|
||||
|
||||
// removing all listeners for a specific event
|
||||
events.removeAll('foo')
|
||||
|
||||
// removing all listeners
|
||||
events.removeAll()
|
||||
```
|
||||
|
||||
### Typical use case
|
||||
|
||||
> Removing all listeners when no longer necessary.
|
||||
|
||||
Manually:
|
||||
|
||||
```js
|
||||
const onFoo = () => {}
|
||||
const onBar = () => {}
|
||||
const onBaz = () => {}
|
||||
emitter.on('foo', onFoo).on('bar', onBar).on('baz', onBaz)
|
||||
|
||||
// CODE LOGIC
|
||||
|
||||
emitter.off('foo', onFoo).off('bar', onBar).off('baz', onBaz)
|
||||
```
|
||||
|
||||
With this library:
|
||||
|
||||
```js
|
||||
const events = new EventListenersManager(emitter)
|
||||
|
||||
events.add('foo', () => {})).add('bar', () => {})).add('baz', () => {}))
|
||||
|
||||
// CODE LOGIC
|
||||
|
||||
events.removeAll()
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are _very_ welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)
|
||||
@@ -1,56 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
exports.EventListenersManager = class EventListenersManager {
|
||||
constructor(emitter) {
|
||||
this._listeners = new Map()
|
||||
|
||||
this._add = (emitter.addListener || emitter.addEventListener).bind(emitter)
|
||||
this._remove = (emitter.removeListener || emitter.removeEventListener).bind(emitter)
|
||||
}
|
||||
|
||||
add(type, listener) {
|
||||
let listeners = this._listeners.get(type)
|
||||
if (listeners === undefined) {
|
||||
listeners = new Set()
|
||||
this._listeners.set(type, listeners)
|
||||
}
|
||||
|
||||
// don't add the same listener multiple times (allowed on Node.js)
|
||||
if (!listeners.has(listener)) {
|
||||
listeners.add(listener)
|
||||
this._add(type, listener)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
remove(type, listener) {
|
||||
const allListeners = this._listeners
|
||||
const listeners = allListeners.get(type)
|
||||
if (listeners !== undefined && listeners.delete(listener)) {
|
||||
this._remove(type, listener)
|
||||
if (listeners.size === 0) {
|
||||
allListeners.delete(type)
|
||||
}
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
removeAll(type) {
|
||||
const allListeners = this._listeners
|
||||
const remove = this._remove
|
||||
const types = type !== undefined ? [type] : allListeners.keys()
|
||||
for (const type of types) {
|
||||
const listeners = allListeners.get(type)
|
||||
if (listeners !== undefined) {
|
||||
allListeners.delete(type)
|
||||
for (const listener of listeners) {
|
||||
remove(type, listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const t = require('tap')
|
||||
const { EventEmitter } = require('events')
|
||||
|
||||
const { EventListenersManager } = require('./')
|
||||
|
||||
const noop = Function.prototype
|
||||
|
||||
// function spy (impl = Function.prototype) {
|
||||
// function spy() {
|
||||
// spy.calls.push([Array.from(arguments), this])
|
||||
// }
|
||||
// spy.calls = []
|
||||
// return spy
|
||||
// }
|
||||
|
||||
function assertListeners(t, event, listeners) {
|
||||
t.strictSame(t.context.ee.listeners(event), listeners)
|
||||
}
|
||||
|
||||
t.beforeEach(function (t) {
|
||||
t.context.ee = new EventEmitter()
|
||||
t.context.em = new EventListenersManager(t.context.ee)
|
||||
})
|
||||
|
||||
t.test('.add adds a listener', function (t) {
|
||||
t.context.em.add('foo', noop)
|
||||
|
||||
assertListeners(t, 'foo', [noop])
|
||||
|
||||
t.end()
|
||||
})
|
||||
|
||||
t.test('.add does not add a duplicate listener', function (t) {
|
||||
t.context.em.add('foo', noop).add('foo', noop)
|
||||
|
||||
assertListeners(t, 'foo', [noop])
|
||||
|
||||
t.end()
|
||||
})
|
||||
|
||||
t.test('.remove removes a listener', function (t) {
|
||||
t.context.em.add('foo', noop).remove('foo', noop)
|
||||
|
||||
assertListeners(t, 'foo', [])
|
||||
|
||||
t.end()
|
||||
})
|
||||
|
||||
t.test('.removeAll removes all listeners of a given type', function (t) {
|
||||
t.context.em.add('foo', noop).add('bar', noop).removeAll('foo')
|
||||
|
||||
assertListeners(t, 'foo', [])
|
||||
assertListeners(t, 'bar', [noop])
|
||||
|
||||
t.end()
|
||||
})
|
||||
|
||||
t.test('.removeAll removes all listeners', function (t) {
|
||||
t.context.em.add('foo', noop).add('bar', noop).removeAll()
|
||||
|
||||
assertListeners(t, 'foo', [])
|
||||
assertListeners(t, 'bar', [])
|
||||
|
||||
t.end()
|
||||
})
|
||||
@@ -1,46 +0,0 @@
|
||||
{
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"private": false,
|
||||
"name": "@vates/event-listeners-manager",
|
||||
"descriptions": "Easy way to clean up event listeners",
|
||||
"keywords": [
|
||||
"add",
|
||||
"addEventListener",
|
||||
"addListener",
|
||||
"browser",
|
||||
"clear",
|
||||
"DOM",
|
||||
"emitter",
|
||||
"event",
|
||||
"EventEmitter",
|
||||
"EventTarget",
|
||||
"management",
|
||||
"manager",
|
||||
"node",
|
||||
"remove",
|
||||
"removeEventListener",
|
||||
"removeListener"
|
||||
],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/event-listeners-manager",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"directory": "@vates/event-listeners-manager",
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Vates SAS",
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"license": "ISC",
|
||||
"version": "1.0.1",
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public",
|
||||
"test": "tap --branches=72"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tap": "^16.2.0"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
../../scripts/npmignore
|
||||
@@ -1,66 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const LRU = require('lru-cache')
|
||||
const Fuse = require('fuse-native')
|
||||
const { VhdSynthetic } = require('vhd-lib')
|
||||
const { Disposable, fromCallback } = require('promise-toolbox')
|
||||
|
||||
// build a s stat object from https://github.com/fuse-friends/fuse-native/blob/master/test/fixtures/stat.js
|
||||
const stat = st => ({
|
||||
mtime: st.mtime || new Date(),
|
||||
atime: st.atime || new Date(),
|
||||
ctime: st.ctime || new Date(),
|
||||
size: st.size !== undefined ? st.size : 0,
|
||||
mode: st.mode === 'dir' ? 16877 : st.mode === 'file' ? 33188 : st.mode === 'link' ? 41453 : st.mode,
|
||||
uid: st.uid !== undefined ? st.uid : process.getuid(),
|
||||
gid: st.gid !== undefined ? st.gid : process.getgid(),
|
||||
})
|
||||
|
||||
exports.mount = Disposable.factory(async function* mount(handler, diskPath, mountDir) {
|
||||
const vhd = yield VhdSynthetic.fromVhdChain(handler, diskPath)
|
||||
|
||||
const cache = new LRU({
|
||||
max: 16, // each cached block is 2MB in size
|
||||
})
|
||||
await vhd.readBlockAllocationTable()
|
||||
const fuse = new Fuse(mountDir, {
|
||||
async readdir(path, cb) {
|
||||
if (path === '/') {
|
||||
return cb(null, ['vhd0'])
|
||||
}
|
||||
cb(Fuse.ENOENT)
|
||||
},
|
||||
async getattr(path, cb) {
|
||||
if (path === '/') {
|
||||
return cb(
|
||||
null,
|
||||
stat({
|
||||
mode: 'dir',
|
||||
size: 4096,
|
||||
})
|
||||
)
|
||||
}
|
||||
if (path === '/vhd0') {
|
||||
return cb(
|
||||
null,
|
||||
stat({
|
||||
mode: 'file',
|
||||
size: vhd.footer.currentSize,
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
cb(Fuse.ENOENT)
|
||||
},
|
||||
read(path, fd, buf, len, pos, cb) {
|
||||
if (path === '/vhd0') {
|
||||
return vhd.readRawData(pos, len, cache, buf).then(cb)
|
||||
}
|
||||
throw new Error(`read file ${path} not exists`)
|
||||
},
|
||||
})
|
||||
return new Disposable(
|
||||
() => fromCallback(() => fuse.unmount()),
|
||||
fromCallback(() => fuse.mount())
|
||||
)
|
||||
})
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"name": "@vates/fuse-vhd",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"private": false,
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/fuse-vhd",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"directory": "@vates/fuse-vhd",
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Vates SAS",
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"fuse-native": "^2.2.6",
|
||||
"lru-cache": "^7.14.0",
|
||||
"promise-toolbox": "^0.21.0",
|
||||
"vhd-lib": "^4.2.1"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public"
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/multi-key-map):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/multi-key-map
|
||||
```
|
||||
> npm install --save @vates/multi-key-map
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
'use strict'
|
||||
|
||||
class Node {
|
||||
constructor(value) {
|
||||
this.children = new Map()
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
'use strict'
|
||||
|
||||
const { describe, it } = require('test')
|
||||
const assert = require('node:assert')
|
||||
/* eslint-env jest */
|
||||
|
||||
const { MultiKeyMap } = require('./')
|
||||
|
||||
@@ -29,9 +26,9 @@ describe('MultiKeyMap', () => {
|
||||
|
||||
keys.forEach((key, i) => {
|
||||
// copy the key to make sure the array itself is not the key
|
||||
assert.strictEqual(map.get(key.slice()), values[i])
|
||||
expect(map.get(key.slice())).toBe(values[i])
|
||||
map.delete(key.slice())
|
||||
assert.strictEqual(map.get(key.slice()), undefined)
|
||||
expect(map.get(key.slice())).toBe(undefined)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -23,10 +23,6 @@
|
||||
"node": ">=8.10"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public",
|
||||
"test": "node--test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"test": "^3.2.1"
|
||||
"postversion": "npm publish --access public"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
### `new NdbClient({address, exportname, secure = true, port = 10809})`
|
||||
|
||||
create a new nbd client
|
||||
|
||||
```js
|
||||
import NbdClient from '@vates/nbd-client'
|
||||
const client = new NbdClient({
|
||||
address: 'MY_NBD_HOST',
|
||||
exportname: 'MY_SECRET_EXPORT',
|
||||
cert: 'Server certificate', // optional, will use encrypted link if provided
|
||||
})
|
||||
|
||||
await client.connect()
|
||||
const block = await client.readBlock(blockIndex, BlockSize)
|
||||
await client.disconnect()
|
||||
```
|
||||
@@ -1 +0,0 @@
|
||||
../../scripts/npmignore
|
||||
@@ -1,47 +0,0 @@
|
||||
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
|
||||
|
||||
# @vates/nbd-client
|
||||
|
||||
[](https://npmjs.org/package/@vates/nbd-client)  [](https://bundlephobia.com/result?p=@vates/nbd-client) [](https://npmjs.org/package/@vates/nbd-client)
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/nbd-client):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/nbd-client
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### `new NdbClient({address, exportname, secure = true, port = 10809})`
|
||||
|
||||
create a new nbd client
|
||||
|
||||
```js
|
||||
import NbdClient from '@vates/nbd-client'
|
||||
const client = new NbdClient({
|
||||
address: 'MY_NBD_HOST',
|
||||
exportname: 'MY_SECRET_EXPORT',
|
||||
cert: 'Server certificate', // optional, will use encrypted link if provided
|
||||
})
|
||||
|
||||
await client.connect()
|
||||
const block = await client.readBlock(blockIndex, BlockSize)
|
||||
await client.disconnect()
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are _very_ welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)
|
||||
@@ -1,42 +0,0 @@
|
||||
'use strict'
|
||||
exports.INIT_PASSWD = Buffer.from('NBDMAGIC') // "NBDMAGIC" ensure we're connected to a nbd server
|
||||
exports.OPTS_MAGIC = Buffer.from('IHAVEOPT') // "IHAVEOPT" start an option block
|
||||
exports.NBD_OPT_REPLY_MAGIC = 1100100111001001n // magic received during negociation
|
||||
exports.NBD_OPT_EXPORT_NAME = 1
|
||||
exports.NBD_OPT_ABORT = 2
|
||||
exports.NBD_OPT_LIST = 3
|
||||
exports.NBD_OPT_STARTTLS = 5
|
||||
exports.NBD_OPT_INFO = 6
|
||||
exports.NBD_OPT_GO = 7
|
||||
|
||||
exports.NBD_FLAG_HAS_FLAGS = 1 << 0
|
||||
exports.NBD_FLAG_READ_ONLY = 1 << 1
|
||||
exports.NBD_FLAG_SEND_FLUSH = 1 << 2
|
||||
exports.NBD_FLAG_SEND_FUA = 1 << 3
|
||||
exports.NBD_FLAG_ROTATIONAL = 1 << 4
|
||||
exports.NBD_FLAG_SEND_TRIM = 1 << 5
|
||||
|
||||
exports.NBD_FLAG_FIXED_NEWSTYLE = 1 << 0
|
||||
|
||||
exports.NBD_CMD_FLAG_FUA = 1 << 0
|
||||
exports.NBD_CMD_FLAG_NO_HOLE = 1 << 1
|
||||
exports.NBD_CMD_FLAG_DF = 1 << 2
|
||||
exports.NBD_CMD_FLAG_REQ_ONE = 1 << 3
|
||||
exports.NBD_CMD_FLAG_FAST_ZERO = 1 << 4
|
||||
|
||||
exports.NBD_CMD_READ = 0
|
||||
exports.NBD_CMD_WRITE = 1
|
||||
exports.NBD_CMD_DISC = 2
|
||||
exports.NBD_CMD_FLUSH = 3
|
||||
exports.NBD_CMD_TRIM = 4
|
||||
exports.NBD_CMD_CACHE = 5
|
||||
exports.NBD_CMD_WRITE_ZEROES = 6
|
||||
exports.NBD_CMD_BLOCK_STATUS = 7
|
||||
exports.NBD_CMD_RESIZE = 8
|
||||
|
||||
exports.NBD_REQUEST_MAGIC = 0x25609513 // magic number to create a new NBD request to send to the server
|
||||
exports.NBD_REPLY_MAGIC = 0x67446698 // magic number received from the server when reading response to a nbd request
|
||||
exports.NBD_REPLY_ACK = 1
|
||||
|
||||
exports.NBD_DEFAULT_PORT = 10809
|
||||
exports.NBD_DEFAULT_BLOCK_SIZE = 64 * 1024
|
||||
@@ -1,243 +0,0 @@
|
||||
'use strict'
|
||||
const assert = require('node:assert')
|
||||
const { Socket } = require('node:net')
|
||||
const { connect } = require('node:tls')
|
||||
const {
|
||||
INIT_PASSWD,
|
||||
NBD_CMD_READ,
|
||||
NBD_DEFAULT_BLOCK_SIZE,
|
||||
NBD_DEFAULT_PORT,
|
||||
NBD_FLAG_FIXED_NEWSTYLE,
|
||||
NBD_FLAG_HAS_FLAGS,
|
||||
NBD_OPT_EXPORT_NAME,
|
||||
NBD_OPT_REPLY_MAGIC,
|
||||
NBD_OPT_STARTTLS,
|
||||
NBD_REPLY_ACK,
|
||||
NBD_REPLY_MAGIC,
|
||||
NBD_REQUEST_MAGIC,
|
||||
OPTS_MAGIC,
|
||||
} = require('./constants.js')
|
||||
const { fromCallback } = require('promise-toolbox')
|
||||
const { readChunkStrict } = require('@vates/read-chunk')
|
||||
|
||||
// documentation is here : https://github.com/NetworkBlockDevice/nbd/blob/master/doc/proto.md
|
||||
|
||||
module.exports = class NbdClient {
|
||||
#serverAddress
|
||||
#serverCert
|
||||
#serverPort
|
||||
#serverSocket
|
||||
|
||||
#exportName
|
||||
#exportSize
|
||||
|
||||
// AFAIK, there is no guaranty the server answers in the same order as the queries
|
||||
// so we handle a backlog of command waiting for response and handle concurrency manually
|
||||
|
||||
#waitingForResponse // there is already a listenner waiting for a response
|
||||
#nextCommandQueryId = BigInt(0)
|
||||
#commandQueryBacklog // map of command waiting for an response queryId => { size/*in byte*/, resolve, reject}
|
||||
|
||||
constructor({ address, port = NBD_DEFAULT_PORT, exportname, cert }) {
|
||||
this.#serverAddress = address
|
||||
this.#serverPort = port
|
||||
this.#exportName = exportname
|
||||
this.#serverCert = cert
|
||||
}
|
||||
|
||||
get exportSize() {
|
||||
return this.#exportSize
|
||||
}
|
||||
|
||||
async #tlsConnect() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.#serverSocket = connect({
|
||||
socket: this.#serverSocket,
|
||||
rejectUnauthorized: false,
|
||||
cert: this.#serverCert,
|
||||
})
|
||||
this.#serverSocket.once('error', reject)
|
||||
this.#serverSocket.once('secureConnect', () => {
|
||||
this.#serverSocket.removeListener('error', reject)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// mandatory , at least to start the handshake
|
||||
async #unsecureConnect() {
|
||||
this.#serverSocket = new Socket()
|
||||
return new Promise((resolve, reject) => {
|
||||
this.#serverSocket.connect(this.#serverPort, this.#serverAddress)
|
||||
this.#serverSocket.once('error', reject)
|
||||
this.#serverSocket.once('connect', () => {
|
||||
this.#serverSocket.removeListener('error', reject)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async connect() {
|
||||
// first we connect to the serve without tls, and then we upgrade the connection
|
||||
// to tls during the handshake
|
||||
await this.#unsecureConnect()
|
||||
await this.#handshake()
|
||||
|
||||
// reset internal state if we reconnected a nbd client
|
||||
this.#commandQueryBacklog = new Map()
|
||||
this.#waitingForResponse = false
|
||||
}
|
||||
|
||||
async disconnect() {
|
||||
await this.#serverSocket.destroy()
|
||||
}
|
||||
|
||||
// we can use individual read/write from the socket here since there is no concurrency
|
||||
async #sendOption(option, buffer = Buffer.alloc(0)) {
|
||||
await this.#write(OPTS_MAGIC)
|
||||
await this.#writeInt32(option)
|
||||
await this.#writeInt32(buffer.length)
|
||||
await this.#write(buffer)
|
||||
assert.strictEqual(await this.#readInt64(), NBD_OPT_REPLY_MAGIC) // magic number everywhere
|
||||
assert.strictEqual(await this.#readInt32(), option) // the option passed
|
||||
assert.strictEqual(await this.#readInt32(), NBD_REPLY_ACK) // ACK
|
||||
const length = await this.#readInt32()
|
||||
assert.strictEqual(length, 0) // length
|
||||
}
|
||||
|
||||
// we can use individual read/write from the socket here since there is only one handshake at once, no concurrency
|
||||
async #handshake() {
|
||||
assert((await this.#read(8)).equals(INIT_PASSWD))
|
||||
assert((await this.#read(8)).equals(OPTS_MAGIC))
|
||||
const flagsBuffer = await this.#read(2)
|
||||
const flags = flagsBuffer.readInt16BE(0)
|
||||
assert.strictEqual(flags & NBD_FLAG_FIXED_NEWSTYLE, NBD_FLAG_FIXED_NEWSTYLE) // only FIXED_NEWSTYLE one is supported from the server options
|
||||
await this.#writeInt32(NBD_FLAG_FIXED_NEWSTYLE) // client also support NBD_FLAG_C_FIXED_NEWSTYLE
|
||||
|
||||
if (this.#serverCert !== undefined) {
|
||||
// upgrade socket to TLS if needed
|
||||
await this.#sendOption(NBD_OPT_STARTTLS)
|
||||
await this.#tlsConnect()
|
||||
}
|
||||
|
||||
// send export name we want to access.
|
||||
// it's implictly closing the negociation phase.
|
||||
await this.#write(OPTS_MAGIC)
|
||||
await this.#writeInt32(NBD_OPT_EXPORT_NAME)
|
||||
const exportNameBuffer = Buffer.from(this.#exportName)
|
||||
await this.#writeInt32(exportNameBuffer.length)
|
||||
await this.#write(exportNameBuffer)
|
||||
|
||||
// 8 (export size ) + 2 (flags) + 124 zero = 134
|
||||
// must read all to ensure nothing stays in the buffer
|
||||
const answer = await this.#read(134)
|
||||
this.#exportSize = answer.readBigUInt64BE(0)
|
||||
const transmissionFlags = answer.readInt16BE(8)
|
||||
assert.strictEqual(transmissionFlags & NBD_FLAG_HAS_FLAGS, NBD_FLAG_HAS_FLAGS, 'NBD_FLAG_HAS_FLAGS') // must always be 1 by the norm
|
||||
|
||||
// note : xapi server always send NBD_FLAG_READ_ONLY (3) as a flag
|
||||
}
|
||||
|
||||
#read(length) {
|
||||
return readChunkStrict(this.#serverSocket, length)
|
||||
}
|
||||
|
||||
#write(buffer) {
|
||||
return fromCallback.call(this.#serverSocket, 'write', buffer)
|
||||
}
|
||||
|
||||
async #readInt32() {
|
||||
const buffer = await this.#read(4)
|
||||
return buffer.readInt32BE(0)
|
||||
}
|
||||
|
||||
async #readInt64() {
|
||||
const buffer = await this.#read(8)
|
||||
return buffer.readBigUInt64BE(0)
|
||||
}
|
||||
|
||||
#writeInt32(int) {
|
||||
const buffer = Buffer.alloc(4)
|
||||
buffer.writeInt32BE(int)
|
||||
return this.#write(buffer)
|
||||
}
|
||||
|
||||
// when one read fail ,stop everything
|
||||
async #rejectAll(error) {
|
||||
this.#commandQueryBacklog.forEach(({ reject }) => {
|
||||
reject(error)
|
||||
})
|
||||
await this.disconnect()
|
||||
}
|
||||
|
||||
async #readBlockResponse() {
|
||||
// ensure at most one read occur in parallel
|
||||
if (this.#waitingForResponse) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
this.#waitingForResponse = true
|
||||
const magic = await this.#readInt32()
|
||||
|
||||
if (magic !== NBD_REPLY_MAGIC) {
|
||||
throw new Error(`magic number for block answer is wrong : ${magic} ${NBD_REPLY_MAGIC}`)
|
||||
}
|
||||
|
||||
const error = await this.#readInt32()
|
||||
if (error !== 0) {
|
||||
// @todo use error code from constants.mjs
|
||||
throw new Error(`GOT ERROR CODE : ${error}`)
|
||||
}
|
||||
|
||||
const blockQueryId = await this.#readInt64()
|
||||
const query = this.#commandQueryBacklog.get(blockQueryId)
|
||||
if (!query) {
|
||||
throw new Error(` no query associated with id ${blockQueryId}`)
|
||||
}
|
||||
this.#commandQueryBacklog.delete(blockQueryId)
|
||||
const data = await this.#read(query.size)
|
||||
query.resolve(data)
|
||||
this.#waitingForResponse = false
|
||||
if (this.#commandQueryBacklog.size > 0) {
|
||||
await this.#readBlockResponse()
|
||||
}
|
||||
} catch (error) {
|
||||
// reject all the promises
|
||||
// we don't need to call readBlockResponse on failure
|
||||
// since we will empty the backlog
|
||||
await this.#rejectAll(error)
|
||||
}
|
||||
}
|
||||
|
||||
async readBlock(index, size = NBD_DEFAULT_BLOCK_SIZE) {
|
||||
const queryId = this.#nextCommandQueryId
|
||||
this.#nextCommandQueryId++
|
||||
|
||||
// create and send command at once to ensure there is no concurrency issue
|
||||
const buffer = Buffer.alloc(28)
|
||||
buffer.writeInt32BE(NBD_REQUEST_MAGIC, 0) // it is a nbd request
|
||||
buffer.writeInt16BE(0, 4) // no command flags for a simple block read
|
||||
buffer.writeInt16BE(NBD_CMD_READ, 6) // we want to read a data block
|
||||
buffer.writeBigUInt64BE(queryId, 8)
|
||||
// byte offset in the raw disk
|
||||
buffer.writeBigUInt64BE(BigInt(index) * BigInt(size), 16)
|
||||
buffer.writeInt32BE(size, 24)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// this will handle one block response, but it can be another block
|
||||
// since server does not guaranty to handle query in order
|
||||
this.#commandQueryBacklog.set(queryId, {
|
||||
size,
|
||||
resolve,
|
||||
reject,
|
||||
})
|
||||
// really send the command to the server
|
||||
this.#write(buffer).catch(reject)
|
||||
|
||||
// #readBlockResponse never throws directly
|
||||
// but if it fails it will reject all the promises in the backlog
|
||||
this.#readBlockResponse()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
'use strict'
|
||||
const NbdClient = require('./index.js')
|
||||
const { spawn } = require('node:child_process')
|
||||
const fs = require('node:fs/promises')
|
||||
const { test } = require('tap')
|
||||
const tmp = require('tmp')
|
||||
const { pFromCallback } = require('promise-toolbox')
|
||||
const { asyncEach } = require('@vates/async-each')
|
||||
|
||||
const FILE_SIZE = 2 * 1024 * 1024
|
||||
|
||||
async function createTempFile(size) {
|
||||
const tmpPath = await pFromCallback(cb => tmp.file(cb))
|
||||
const data = Buffer.alloc(size, 0)
|
||||
for (let i = 0; i < size; i += 4) {
|
||||
data.writeUInt32BE(i, i)
|
||||
}
|
||||
await fs.writeFile(tmpPath, data)
|
||||
|
||||
return tmpPath
|
||||
}
|
||||
|
||||
test('it works with unsecured network', async tap => {
|
||||
const path = await createTempFile(FILE_SIZE)
|
||||
|
||||
const nbdServer = spawn(
|
||||
'nbdkit',
|
||||
[
|
||||
'file',
|
||||
path,
|
||||
'--newstyle', //
|
||||
'--exit-with-parent',
|
||||
'--read-only',
|
||||
'--export-name=MY_SECRET_EXPORT',
|
||||
],
|
||||
{
|
||||
stdio: ['inherit', 'inherit', 'inherit'],
|
||||
}
|
||||
)
|
||||
|
||||
const client = new NbdClient({
|
||||
address: 'localhost',
|
||||
exportname: 'MY_SECRET_EXPORT',
|
||||
secure: false,
|
||||
})
|
||||
|
||||
await client.connect()
|
||||
tap.equal(client.exportSize, BigInt(FILE_SIZE))
|
||||
const CHUNK_SIZE = 128 * 1024 // non default size
|
||||
const indexes = []
|
||||
for (let i = 0; i < FILE_SIZE / CHUNK_SIZE; i++) {
|
||||
indexes.push(i)
|
||||
}
|
||||
// read mutiple blocks in parallel
|
||||
await asyncEach(
|
||||
indexes,
|
||||
async i => {
|
||||
const block = await client.readBlock(i, CHUNK_SIZE)
|
||||
let blockOk = true
|
||||
let firstFail
|
||||
for (let j = 0; j < CHUNK_SIZE; j += 4) {
|
||||
const wanted = i * CHUNK_SIZE + j
|
||||
const found = block.readUInt32BE(j)
|
||||
blockOk = blockOk && found === wanted
|
||||
if (!blockOk && firstFail === undefined) {
|
||||
firstFail = j
|
||||
}
|
||||
}
|
||||
tap.ok(blockOk, `check block ${i} content`)
|
||||
},
|
||||
{ concurrency: 8 }
|
||||
)
|
||||
await client.disconnect()
|
||||
nbdServer.kill()
|
||||
await fs.unlink(path)
|
||||
})
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "@vates/nbd-client",
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/nbd-client",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"directory": "@vates/nbd-client",
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Vates SAS",
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"license": "ISC",
|
||||
"version": "1.0.0",
|
||||
"engines": {
|
||||
"node": ">=14.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vates/async-each": "^1.0.0",
|
||||
"@vates/read-chunk": "^1.0.1",
|
||||
"@xen-orchestra/async-map": "^0.1.2",
|
||||
"promise-toolbox": "^0.21.0",
|
||||
"xen-api": "^1.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tap": "^16.3.0",
|
||||
"tmp": "^0.2.1"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public",
|
||||
"test-integration": "tap *.spec.js"
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
### Usual workflow
|
||||
|
||||
> This section presents how this library should be used to implement a classic two factor authentification.
|
||||
|
||||
#### Setup
|
||||
|
||||
```js
|
||||
import { generateSecret, generateTotp } from '@vates/otp'
|
||||
import QrCode from 'qrcode'
|
||||
|
||||
// Generates a secret that will be shared by both the service and the user:
|
||||
const secret = generateSecret()
|
||||
|
||||
// Stores the secret in the service:
|
||||
await currentUser.saveOtpSecret(secret)
|
||||
|
||||
// Generates an URI to present to the user
|
||||
const uri = generateTotpUri({ secret })
|
||||
|
||||
// Generates the QR code from the URI to make it easily importable in Authy or Google Authenticator
|
||||
const qr = await QrCode.toDataURL(uri)
|
||||
```
|
||||
|
||||
#### Authentication
|
||||
|
||||
```js
|
||||
import { verifyTotp } from '@vates/otp'
|
||||
|
||||
// Verifies a `token` entered by the user against a `secret` generated during setup.
|
||||
if (await verifyTotp(token, { secret })) {
|
||||
console.log('authenticated!')
|
||||
}
|
||||
```
|
||||
|
||||
### API
|
||||
|
||||
#### Secret
|
||||
|
||||
```js
|
||||
import { generateSecret } from '@vates/otp'
|
||||
|
||||
const secret = generateSecret()
|
||||
// 'OJOKA65RY5FQQ2RYWVKD5Y3YG5CSHGYH'
|
||||
```
|
||||
|
||||
#### HOTP
|
||||
|
||||
> This is likely not what you want to use, see TOTP below instead.
|
||||
|
||||
```js
|
||||
import { generateHotp, generateHotpUri, verifyHotp } from '@vates/otp'
|
||||
|
||||
// a sequence number, see HOTP specification
|
||||
const counter = 0
|
||||
|
||||
// generate a token
|
||||
//
|
||||
// optional params:
|
||||
// - digits
|
||||
const token = await generateHotp({ counter, secret })
|
||||
// '239988'
|
||||
|
||||
// verify a token
|
||||
//
|
||||
// optional params:
|
||||
// - digits
|
||||
const isValid = await verifyHotp(token, { counter, secret })
|
||||
// true
|
||||
|
||||
// generate a URI than can be displayed as a QR code to be used with Authy or Google Authenticator
|
||||
//
|
||||
// optional params:
|
||||
// - digits
|
||||
const uri = generateHotpUri({ counter, label: 'account name', issuer: 'my app', secret })
|
||||
// 'otpauth://hotp/my%20app:account%20name?counter=0&issuer=my%20app&secret=OJOKA65RY5FQQ2RYWVKD5Y3YG5CSHGYH'
|
||||
```
|
||||
|
||||
Optional params and their default values:
|
||||
|
||||
- `digits = 6`: length of the token, avoid using it because not compatible with Google Authenticator
|
||||
|
||||
#### TOTP
|
||||
|
||||
```js
|
||||
import { generateTotp, generateTotpUri, verifyTotp } from '@vates/otp'
|
||||
|
||||
// generate a token
|
||||
//
|
||||
// optional params:
|
||||
// - digits
|
||||
// - period
|
||||
// - timestamp
|
||||
const token = await generateTotp({ secret })
|
||||
// '632869'
|
||||
|
||||
// verify a token
|
||||
//
|
||||
// optional params:
|
||||
// - digits
|
||||
// - period
|
||||
// - timestamp
|
||||
// - window
|
||||
const isValid = await verifyTotp(token, { secret })
|
||||
// true
|
||||
|
||||
// generate a URI than can be displayed as a QR code to be used with Authy or Google Authenticator
|
||||
//
|
||||
// optional params:
|
||||
// - digits
|
||||
// - period
|
||||
const uri = generateTotpUri({ label: 'account name', issuer: 'my app', secret })
|
||||
// 'otpauth://totp/my%20app:account%20name?issuer=my%20app&secret=OJOKA65RY5FQQ2RYWVKD5Y3YG5CSHGYH'
|
||||
```
|
||||
|
||||
Optional params and their default values:
|
||||
|
||||
- `digits = 6`: length of the token, avoid using it because not compatible with Google Authenticator
|
||||
- `period = 30`: number of seconds a token is valid
|
||||
- `timestamp = Date.now() / 1e3`: Unix timestamp, in seconds, when this token will be valid, default to now
|
||||
- `window = 1`: number of periods before and after `timestamp` for which the token is considered valid
|
||||
|
||||
#### Verification from URI
|
||||
|
||||
```js
|
||||
import { verifyFromUri } from '@vates/otp'
|
||||
|
||||
// Verify the token using all the information contained in the URI
|
||||
const isValid = await verifyFromUri(token, uri)
|
||||
// true
|
||||
```
|
||||
@@ -1 +0,0 @@
|
||||
../../scripts/npmignore
|
||||
@@ -1,163 +0,0 @@
|
||||
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
|
||||
|
||||
# @vates/otp
|
||||
|
||||
[](https://npmjs.org/package/@vates/otp)  [](https://bundlephobia.com/result?p=@vates/otp) [](https://npmjs.org/package/@vates/otp)
|
||||
|
||||
> Minimal HTOP/TOTP implementation
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/otp):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/otp
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Usual workflow
|
||||
|
||||
> This section presents how this library should be used to implement a classic two factor authentification.
|
||||
|
||||
#### Setup
|
||||
|
||||
```js
|
||||
import { generateSecret, generateTotp } from '@vates/otp'
|
||||
import QrCode from 'qrcode'
|
||||
|
||||
// Generates a secret that will be shared by both the service and the user:
|
||||
const secret = generateSecret()
|
||||
|
||||
// Stores the secret in the service:
|
||||
await currentUser.saveOtpSecret(secret)
|
||||
|
||||
// Generates an URI to present to the user
|
||||
const uri = generateTotpUri({ secret })
|
||||
|
||||
// Generates the QR code from the URI to make it easily importable in Authy or Google Authenticator
|
||||
const qr = await QrCode.toDataURL(uri)
|
||||
```
|
||||
|
||||
#### Authentication
|
||||
|
||||
```js
|
||||
import { verifyTotp } from '@vates/otp'
|
||||
|
||||
// Verifies a `token` entered by the user against a `secret` generated during setup.
|
||||
if (await verifyTotp(token, { secret })) {
|
||||
console.log('authenticated!')
|
||||
}
|
||||
```
|
||||
|
||||
### API
|
||||
|
||||
#### Secret
|
||||
|
||||
```js
|
||||
import { generateSecret } from '@vates/otp'
|
||||
|
||||
const secret = generateSecret()
|
||||
// 'OJOKA65RY5FQQ2RYWVKD5Y3YG5CSHGYH'
|
||||
```
|
||||
|
||||
#### HOTP
|
||||
|
||||
> This is likely not what you want to use, see TOTP below instead.
|
||||
|
||||
```js
|
||||
import { generateHotp, generateHotpUri, verifyHotp } from '@vates/otp'
|
||||
|
||||
// a sequence number, see HOTP specification
|
||||
const counter = 0
|
||||
|
||||
// generate a token
|
||||
//
|
||||
// optional params:
|
||||
// - digits
|
||||
const token = await generateHotp({ counter, secret })
|
||||
// '239988'
|
||||
|
||||
// verify a token
|
||||
//
|
||||
// optional params:
|
||||
// - digits
|
||||
const isValid = await verifyHotp(token, { counter, secret })
|
||||
// true
|
||||
|
||||
// generate a URI than can be displayed as a QR code to be used with Authy or Google Authenticator
|
||||
//
|
||||
// optional params:
|
||||
// - digits
|
||||
const uri = generateHotpUri({ counter, label: 'account name', issuer: 'my app', secret })
|
||||
// 'otpauth://hotp/my%20app:account%20name?counter=0&issuer=my%20app&secret=OJOKA65RY5FQQ2RYWVKD5Y3YG5CSHGYH'
|
||||
```
|
||||
|
||||
Optional params and their default values:
|
||||
|
||||
- `digits = 6`: length of the token, avoid using it because not compatible with Google Authenticator
|
||||
|
||||
#### TOTP
|
||||
|
||||
```js
|
||||
import { generateTotp, generateTotpUri, verifyTotp } from '@vates/otp'
|
||||
|
||||
// generate a token
|
||||
//
|
||||
// optional params:
|
||||
// - digits
|
||||
// - period
|
||||
// - timestamp
|
||||
const token = await generateTotp({ secret })
|
||||
// '632869'
|
||||
|
||||
// verify a token
|
||||
//
|
||||
// optional params:
|
||||
// - digits
|
||||
// - period
|
||||
// - timestamp
|
||||
// - window
|
||||
const isValid = await verifyTotp(token, { secret })
|
||||
// true
|
||||
|
||||
// generate a URI than can be displayed as a QR code to be used with Authy or Google Authenticator
|
||||
//
|
||||
// optional params:
|
||||
// - digits
|
||||
// - period
|
||||
const uri = generateTotpUri({ label: 'account name', issuer: 'my app', secret })
|
||||
// 'otpauth://totp/my%20app:account%20name?issuer=my%20app&secret=OJOKA65RY5FQQ2RYWVKD5Y3YG5CSHGYH'
|
||||
```
|
||||
|
||||
Optional params and their default values:
|
||||
|
||||
- `digits = 6`: length of the token, avoid using it because not compatible with Google Authenticator
|
||||
- `period = 30`: number of seconds a token is valid
|
||||
- `timestamp = Date.now() / 1e3`: Unix timestamp, in seconds, when this token will be valid, default to now
|
||||
- `window = 1`: number of periods before and after `timestamp` for which the token is considered valid
|
||||
|
||||
#### Verification from URI
|
||||
|
||||
```js
|
||||
import { verifyFromUri } from '@vates/otp'
|
||||
|
||||
// Verify the token using all the information contained in the URI
|
||||
const isValid = await verifyFromUri(token, uri)
|
||||
// true
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are _very_ welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)
|
||||
@@ -1,111 +0,0 @@
|
||||
import { base32 } from 'rfc4648'
|
||||
import { webcrypto } from 'node:crypto'
|
||||
|
||||
const { subtle } = webcrypto
|
||||
|
||||
function assert(name, value) {
|
||||
if (!value) {
|
||||
throw new TypeError('invalid value for param ' + name)
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/google/google-authenticator/wiki/Key-Uri-Format
|
||||
function generateUri(protocol, label, params) {
|
||||
assert('label', typeof label === 'string')
|
||||
assert('secret', typeof params.secret === 'string')
|
||||
|
||||
let path = encodeURIComponent(label)
|
||||
|
||||
const { issuer } = params
|
||||
if (issuer !== undefined) {
|
||||
path = encodeURIComponent(issuer) + ':' + path
|
||||
}
|
||||
|
||||
const query = Object.entries(params)
|
||||
.filter(_ => _[1] !== undefined)
|
||||
.map(([key, value]) => key + '=' + encodeURIComponent(value))
|
||||
.join('&')
|
||||
|
||||
return `otpauth://${protocol}/${path}?${query}`
|
||||
}
|
||||
|
||||
export function generateSecret() {
|
||||
// https://www.rfc-editor.org/rfc/rfc4226 recommends 160 bits (i.e. 20 bytes)
|
||||
const data = new Uint8Array(20)
|
||||
webcrypto.getRandomValues(data)
|
||||
return base32.stringify(data, { pad: false })
|
||||
}
|
||||
|
||||
const DIGITS = 6
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc4226
|
||||
export async function generateHotp({ counter, digits = DIGITS, secret }) {
|
||||
const data = new Uint8Array(8)
|
||||
new DataView(data.buffer).setBigInt64(0, BigInt(counter), false)
|
||||
|
||||
const key = await subtle.importKey(
|
||||
'raw',
|
||||
base32.parse(secret, { loose: true }),
|
||||
{ name: 'HMAC', hash: 'SHA-1' },
|
||||
false,
|
||||
['sign', 'verify']
|
||||
)
|
||||
const digest = new DataView(await subtle.sign('HMAC', key, data))
|
||||
|
||||
const offset = digest.getUint8(digest.byteLength - 1) & 0xf
|
||||
const p = digest.getUint32(offset) & 0x7f_ff_ff_ff
|
||||
|
||||
return String(p % Math.pow(10, digits)).padStart(digits, '0')
|
||||
}
|
||||
|
||||
export function generateHotpUri({ counter, digits, issuer, label, secret }) {
|
||||
assert('counter', typeof counter === 'number')
|
||||
return generateUri('hotp', label, { counter, digits, issuer, secret })
|
||||
}
|
||||
|
||||
export async function verifyHotp(token, opts) {
|
||||
return token === (await generateHotp(opts))
|
||||
}
|
||||
|
||||
function totpCounter(period = 30, timestamp = Math.floor(Date.now() / 1e3)) {
|
||||
return Math.floor(timestamp / period)
|
||||
}
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc6238.html
|
||||
export async function generateTotp({ period, timestamp, ...opts }) {
|
||||
opts.counter = totpCounter(period, timestamp)
|
||||
return await generateHotp(opts)
|
||||
}
|
||||
|
||||
export function generateTotpUri({ digits, issuer, label, period, secret }) {
|
||||
return generateUri('totp', label, { digits, issuer, period, secret })
|
||||
}
|
||||
|
||||
export async function verifyTotp(token, { period, timestamp, window = 1, ...opts }) {
|
||||
const counter = totpCounter(period, timestamp)
|
||||
const end = counter + window
|
||||
opts.counter = counter - window
|
||||
while (opts.counter <= end) {
|
||||
if (token === (await generateHotp(opts))) {
|
||||
return true
|
||||
}
|
||||
opts.counter += 1
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export async function verifyFromUri(token, uri) {
|
||||
const url = new URL(uri)
|
||||
assert('protocol', url.protocol === 'otpauth:')
|
||||
|
||||
const { host } = url
|
||||
const opts = Object.fromEntries(url.searchParams.entries())
|
||||
if (host === 'hotp') {
|
||||
return await verifyHotp(token, opts)
|
||||
}
|
||||
if (host === 'totp') {
|
||||
return await verifyTotp(token, opts)
|
||||
}
|
||||
|
||||
assert('host', false)
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
import { strict as assert } from 'node:assert'
|
||||
import { describe, it } from 'tap/mocha'
|
||||
|
||||
import {
|
||||
generateHotp,
|
||||
generateHotpUri,
|
||||
generateSecret,
|
||||
generateTotp,
|
||||
generateTotpUri,
|
||||
verifyHotp,
|
||||
verifyTotp,
|
||||
} from './index.mjs'
|
||||
|
||||
describe('generateSecret', function () {
|
||||
it('generates a string of 32 chars', async function () {
|
||||
const secret = generateSecret()
|
||||
assert.equal(typeof secret, 'string')
|
||||
assert.equal(secret.length, 32)
|
||||
})
|
||||
|
||||
it('generates a different secret at each call', async function () {
|
||||
assert.notEqual(generateSecret(), generateSecret())
|
||||
})
|
||||
})
|
||||
|
||||
describe('HOTP', function () {
|
||||
it('generate and verify valid tokens', async function () {
|
||||
for (const [token, opts] of Object.entries({
|
||||
382752: {
|
||||
counter: -3088,
|
||||
secret: 'PJYFSZ3JNVXVQMZXOB2EQYJSKB2HE6TB',
|
||||
},
|
||||
163376: {
|
||||
counter: 30598,
|
||||
secret: 'GBUDQZ3UKZZGIMRLNVYXA33GMFMEGQKN',
|
||||
},
|
||||
})) {
|
||||
assert.equal(await generateHotp(opts), token)
|
||||
assert(await verifyHotp(token, opts))
|
||||
}
|
||||
})
|
||||
|
||||
describe('generateHotpUri', function () {
|
||||
const opts = {
|
||||
counter: 59732,
|
||||
label: 'the label',
|
||||
secret: 'OGK45BBZAIGNGELHZPXYKN4GUVWWO6YX',
|
||||
}
|
||||
|
||||
Object.entries({
|
||||
'without optional params': [
|
||||
opts,
|
||||
'otpauth://hotp/the%20label?counter=59732&secret=OGK45BBZAIGNGELHZPXYKN4GUVWWO6YX',
|
||||
],
|
||||
'with issuer': [
|
||||
{ ...opts, issuer: 'the issuer' },
|
||||
'otpauth://hotp/the%20issuer:the%20label?counter=59732&issuer=the%20issuer&secret=OGK45BBZAIGNGELHZPXYKN4GUVWWO6YX',
|
||||
],
|
||||
'with digits': [
|
||||
{ ...opts, digits: 7 },
|
||||
'otpauth://hotp/the%20label?counter=59732&digits=7&secret=OGK45BBZAIGNGELHZPXYKN4GUVWWO6YX',
|
||||
],
|
||||
}).forEach(([title, [opts, uri]]) => {
|
||||
it(title, async function () {
|
||||
assert.strictEqual(generateHotpUri(opts), uri)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('TOTP', function () {
|
||||
Object.entries({
|
||||
'033702': {
|
||||
secret: 'PJYFSZ3JNVXVQMZXOB2EQYJSKB2HE6TB',
|
||||
timestamp: 1665416296,
|
||||
period: 30,
|
||||
},
|
||||
107250: {
|
||||
secret: 'GBUDQZ3UKZZGIMRLNVYXA33GMFMEGQKN',
|
||||
timestamp: 1665416674,
|
||||
period: 60,
|
||||
},
|
||||
}).forEach(([token, opts]) => {
|
||||
it('works', async function () {
|
||||
assert.equal(await generateTotp(opts), token)
|
||||
assert(await verifyTotp(token, opts))
|
||||
})
|
||||
})
|
||||
|
||||
describe('generateHotpUri', function () {
|
||||
const opts = {
|
||||
label: 'the label',
|
||||
secret: 'OGK45BBZAIGNGELHZPXYKN4GUVWWO6YX',
|
||||
}
|
||||
|
||||
Object.entries({
|
||||
'without optional params': [opts, 'otpauth://totp/the%20label?secret=OGK45BBZAIGNGELHZPXYKN4GUVWWO6YX'],
|
||||
'with issuer': [
|
||||
{ ...opts, issuer: 'the issuer' },
|
||||
'otpauth://totp/the%20issuer:the%20label?issuer=the%20issuer&secret=OGK45BBZAIGNGELHZPXYKN4GUVWWO6YX',
|
||||
],
|
||||
'with digits': [
|
||||
{ ...opts, digits: 7 },
|
||||
'otpauth://totp/the%20label?digits=7&secret=OGK45BBZAIGNGELHZPXYKN4GUVWWO6YX',
|
||||
],
|
||||
}).forEach(([title, [opts, uri]]) => {
|
||||
it(title, async function () {
|
||||
assert.strictEqual(generateTotpUri(opts), uri)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "@vates/otp",
|
||||
"description": "Minimal HTOP/TOTP implementation",
|
||||
"keywords": [
|
||||
"2fa",
|
||||
"authenticator",
|
||||
"hotp",
|
||||
"otp",
|
||||
"totp"
|
||||
],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/otp",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"main": "index.mjs",
|
||||
"repository": {
|
||||
"directory": "@vates/otp",
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Vates SAS",
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"license": "ISC",
|
||||
"version": "1.0.0",
|
||||
"engines": {
|
||||
"node": ">=15"
|
||||
},
|
||||
"dependencies": {
|
||||
"rfc4648": "^1.5.2"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public",
|
||||
"test": "tap"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tap": "^16.3.0"
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/parse-duration):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/parse-duration
|
||||
```
|
||||
> npm install --save @vates/parse-duration
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
'use strict'
|
||||
|
||||
const ms = require('ms')
|
||||
|
||||
exports.parseDuration = value => {
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
`undefined` predicates are ignored and `undefined` is returned if all predicates are `undefined`, this permits the most efficient composition:
|
||||
|
||||
```js
|
||||
const compositePredicate = not(every(undefined, some(not(predicate2), undefined)))
|
||||
|
||||
// ends up as
|
||||
|
||||
const compositePredicate = predicate2
|
||||
```
|
||||
|
||||
Predicates can also be passed wrapped in an array:
|
||||
|
||||
```js
|
||||
const compositePredicate = every([predicate1, some([predicate2, predicate3])])
|
||||
```
|
||||
|
||||
`this` and all arguments are passed to the nested predicates.
|
||||
|
||||
### `every(predicates)`
|
||||
|
||||
> Returns a predicate that returns `true` iff every predicate returns `true`.
|
||||
|
||||
```js
|
||||
const isBetween3And7 = every(
|
||||
n => n >= 3,
|
||||
n => n <= 7
|
||||
)
|
||||
|
||||
isBetween3And10(0)
|
||||
// → false
|
||||
|
||||
isBetween3And10(5)
|
||||
// → true
|
||||
|
||||
isBetween3And10(10)
|
||||
// → false
|
||||
```
|
||||
|
||||
### `not(predicate)`
|
||||
|
||||
> Returns a predicate that returns the negation of the predicate.
|
||||
|
||||
```js
|
||||
const isEven = n => n % 2 === 0
|
||||
const isOdd = not(isEven)
|
||||
|
||||
isOdd(1)
|
||||
// true
|
||||
|
||||
isOdd(2)
|
||||
// false
|
||||
```
|
||||
|
||||
### `some(predicates)`
|
||||
|
||||
> Returns a predicate that returns `true` iff some predicate returns `true`.
|
||||
|
||||
```js
|
||||
const isAliceOrBob = some(
|
||||
name => name === 'Alice',
|
||||
name => name === 'Bob'
|
||||
)
|
||||
|
||||
isAliceOrBob('Alice')
|
||||
// → true
|
||||
|
||||
isAliceOrBob('Bob')
|
||||
// → true
|
||||
|
||||
isAliceOrBob('Oscar')
|
||||
// → false
|
||||
```
|
||||
@@ -1 +0,0 @@
|
||||
../../scripts/npmignore
|
||||
@@ -1,105 +0,0 @@
|
||||
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
|
||||
|
||||
# @vates/predicates
|
||||
|
||||
[](https://npmjs.org/package/@vates/predicates)  [](https://bundlephobia.com/result?p=@vates/predicates) [](https://npmjs.org/package/@vates/predicates)
|
||||
|
||||
> Utilities to compose predicates
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/predicates):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/predicates
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
`undefined` predicates are ignored and `undefined` is returned if all predicates are `undefined`, this permits the most efficient composition:
|
||||
|
||||
```js
|
||||
const compositePredicate = not(every(undefined, some(not(predicate2), undefined)))
|
||||
|
||||
// ends up as
|
||||
|
||||
const compositePredicate = predicate2
|
||||
```
|
||||
|
||||
Predicates can also be passed wrapped in an array:
|
||||
|
||||
```js
|
||||
const compositePredicate = every([predicate1, some([predicate2, predicate3])])
|
||||
```
|
||||
|
||||
`this` and all arguments are passed to the nested predicates.
|
||||
|
||||
### `every(predicates)`
|
||||
|
||||
> Returns a predicate that returns `true` iff every predicate returns `true`.
|
||||
|
||||
```js
|
||||
const isBetween3And7 = every(
|
||||
n => n >= 3,
|
||||
n => n <= 7
|
||||
)
|
||||
|
||||
isBetween3And10(0)
|
||||
// → false
|
||||
|
||||
isBetween3And10(5)
|
||||
// → true
|
||||
|
||||
isBetween3And10(10)
|
||||
// → false
|
||||
```
|
||||
|
||||
### `not(predicate)`
|
||||
|
||||
> Returns a predicate that returns the negation of the predicate.
|
||||
|
||||
```js
|
||||
const isEven = n => n % 2 === 0
|
||||
const isOdd = not(isEven)
|
||||
|
||||
isOdd(1)
|
||||
// true
|
||||
|
||||
isOdd(2)
|
||||
// false
|
||||
```
|
||||
|
||||
### `some(predicates)`
|
||||
|
||||
> Returns a predicate that returns `true` iff some predicate returns `true`.
|
||||
|
||||
```js
|
||||
const isAliceOrBob = some(
|
||||
name => name === 'Alice',
|
||||
name => name === 'Bob'
|
||||
)
|
||||
|
||||
isAliceOrBob('Alice')
|
||||
// → true
|
||||
|
||||
isAliceOrBob('Bob')
|
||||
// → true
|
||||
|
||||
isAliceOrBob('Oscar')
|
||||
// → false
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are _very_ welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)
|
||||
@@ -1,87 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const {
|
||||
isArray,
|
||||
prototype: { filter },
|
||||
} = Array
|
||||
|
||||
class InvalidPredicate extends TypeError {
|
||||
constructor(value) {
|
||||
super('not a valid predicate')
|
||||
this.value = value
|
||||
}
|
||||
}
|
||||
|
||||
function isDefinedPredicate(value) {
|
||||
if (value === undefined) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof value !== 'function') {
|
||||
throw new InvalidPredicate(value)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function handleArgs() {
|
||||
let predicates
|
||||
if (!(arguments.length === 1 && isArray((predicates = arguments[0])))) {
|
||||
predicates = arguments
|
||||
}
|
||||
return filter.call(predicates, isDefinedPredicate)
|
||||
}
|
||||
|
||||
exports.every = function every() {
|
||||
const predicates = handleArgs.apply(this, arguments)
|
||||
const n = predicates.length
|
||||
if (n === 0) {
|
||||
return
|
||||
}
|
||||
if (n === 1) {
|
||||
return predicates[0]
|
||||
}
|
||||
return function everyPredicate() {
|
||||
for (let i = 0; i < n; ++i) {
|
||||
if (!predicates[i].apply(this, arguments)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const notPredicateTag = {}
|
||||
exports.not = function not(predicate) {
|
||||
if (isDefinedPredicate(predicate)) {
|
||||
if (predicate.tag === notPredicateTag) {
|
||||
return predicate.predicate
|
||||
}
|
||||
|
||||
function notPredicate() {
|
||||
return !predicate.apply(this, arguments)
|
||||
}
|
||||
notPredicate.predicate = predicate
|
||||
notPredicate.tag = notPredicateTag
|
||||
return notPredicate
|
||||
}
|
||||
}
|
||||
|
||||
exports.some = function some() {
|
||||
const predicates = handleArgs.apply(this, arguments)
|
||||
const n = predicates.length
|
||||
if (n === 0) {
|
||||
return
|
||||
}
|
||||
if (n === 1) {
|
||||
return predicates[0]
|
||||
}
|
||||
return function somePredicate() {
|
||||
for (let i = 0; i < n; ++i) {
|
||||
if (predicates[i].apply(this, arguments)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const assert = require('assert/strict')
|
||||
const { describe, it } = require('tap').mocha
|
||||
|
||||
const { every, not, some } = require('./')
|
||||
|
||||
const T = () => true
|
||||
const F = () => false
|
||||
|
||||
const testArgHandling = fn => {
|
||||
it('returns undefined if predicate is undefined', () => {
|
||||
assert.equal(fn(undefined), undefined)
|
||||
})
|
||||
|
||||
it('throws if it receives a non-predicate', () => {
|
||||
const error = new TypeError('not a valid predicate')
|
||||
error.value = 3
|
||||
assert.throws(() => fn(3), error)
|
||||
})
|
||||
}
|
||||
|
||||
const testArgsHandling = fn => {
|
||||
testArgHandling(fn)
|
||||
|
||||
it('returns the predicate if only a single one is passed', () => {
|
||||
assert.equal(fn(undefined, T), T)
|
||||
assert.equal(fn([undefined, T]), T)
|
||||
})
|
||||
|
||||
it('forwards this and arguments to predicates', () => {
|
||||
const thisArg = 'qux'
|
||||
const args = ['foo', 'bar', 'baz']
|
||||
const predicate = function () {
|
||||
assert.equal(this, thisArg)
|
||||
assert.deepEqual(Array.from(arguments), args)
|
||||
}
|
||||
fn(predicate, predicate).apply(thisArg, args)
|
||||
})
|
||||
}
|
||||
|
||||
const runTests = (fn, acceptMultiple, truthTable) =>
|
||||
it('works', () => {
|
||||
truthTable.forEach(([result, ...predicates]) => {
|
||||
if (acceptMultiple) {
|
||||
assert.equal(fn(predicates)(), result)
|
||||
} else {
|
||||
assert.equal(predicates.length, 1)
|
||||
}
|
||||
assert.equal(fn(...predicates)(), result)
|
||||
})
|
||||
})
|
||||
|
||||
describe('every', () => {
|
||||
testArgsHandling(every)
|
||||
runTests(every, true, [
|
||||
[true, T, T],
|
||||
[false, T, F],
|
||||
[false, F, T],
|
||||
[false, F, F],
|
||||
])
|
||||
})
|
||||
|
||||
describe('not', () => {
|
||||
testArgHandling(not)
|
||||
|
||||
it('returns the original predicate if negated twice', () => {
|
||||
assert.equal(not(not(T)), T)
|
||||
})
|
||||
|
||||
runTests(not, false, [
|
||||
[true, F],
|
||||
[false, T],
|
||||
])
|
||||
})
|
||||
|
||||
describe('some', () => {
|
||||
testArgsHandling(some)
|
||||
runTests(some, true, [
|
||||
[true, T, T],
|
||||
[true, T, F],
|
||||
[true, F, T],
|
||||
[false, F, F],
|
||||
])
|
||||
})
|
||||
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "@vates/predicates",
|
||||
"description": "Utilities to compose predicates",
|
||||
"keywords": [
|
||||
"and",
|
||||
"combine",
|
||||
"compose",
|
||||
"every",
|
||||
"function",
|
||||
"functions",
|
||||
"or",
|
||||
"predicate",
|
||||
"predicates",
|
||||
"some"
|
||||
],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/predicates",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"directory": "@vates/predicates",
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Vates SAS",
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"license": "ISC",
|
||||
"version": "1.1.0",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public",
|
||||
"test": "tap"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tap": "^16.0.1"
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
### `readChunk(stream, [size])`
|
||||
|
||||
- returns the next available chunk of data
|
||||
- like `stream.read()`, a number of bytes can be specified
|
||||
- returns with less data than expected if stream has ended
|
||||
- returns `null` if the stream has ended and no data has been read
|
||||
|
||||
```js
|
||||
import { readChunk } from '@vates/read-chunk'
|
||||
;(async () => {
|
||||
let chunk
|
||||
while ((chunk = await readChunk(stream, 1024)) !== null) {
|
||||
// do something with chunk
|
||||
}
|
||||
})()
|
||||
```
|
||||
|
||||
### `readChunkStrict(stream, [size])`
|
||||
|
||||
Similar behavior to `readChunk` but throws if the stream ended before the requested data could be read.
|
||||
|
||||
```js
|
||||
import { readChunkStrict } from '@vates/read-chunk'
|
||||
|
||||
const chunk = await readChunkStrict(stream, 1024)
|
||||
```
|
||||
@@ -10,18 +10,15 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/read-chunk):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/read-chunk
|
||||
```
|
||||
> npm install --save @vates/read-chunk
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### `readChunk(stream, [size])`
|
||||
|
||||
- returns the next available chunk of data
|
||||
- like `stream.read()`, a number of bytes can be specified
|
||||
- returns with less data than expected if stream has ended
|
||||
- returns `null` if the stream has ended and no data has been read
|
||||
- returns `null` if the stream has ended
|
||||
|
||||
```js
|
||||
import { readChunk } from '@vates/read-chunk'
|
||||
@@ -33,16 +30,6 @@ import { readChunk } from '@vates/read-chunk'
|
||||
})()
|
||||
```
|
||||
|
||||
### `readChunkStrict(stream, [size])`
|
||||
|
||||
Similar behavior to `readChunk` but throws if the stream ended before the requested data could be read.
|
||||
|
||||
```js
|
||||
import { readChunkStrict } from '@vates/read-chunk'
|
||||
|
||||
const chunk = await readChunkStrict(stream, 1024)
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are _very_ welcomed, either on the documentation or on
|
||||
|
||||
13
@vates/read-chunk/USAGE.md
Normal file
13
@vates/read-chunk/USAGE.md
Normal file
@@ -0,0 +1,13 @@
|
||||
- returns the next available chunk of data
|
||||
- like `stream.read()`, a number of bytes can be specified
|
||||
- returns `null` if the stream has ended
|
||||
|
||||
```js
|
||||
import { readChunk } from '@vates/read-chunk'
|
||||
;(async () => {
|
||||
let chunk
|
||||
while ((chunk = await readChunk(stream, 1024)) !== null) {
|
||||
// do something with chunk
|
||||
}
|
||||
})()
|
||||
```
|
||||
@@ -1,9 +1,5 @@
|
||||
'use strict'
|
||||
|
||||
const readChunk = (stream, size) =>
|
||||
stream.closed || stream.readableEnded
|
||||
? Promise.resolve(null)
|
||||
: size === 0
|
||||
size === 0
|
||||
? Promise.resolve(Buffer.alloc(0))
|
||||
: new Promise((resolve, reject) => {
|
||||
function onEnd() {
|
||||
@@ -32,22 +28,3 @@ const readChunk = (stream, size) =>
|
||||
onReadable()
|
||||
})
|
||||
exports.readChunk = readChunk
|
||||
|
||||
exports.readChunkStrict = async function readChunkStrict(stream, size) {
|
||||
const chunk = await readChunk(stream, size)
|
||||
if (chunk === null) {
|
||||
throw new Error('stream has ended without data')
|
||||
}
|
||||
|
||||
if (size !== undefined && chunk.length !== size) {
|
||||
const error = new Error('stream has ended with not enough data')
|
||||
Object.defineProperties(error, {
|
||||
chunk: {
|
||||
value: chunk,
|
||||
},
|
||||
})
|
||||
throw error
|
||||
}
|
||||
|
||||
return chunk
|
||||
}
|
||||
|
||||
43
@vates/read-chunk/index.spec.js
Normal file
43
@vates/read-chunk/index.spec.js
Normal file
@@ -0,0 +1,43 @@
|
||||
/* eslint-env jest */
|
||||
|
||||
const { Readable } = require('stream')
|
||||
|
||||
const { readChunk } = require('./')
|
||||
|
||||
const makeStream = it => Readable.from(it, { objectMode: false })
|
||||
makeStream.obj = Readable.from
|
||||
|
||||
describe('readChunk', () => {
|
||||
it('returns null if stream is empty', async () => {
|
||||
expect(await readChunk(makeStream([]))).toBe(null)
|
||||
})
|
||||
|
||||
describe('with binary stream', () => {
|
||||
it('returns the first chunk of data', async () => {
|
||||
expect(await readChunk(makeStream(['foo', 'bar']))).toEqual(Buffer.from('foo'))
|
||||
})
|
||||
|
||||
it('returns a chunk of the specified size (smaller than first)', async () => {
|
||||
expect(await readChunk(makeStream(['foo', 'bar']), 2)).toEqual(Buffer.from('fo'))
|
||||
})
|
||||
|
||||
it('returns a chunk of the specified size (larger than first)', async () => {
|
||||
expect(await readChunk(makeStream(['foo', 'bar']), 4)).toEqual(Buffer.from('foob'))
|
||||
})
|
||||
|
||||
it('returns less data if stream ends', async () => {
|
||||
expect(await readChunk(makeStream(['foo', 'bar']), 10)).toEqual(Buffer.from('foobar'))
|
||||
})
|
||||
|
||||
it('returns an empty buffer if the specified size is 0', async () => {
|
||||
expect(await readChunk(makeStream(['foo', 'bar']), 0)).toEqual(Buffer.alloc(0))
|
||||
})
|
||||
})
|
||||
|
||||
describe('with object stream', () => {
|
||||
it('returns the first chunk of data verbatim', async () => {
|
||||
const chunks = [{}, {}]
|
||||
expect(await readChunk(makeStream.obj(chunks))).toBe(chunks[0])
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,77 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const { describe, it } = require('test')
|
||||
const assert = require('node:assert').strict
|
||||
|
||||
const { Readable } = require('stream')
|
||||
|
||||
const { readChunk, readChunkStrict } = require('./')
|
||||
|
||||
const makeStream = it => Readable.from(it, { objectMode: false })
|
||||
makeStream.obj = Readable.from
|
||||
|
||||
describe('readChunk', () => {
|
||||
it('returns null if stream is empty', async () => {
|
||||
assert.strictEqual(await readChunk(makeStream([])), null)
|
||||
})
|
||||
|
||||
it('returns null if the stream is already ended', async () => {
|
||||
const stream = await makeStream([])
|
||||
await readChunk(stream)
|
||||
|
||||
assert.strictEqual(await readChunk(stream), null)
|
||||
})
|
||||
|
||||
describe('with binary stream', () => {
|
||||
it('returns the first chunk of data', async () => {
|
||||
assert.deepEqual(await readChunk(makeStream(['foo', 'bar'])), Buffer.from('foo'))
|
||||
})
|
||||
|
||||
it('returns a chunk of the specified size (smaller than first)', async () => {
|
||||
assert.deepEqual(await readChunk(makeStream(['foo', 'bar']), 2), Buffer.from('fo'))
|
||||
})
|
||||
|
||||
it('returns a chunk of the specified size (larger than first)', async () => {
|
||||
assert.deepEqual(await readChunk(makeStream(['foo', 'bar']), 4), Buffer.from('foob'))
|
||||
})
|
||||
|
||||
it('returns less data if stream ends', async () => {
|
||||
assert.deepEqual(await readChunk(makeStream(['foo', 'bar']), 10), Buffer.from('foobar'))
|
||||
})
|
||||
|
||||
it('returns an empty buffer if the specified size is 0', async () => {
|
||||
assert.deepEqual(await readChunk(makeStream(['foo', 'bar']), 0), Buffer.alloc(0))
|
||||
})
|
||||
})
|
||||
|
||||
describe('with object stream', () => {
|
||||
it('returns the first chunk of data verbatim', async () => {
|
||||
const chunks = [{}, {}]
|
||||
assert.strictEqual(await readChunk(makeStream.obj(chunks)), chunks[0])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const rejectionOf = promise =>
|
||||
promise.then(
|
||||
value => {
|
||||
throw value
|
||||
},
|
||||
error => error
|
||||
)
|
||||
|
||||
describe('readChunkStrict', function () {
|
||||
it('throws if stream is empty', async () => {
|
||||
const error = await rejectionOf(readChunkStrict(makeStream([])))
|
||||
assert(error instanceof Error)
|
||||
assert.strictEqual(error.message, 'stream has ended without data')
|
||||
assert.strictEqual(error.chunk, undefined)
|
||||
})
|
||||
|
||||
it('throws if stream ends with not enough data', async () => {
|
||||
const error = await rejectionOf(readChunkStrict(makeStream(['foo', 'bar']), 10))
|
||||
assert(error instanceof Error)
|
||||
assert.strictEqual(error.message, 'stream has ended with not enough data')
|
||||
assert.deepEqual(error.chunk, Buffer.from('foobar'))
|
||||
})
|
||||
})
|
||||
@@ -19,19 +19,15 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"version": "1.0.1",
|
||||
"version": "0.1.2",
|
||||
"engines": {
|
||||
"node": ">=8.10"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public",
|
||||
"test": "node--test"
|
||||
"postversion": "npm publish --access public"
|
||||
},
|
||||
"author": {
|
||||
"name": "Vates SAS",
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"devDependencies": {
|
||||
"test": "^3.2.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
```js
|
||||
import { Task } from '@vates/task'
|
||||
|
||||
const task = new Task({
|
||||
name: 'my task',
|
||||
|
||||
// if defined, a new detached task is created
|
||||
//
|
||||
// if not defined and created inside an existing task, the new task is considered a subtask
|
||||
onProgress(event) {
|
||||
// this function is called each time this task or one of it's subtasks change state
|
||||
const { id, timestamp, type } = event
|
||||
if (type === 'start') {
|
||||
const { name, parentId } = event
|
||||
} else if (type === 'end') {
|
||||
const { result, status } = event
|
||||
} else if (type === 'info' || type === 'warning') {
|
||||
const { data, message } = event
|
||||
} else if (type === 'property') {
|
||||
const { name, value } = event
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// this field is settable once before being observed
|
||||
task.id
|
||||
|
||||
task.status
|
||||
await task.abort()
|
||||
|
||||
// if fn rejects, the task will be marked as failed
|
||||
const result = await task.runInside(fn)
|
||||
|
||||
// if fn rejects, the task will be marked as failed
|
||||
// if fn resolves, the task will be marked as succeeded
|
||||
const result = await task.run(fn)
|
||||
|
||||
// the abort signal of the current task if any, otherwise is `undefined`
|
||||
Task.abortSignal
|
||||
|
||||
// sends an info on the current task if any, otherwise does nothing
|
||||
Task.info(message, data)
|
||||
|
||||
// sends an info on the current task if any, otherwise does nothing
|
||||
Task.warning(message, data)
|
||||
|
||||
// attaches a property to the current task if any, otherwise does nothing
|
||||
//
|
||||
// the latest value takes precedence
|
||||
//
|
||||
// examples:
|
||||
// - progress
|
||||
Task.set(property, value)
|
||||
```
|
||||
@@ -1 +0,0 @@
|
||||
../../scripts/npmignore
|
||||
@@ -1,85 +0,0 @@
|
||||
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
|
||||
|
||||
# @vates/task
|
||||
|
||||
[](https://npmjs.org/package/@vates/task)  [](https://bundlephobia.com/result?p=@vates/task) [](https://npmjs.org/package/@vates/task)
|
||||
|
||||
## Install
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/task):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/task
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
import { Task } from '@vates/task'
|
||||
|
||||
const task = new Task({
|
||||
name: 'my task',
|
||||
|
||||
// if defined, a new detached task is created
|
||||
//
|
||||
// if not defined and created inside an existing task, the new task is considered a subtask
|
||||
onProgress(event) {
|
||||
// this function is called each time this task or one of it's subtasks change state
|
||||
const { id, timestamp, type } = event
|
||||
if (type === 'start') {
|
||||
const { name, parentId } = event
|
||||
} else if (type === 'end') {
|
||||
const { result, status } = event
|
||||
} else if (type === 'info' || type === 'warning') {
|
||||
const { data, message } = event
|
||||
} else if (type === 'property') {
|
||||
const { name, value } = event
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// this field is settable once before being observed
|
||||
task.id
|
||||
|
||||
task.status
|
||||
await task.abort()
|
||||
|
||||
// if fn rejects, the task will be marked as failed
|
||||
const result = await task.runInside(fn)
|
||||
|
||||
// if fn rejects, the task will be marked as failed
|
||||
// if fn resolves, the task will be marked as succeeded
|
||||
const result = await task.run(fn)
|
||||
|
||||
// the abort signal of the current task if any, otherwise is `undefined`
|
||||
Task.abortSignal
|
||||
|
||||
// sends an info on the current task if any, otherwise does nothing
|
||||
Task.info(message, data)
|
||||
|
||||
// sends an info on the current task if any, otherwise does nothing
|
||||
Task.warning(message, data)
|
||||
|
||||
// attaches a property to the current task if any, otherwise does nothing
|
||||
//
|
||||
// the latest value takes precedence
|
||||
//
|
||||
// examples:
|
||||
// - progress
|
||||
Task.set(property, value)
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are _very_ welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
[ISC](https://spdx.org/licenses/ISC) © [Vates SAS](https://vates.fr)
|
||||
@@ -1,184 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
const assert = require('node:assert').strict
|
||||
const { AsyncLocalStorage } = require('node:async_hooks')
|
||||
|
||||
// define a read-only, non-enumerable, non-configurable property
|
||||
function define(object, property, value) {
|
||||
Object.defineProperty(object, property, { value })
|
||||
}
|
||||
|
||||
const noop = Function.prototype
|
||||
|
||||
const ABORTED = 'aborted'
|
||||
const ABORTING = 'aborting'
|
||||
const FAILURE = 'failure'
|
||||
const PENDING = 'pending'
|
||||
const SUCCESS = 'success'
|
||||
exports.STATUS = { ABORTED, ABORTING, FAILURE, PENDING, SUCCESS }
|
||||
|
||||
const asyncStorage = new AsyncLocalStorage()
|
||||
const getTask = () => asyncStorage.getStore()
|
||||
|
||||
exports.Task = class Task {
|
||||
static get abortSignal() {
|
||||
const task = getTask()
|
||||
if (task !== undefined) {
|
||||
return task.#abortController.signal
|
||||
}
|
||||
}
|
||||
|
||||
static info(message, data) {
|
||||
const task = getTask()
|
||||
if (task !== undefined) {
|
||||
task.#emit('info', { data, message })
|
||||
}
|
||||
}
|
||||
|
||||
static run(opts, fn) {
|
||||
return new this(opts).run(fn)
|
||||
}
|
||||
|
||||
static set(name, value) {
|
||||
const task = getTask()
|
||||
if (task !== undefined) {
|
||||
task.#emit('property', { name, value })
|
||||
}
|
||||
}
|
||||
|
||||
static warning(message, data) {
|
||||
const task = getTask()
|
||||
if (task !== undefined) {
|
||||
task.#emit('warning', { data, message })
|
||||
}
|
||||
}
|
||||
|
||||
static wrap(opts, fn) {
|
||||
// compatibility with @decorateWith
|
||||
if (typeof fn !== 'function') {
|
||||
;[fn, opts] = [opts, fn]
|
||||
}
|
||||
|
||||
return function taskRun() {
|
||||
return Task.run(typeof opts === 'function' ? opts.apply(this, arguments) : opts, () => fn.apply(this, arguments))
|
||||
}
|
||||
}
|
||||
|
||||
#abortController = new AbortController()
|
||||
#onProgress
|
||||
#parent
|
||||
|
||||
get id() {
|
||||
return (this.id = Math.random().toString(36).slice(2))
|
||||
}
|
||||
set id(value) {
|
||||
define(this, 'id', value)
|
||||
}
|
||||
|
||||
#startData
|
||||
|
||||
#status = PENDING
|
||||
get status() {
|
||||
return this.#status
|
||||
}
|
||||
|
||||
constructor({ name, onProgress }) {
|
||||
this.#startData = { name }
|
||||
|
||||
if (onProgress !== undefined) {
|
||||
this.#onProgress = onProgress
|
||||
} else {
|
||||
const parent = getTask()
|
||||
if (parent !== undefined) {
|
||||
this.#parent = parent
|
||||
|
||||
const { signal } = parent.#abortController
|
||||
signal.addEventListener('abort', () => {
|
||||
this.#abortController.abort(signal.reason)
|
||||
})
|
||||
|
||||
this.#onProgress = parent.#onProgress
|
||||
this.#startData.parentId = parent.id
|
||||
} else {
|
||||
this.#onProgress = noop
|
||||
}
|
||||
}
|
||||
|
||||
const { signal } = this.#abortController
|
||||
signal.addEventListener('abort', () => {
|
||||
if (this.status === PENDING) {
|
||||
this.#status = this.#running ? ABORTING : ABORTED
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
abort(reason) {
|
||||
this.#abortController.abort(reason)
|
||||
}
|
||||
|
||||
#emit(type, data) {
|
||||
data.id = this.id
|
||||
data.timestamp = Date.now()
|
||||
data.type = type
|
||||
this.#onProgress(data)
|
||||
}
|
||||
|
||||
#handleMaybeAbortion(result) {
|
||||
if (this.status === ABORTING) {
|
||||
this.#status = ABORTED
|
||||
this.#emit('end', { status: ABORTED, result })
|
||||
return true
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async run(fn) {
|
||||
const result = await this.runInside(fn)
|
||||
if (this.status === PENDING) {
|
||||
this.#status = SUCCESS
|
||||
this.#emit('end', { status: SUCCESS, result })
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
#running = false
|
||||
async runInside(fn) {
|
||||
assert.equal(this.status, PENDING)
|
||||
assert.equal(this.#running, false)
|
||||
this.#running = true
|
||||
|
||||
const startData = this.#startData
|
||||
if (startData !== undefined) {
|
||||
this.#startData = undefined
|
||||
this.#emit('start', startData)
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await asyncStorage.run(this, fn)
|
||||
this.#handleMaybeAbortion(result)
|
||||
this.#running = false
|
||||
return result
|
||||
} catch (result) {
|
||||
if (!this.#handleMaybeAbortion(result)) {
|
||||
this.#status = FAILURE
|
||||
this.#emit('end', { status: FAILURE, result })
|
||||
}
|
||||
throw result
|
||||
}
|
||||
}
|
||||
|
||||
wrap(fn) {
|
||||
const task = this
|
||||
return function taskRun() {
|
||||
return task.run(() => fn.apply(this, arguments))
|
||||
}
|
||||
}
|
||||
|
||||
wrapInside(fn) {
|
||||
const task = this
|
||||
return function taskRunInside() {
|
||||
return task.runInside(() => fn.apply(this, arguments))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "@vates/task",
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@vates/task",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"directory": "@vates/task",
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Vates SAS",
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"license": "ISC",
|
||||
"version": "0.0.1",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish --access public"
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@vates/toggle-scripts):
|
||||
|
||||
```sh
|
||||
npm install --save @vates/toggle-scripts
|
||||
```
|
||||
> npm install --save @vates/toggle-scripts
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
'use strict'
|
||||
|
||||
const fs = require('fs')
|
||||
|
||||
const mapKeys = (object, iteratee) => {
|
||||
@@ -30,7 +28,6 @@ if (args.length === 0) {
|
||||
|
||||
${name} v${version}
|
||||
`)
|
||||
// eslint-disable-next-line n/no-process-exit
|
||||
process.exit()
|
||||
}
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/async-map):
|
||||
|
||||
```sh
|
||||
npm install --save @xen-orchestra/async-map
|
||||
```
|
||||
> npm install --save @xen-orchestra/async-map
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
'use strict'
|
||||
|
||||
const wrapCall = (fn, arg, thisArg) => {
|
||||
try {
|
||||
return Promise.resolve(fn.call(thisArg, arg))
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
'use strict'
|
||||
|
||||
const { describe, it } = require('test')
|
||||
const assert = require('assert').strict
|
||||
const sinon = require('sinon')
|
||||
/* eslint-env jest */
|
||||
|
||||
const { asyncMapSettled } = require('./')
|
||||
|
||||
@@ -11,29 +7,26 @@ const noop = Function.prototype
|
||||
describe('asyncMapSettled', () => {
|
||||
it('works', async () => {
|
||||
const values = [Math.random(), Math.random()]
|
||||
const spy = sinon.spy(async v => v * 2)
|
||||
const spy = jest.fn(async v => v * 2)
|
||||
const iterable = new Set(values)
|
||||
|
||||
// returns an array containing the result of each calls
|
||||
assert.deepStrictEqual(
|
||||
await asyncMapSettled(iterable, spy),
|
||||
values.map(value => value * 2)
|
||||
)
|
||||
expect(await asyncMapSettled(iterable, spy)).toEqual(values.map(value => value * 2))
|
||||
|
||||
for (let i = 0, n = values.length; i < n; ++i) {
|
||||
// each call receive the current item as sole argument
|
||||
assert.deepStrictEqual(spy.args[i], [values[i]])
|
||||
expect(spy.mock.calls[i]).toEqual([values[i]])
|
||||
|
||||
// each call as this bind to the iterable
|
||||
assert.deepStrictEqual(spy.thisValues[i], iterable)
|
||||
expect(spy.mock.instances[i]).toBe(iterable)
|
||||
}
|
||||
})
|
||||
|
||||
it('can use a specified thisArg', () => {
|
||||
const thisArg = {}
|
||||
const spy = sinon.spy()
|
||||
const spy = jest.fn()
|
||||
asyncMapSettled(['foo'], spy, thisArg)
|
||||
assert.deepStrictEqual(spy.thisValues[0], thisArg)
|
||||
expect(spy.mock.instances[0]).toBe(thisArg)
|
||||
})
|
||||
|
||||
it('rejects only when all calls as resolved', async () => {
|
||||
@@ -60,22 +53,19 @@ describe('asyncMapSettled', () => {
|
||||
// wait for all microtasks to settle
|
||||
await new Promise(resolve => setImmediate(resolve))
|
||||
|
||||
assert.strictEqual(hasSettled, false)
|
||||
expect(hasSettled).toBe(false)
|
||||
|
||||
defers[1].resolve()
|
||||
|
||||
// wait for all microtasks to settle
|
||||
await new Promise(resolve => setImmediate(resolve))
|
||||
|
||||
assert.strictEqual(hasSettled, true)
|
||||
await assert.rejects(promise, error)
|
||||
expect(hasSettled).toBe(true)
|
||||
await expect(promise).rejects.toBe(error)
|
||||
})
|
||||
|
||||
it('issues when latest promise rejects', async () => {
|
||||
const error = new Error()
|
||||
await assert.rejects(
|
||||
asyncMapSettled([1], () => Promise.reject(error)),
|
||||
error
|
||||
)
|
||||
await expect(asyncMapSettled([1], () => Promise.reject(error))).rejects.toBe(error)
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,3 @@
|
||||
'use strict'
|
||||
|
||||
// type MaybePromise<T> = Promise<T> | T
|
||||
//
|
||||
// declare export function asyncMap<T1, T2>(
|
||||
|
||||
@@ -31,11 +31,6 @@
|
||||
"lodash": "^4.17.4"
|
||||
},
|
||||
"scripts": {
|
||||
"postversion": "npm publish",
|
||||
"test": "node--test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"sinon": "^15.0.1",
|
||||
"test": "^3.2.1"
|
||||
"postversion": "npm publish"
|
||||
}
|
||||
}
|
||||
|
||||
1
@xen-orchestra/audit-core/.babelrc.js
Normal file
1
@xen-orchestra/audit-core/.babelrc.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require('../../@xen-orchestra/babel-config')(require('./package.json'))
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
Installation of the [npm package](https://npmjs.org/package/@xen-orchestra/audit-core):
|
||||
|
||||
```sh
|
||||
npm install --save @xen-orchestra/audit-core
|
||||
```
|
||||
> npm install --save @xen-orchestra/audit-core
|
||||
```
|
||||
|
||||
## Contributions
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user