From 99b08738ce3cc894acbf013acca51d1c7219fb7f Mon Sep 17 00:00:00 2001 From: Egor Dydykin Date: Wed, 30 Oct 2024 15:31:44 +0300 Subject: [PATCH 1/3] v4: typescript first --- .eslintrc.js | 205 ++- .github/workflows/npm-publish.yml | 2 +- .gitignore | 1 + docs/array_block.md | 38 +- docs/cache.md | 2 +- docs/cancel.md | 8 +- docs/context.md | 4 +- docs/deps.md | 116 +- docs/first_block.md | 22 +- docs/function_block.md | 18 +- docs/http_block.md | 82 +- docs/inheritance.md | 4 +- docs/logs.md | 18 +- docs/object_block.md | 38 +- docs/options.md | 4 +- docs/options_after.md | 4 +- docs/options_before.md | 2 +- docs/options_params.md | 16 +- docs/options_required.md | 10 +- docs/pipe_block.md | 32 +- docs/run.md | 6 +- docs/typescript-examples/array.ts | 146 +- docs/typescript-examples/error.ts | 23 +- docs/typescript-examples/first.ts | 168 ++ docs/typescript-examples/func.ts | 110 +- docs/typescript-examples/http.ts | 122 +- docs/typescript-examples/object.ts | 219 +-- docs/typescript-examples/options.ts | 85 +- docs/typescript-examples/pipe.ts | 168 ++ docs/typescript-examples/session.ts | 159 +- global.d.ts | 13 + jest.config.js | 19 +- lib/arrayBlock.ts | 145 ++ lib/array_block.js | 33 - lib/block.js | 419 ----- lib/block.ts | 656 ++++++++ lib/cache.js | 32 - lib/cache.ts | 41 + lib/cancel.js | 78 - lib/cancel.ts | 78 + lib/compositeBlock.ts | 117 ++ lib/composite_block.js | 65 - lib/context.js | 107 -- lib/context.ts | 190 +++ lib/depsDomain.ts | 23 + lib/deps_domain.js | 21 - lib/error.js | 93 -- lib/error.ts | 148 ++ lib/extend.js | 16 - lib/extend.ts | 16 + lib/extendOption.ts | 19 + lib/extend_option.js | 16 - lib/firstBlock.ts | 174 ++ lib/first_block.js | 51 - lib/functionBlock.ts | 133 ++ lib/function_block.js | 51 - lib/getDeferred.ts | 26 + lib/get_deferred.js | 15 - lib/httpBlock.ts | 580 +++++++ lib/http_block.js | 350 ---- lib/{index.d.ts => index.d2.jtxt} | 530 +++--- lib/index.js | 72 - lib/index.ts | 187 +++ lib/{is_plain_object.js => isPlainObject.ts} | 9 +- lib/logger.js | 99 -- lib/logger.ts | 142 ++ lib/objectBlock.ts | 149 ++ lib/object_block.js | 40 - lib/pipeBlock.ts | 170 ++ lib/pipe_block.js | 45 - lib/request.js | 560 ------- lib/request.ts | 638 +++++++ lib/stripNullAndUndefinedValues.ts | 18 + lib/strip_null_and_undefined_values.js | 14 - lib/types.ts | 187 +++ package-lock.json | 582 ++++--- package.json | 17 +- tests/arrayBlock.test.ts | 420 +++++ tests/array_block.test.js | 372 ----- tests/cache.test.js | 78 - tests/cache.test.ts | 78 + tests/cancel.test.js | 221 --- tests/cancel.test.ts | 202 +++ tests/descript.test.js | 36 - tests/descript.test.ts | 48 + tests/error.test.js | 78 - tests/error.test.ts | 92 ++ tests/{expect.js => expect.ts} | 15 +- tests/firstBlock.test.ts | 108 ++ tests/first_block.test.js | 107 -- tests/functionBlock.test.ts | 130 ++ tests/function_block.test.js | 124 -- tests/helpers.js | 99 -- tests/helpers.ts | 105 ++ tests/httpBlock.test.ts | 1465 +++++++++++++++++ tests/http_block.test.js | 1372 --------------- tests/isPlainObject.test.ts | 34 + tests/is_plain_object.test.js | 33 - tests/lifecycle.test.js | 85 - tests/lifecycle.test.ts | 84 + tests/objectBlock.test.ts | 438 +++++ tests/object_block.test.js | 421 ----- tests/options.after.test.js | 399 ----- tests/options.after.test.ts | 397 +++++ tests/options.before.test.js | 331 ---- tests/options.before.test.ts | 333 ++++ tests/options.cache.test.js | 333 ---- tests/options.cache.test.ts | 321 ++++ tests/options.deps.test.js | 960 ----------- tests/options.deps.test.ts | 983 +++++++++++ tests/options.error.test.js | 206 --- tests/options.error.test.ts | 181 ++ tests/options.params.test.js | 208 --- tests/options.params.test.ts | 190 +++ tests/options.timeout.test.js | 42 - tests/options.timeout.test.ts | 41 + tests/pipeBlock.test.ts | 117 ++ tests/pipe_block.test.js | 117 -- tests/request.test.js | 1178 ------------- tests/request.test.ts | 1197 ++++++++++++++ tests/server.js | 135 +- tests/stripNullAndUndefinedValues.test.ts | 33 + tests/strip_null_and_undefined_values.test.js | 34 - tsconfig-build.json | 9 + tsconfig.json | 95 +- 125 files changed, 12863 insertions(+), 10238 deletions(-) create mode 100644 docs/typescript-examples/first.ts create mode 100644 docs/typescript-examples/pipe.ts create mode 100644 global.d.ts create mode 100644 lib/arrayBlock.ts delete mode 100644 lib/array_block.js delete mode 100644 lib/block.js create mode 100644 lib/block.ts delete mode 100644 lib/cache.js create mode 100644 lib/cache.ts delete mode 100644 lib/cancel.js create mode 100644 lib/cancel.ts create mode 100644 lib/compositeBlock.ts delete mode 100644 lib/composite_block.js delete mode 100644 lib/context.js create mode 100644 lib/context.ts create mode 100644 lib/depsDomain.ts delete mode 100644 lib/deps_domain.js delete mode 100644 lib/error.js create mode 100644 lib/error.ts delete mode 100644 lib/extend.js create mode 100644 lib/extend.ts create mode 100644 lib/extendOption.ts delete mode 100644 lib/extend_option.js create mode 100644 lib/firstBlock.ts delete mode 100644 lib/first_block.js create mode 100644 lib/functionBlock.ts delete mode 100644 lib/function_block.js create mode 100644 lib/getDeferred.ts delete mode 100644 lib/get_deferred.js create mode 100644 lib/httpBlock.ts delete mode 100644 lib/http_block.js rename lib/{index.d.ts => index.d2.jtxt} (67%) delete mode 100644 lib/index.js create mode 100644 lib/index.ts rename lib/{is_plain_object.js => isPlainObject.ts} (65%) delete mode 100644 lib/logger.js create mode 100644 lib/logger.ts create mode 100644 lib/objectBlock.ts delete mode 100644 lib/object_block.js create mode 100644 lib/pipeBlock.ts delete mode 100644 lib/pipe_block.js delete mode 100644 lib/request.js create mode 100644 lib/request.ts create mode 100644 lib/stripNullAndUndefinedValues.ts delete mode 100644 lib/strip_null_and_undefined_values.js create mode 100644 lib/types.ts create mode 100644 tests/arrayBlock.test.ts delete mode 100644 tests/array_block.test.js delete mode 100644 tests/cache.test.js create mode 100644 tests/cache.test.ts delete mode 100644 tests/cancel.test.js create mode 100644 tests/cancel.test.ts delete mode 100644 tests/descript.test.js create mode 100644 tests/descript.test.ts delete mode 100644 tests/error.test.js create mode 100644 tests/error.test.ts rename tests/{expect.js => expect.ts} (58%) create mode 100644 tests/firstBlock.test.ts delete mode 100644 tests/first_block.test.js create mode 100644 tests/functionBlock.test.ts delete mode 100644 tests/function_block.test.js delete mode 100644 tests/helpers.js create mode 100644 tests/helpers.ts create mode 100644 tests/httpBlock.test.ts delete mode 100644 tests/http_block.test.js create mode 100644 tests/isPlainObject.test.ts delete mode 100644 tests/is_plain_object.test.js delete mode 100644 tests/lifecycle.test.js create mode 100644 tests/lifecycle.test.ts create mode 100644 tests/objectBlock.test.ts delete mode 100644 tests/object_block.test.js delete mode 100644 tests/options.after.test.js create mode 100644 tests/options.after.test.ts delete mode 100644 tests/options.before.test.js create mode 100644 tests/options.before.test.ts delete mode 100644 tests/options.cache.test.js create mode 100644 tests/options.cache.test.ts delete mode 100644 tests/options.deps.test.js create mode 100644 tests/options.deps.test.ts delete mode 100644 tests/options.error.test.js create mode 100644 tests/options.error.test.ts delete mode 100644 tests/options.params.test.js create mode 100644 tests/options.params.test.ts delete mode 100644 tests/options.timeout.test.js create mode 100644 tests/options.timeout.test.ts create mode 100644 tests/pipeBlock.test.ts delete mode 100644 tests/pipe_block.test.js delete mode 100644 tests/request.test.js create mode 100644 tests/request.test.ts create mode 100644 tests/stripNullAndUndefinedValues.test.ts delete mode 100644 tests/strip_null_and_undefined_values.test.js create mode 100644 tsconfig-build.json diff --git a/.eslintrc.js b/.eslintrc.js index 6616596..b7a3f01 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,15 +1,194 @@ module.exports = { - extends: [ - 'plugin:nop/nop', + 'extends': [ 'eslint:recommended', + 'plugin:jest/recommended', + 'plugin:jest/style', + 'plugin:@typescript-eslint/eslint-recommended', + 'plugin:@typescript-eslint/recommended', ], plugins: [ 'jest', + '@typescript-eslint', ], - env: { - jest: true, + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, }, rules: { + 'no-var': [ 'off' ], + + '@typescript-eslint/array-type': [ 'error', { + 'default': 'generic', + readonly: 'generic', + } ], + '@typescript-eslint/brace-style': [ 'error', '1tbs' ], + '@typescript-eslint/consistent-type-imports': [ 'error' ], + '@typescript-eslint/explicit-function-return-type': [ 'off' ], + '@typescript-eslint/explicit-module-boundary-types': [ 'off' ], + '@typescript-eslint/indent': [ 'error', 4, { + SwitchCase: 1, + } ], + '@typescript-eslint/member-delimiter-style': [ 'error' ], + '@typescript-eslint/naming-convention': [ 'error', + { + selector: 'default', + format: [ 'camelCase' ], + leadingUnderscore: 'allow', + trailingUnderscore: 'forbid', + filter: { + regex: '^(UNSAFE_componentWillReceiveProps|UNSAFE_componentWillMount|UNSAFE_componentWillUpdate)$', + match: false, + }, + }, + { + selector: 'class', + format: [ 'PascalCase' ], + }, + { + selector: 'enum', + format: [ 'PascalCase', 'UPPER_CASE' ], + }, + { + selector: 'enumMember', + format: [ 'camelCase', 'PascalCase', 'UPPER_CASE' ], + }, + { + selector: 'function', + format: [ 'camelCase', 'PascalCase' ], + }, + { + selector: 'interface', + format: [ 'PascalCase' ], + }, + { + selector: 'method', + format: [ 'camelCase', 'snake_case', 'UPPER_CASE' ], + leadingUnderscore: 'allow', + filter: { + regex: '^(UNSAFE_componentWillReceiveProps|UNSAFE_componentWillMount|UNSAFE_componentWillUpdate)$', + match: false, + }, + }, + { + selector: 'parameter', + format: [ 'camelCase', 'PascalCase' ], + leadingUnderscore: 'allow', + }, + { + selector: 'property', + format: null, + }, + { + selector: 'typeAlias', + format: [ 'PascalCase' ], + }, + { + selector: 'typeParameter', + format: [ 'PascalCase', 'UPPER_CASE' ], + }, + { + selector: 'variable', + format: [ 'camelCase', 'PascalCase', 'UPPER_CASE' ], + leadingUnderscore: 'allow', + } ], + '@typescript-eslint/no-duplicate-imports': [ 'error' ], + '@typescript-eslint/no-empty-function': [ 'off' ], + '@typescript-eslint/no-unused-vars': 'error', + '@typescript-eslint/no-use-before-define': 'off', + '@typescript-eslint/no-useless-constructor': [ 'error' ], + '@typescript-eslint/type-annotation-spacing': 'error', + + // отключены в пользу @typescript-eslint + 'brace-style': 'off', + camelcase: 'off', + indent: 'off', + 'no-unused-vars': 'off', + 'no-use-before-define': 'off', + 'no-useless-constructor': 'off', + + 'array-bracket-spacing': [ 'error', 'always' ], + 'comma-dangle': [ 'error', 'always-multiline' ], + 'comma-spacing': [ 'error' ], + 'comma-style': [ 'error', 'last' ], + curly: [ 'error', 'all' ], + 'eol-last': 'error', + eqeqeq: [ 'error', 'allow-null' ], + 'id-match': [ 'error', '^[\\w$]+$' ], + 'jsx-quotes': [ 'error', 'prefer-double' ], + 'key-spacing': [ 'error', { + beforeColon: false, + afterColon: true, + } ], + 'keyword-spacing': 'error', + 'linebreak-style': [ 'error', 'unix' ], + 'lines-around-comment': [ 'error', { + beforeBlockComment: true, + allowBlockStart: true, + } ], + 'max-len': [ 'error', 160, 4 ], + 'no-console': 'error', + 'no-empty': [ 'error', { allowEmptyCatch: true } ], + 'no-implicit-coercion': [ 'error', { + number: true, + 'boolean': true, + string: true, + } ], + 'no-mixed-operators': [ 'error', { + groups: [ + [ '&&', '||' ], + ], + } ], + 'no-mixed-spaces-and-tabs': 'error', + 'no-multiple-empty-lines': [ 'error', { + max: 2, + maxEOF: 0, + maxBOF: 0, + } ], + 'no-multi-spaces': 'error', + 'no-multi-str': 'error', + 'no-nested-ternary': 'error', + // Это правило добавили в eslint@6 в eslint:recommended. Оно нам не надо + 'no-prototype-builtins': 'off', + 'no-trailing-spaces': 'error', + 'no-spaced-func': 'error', + 'no-with': 'error', + 'object-curly-spacing': [ 'error', 'always' ], + 'object-shorthand': 'off', + 'one-var': [ 'error', 'never' ], + 'operator-linebreak': [ 'error', 'after' ], + 'prefer-const': 'error', + 'prefer-rest-params': 'off', + 'prefer-spread': 'off', + 'quote-props': [ 'error', 'as-needed', { + keywords: true, + numbers: true, + } ], + quotes: [ 'error', 'single', { + allowTemplateLiterals: true, + } ], + radix: 'error', + semi: [ 'error', 'always' ], + 'space-before-function-paren': [ 'error', 'never' ], + 'space-before-blocks': [ 'error', 'always' ], + 'space-in-parens': [ 'error', 'never' ], + 'space-infix-ops': 'error', + 'space-unary-ops': 'off', + 'template-curly-spacing': [ 'error', 'always' ], + 'valid-jsdoc': [ 'error', { + requireParamDescription: false, + requireReturnDescription: false, + requireReturn: false, + prefer: { + 'return': 'returns', + }, + } ], + 'wrap-iife': [ 'error', 'inside' ], + yoda: [ 'error', 'never', { exceptRange: true } ], + 'jest/consistent-test-it': [ 'error', { fn: 'it', withinDescribe: 'it', @@ -24,5 +203,21 @@ module.exports = { 'jest/no-large-snapshots': [ 'error', { maxSize: 500 } ], 'jest/valid-expect': 'error', }, + env: { + node: true, + jest: true, + }, + overrides: [ + { + files: [ '*.ts', '*.tsx' ], + rules: { + '@typescript-eslint/no-require-imports': 'error', + '@typescript-eslint/no-use-before-define': [ 'error', { + functions: false, + } ], + 'prefer-rest-params': 'error', + 'prefer-spread': 'error', + }, + }, + ], }; - diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 7b72590..7bae146 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -16,7 +16,7 @@ jobs: with: node-version: 20 - run: npm ci - - run: npm test && npm run ts-compile + - run: npm test && npm run prepack publish-npm: needs: build diff --git a/.gitignore b/.gitignore index 5af28b7..ffd68ff 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ node_modules tests/server.key tests/server.crt coverage +build diff --git a/docs/array_block.md b/docs/array_block.md index ba621d8..2015034 100644 --- a/docs/array_block.md +++ b/docs/array_block.md @@ -4,30 +4,30 @@ из других блоков: ```js -const block_foo = require( '.../blocks/foo' ); -const block_bar = require( '.../blocks/bar' ); +const blockFoo = require( '.../blocks/foo' ); +const blockBar = require( '.../blocks/bar' ); ... const block = de.array( { block: [ - block_foo, - block_bar, + blockFoo, + blockBar, ... ], } ); ``` -Когда мы запускаем `de.array`, запускаются все его непосредственные подблоки (`block_foo`, `block_bar`, ...) +Когда мы запускаем `de.array`, запускаются все его непосредственные подблоки (`blockFoo`, `blockBar`, ...) и из их результатов составляется результат `de.array`'а: ```js const result = [ - // Результат работы block_foo - result_foo, - // Результат работы block_bar - result_bar, + // Результат работы blockFoo + resultFoo, + // Результат работы blockBar + resultBar, ... ]; ``` @@ -43,10 +43,10 @@ const result = [ ```js const result = [ - // Ошибка block_foo - error_foo, - // Результат работы block_bar - result_bar, + // Ошибка blockFoo + errorFoo, + // Результат работы blockBar + resultBar, ... ]; ``` @@ -61,12 +61,12 @@ const block = de.array( { block: [ // Если этот блок зафейлится, то и весь de.array так же зафейлится. - block_foo( { + blockFoo.extend( { options: { required: true, }, } ), - block_bar, + blockBar, ... ], @@ -80,10 +80,10 @@ const block = de.array( { ```js const block = de.array( { block: [ - block_foo, + blockFoo, de.array( { block: [ - block_quu, + blockQuu, ... ], } ), @@ -96,9 +96,9 @@ const block = de.array( { ```js const result = [ - result_foo, + resultFoo, [ - result_quu, + resultQuu, ... ], ... diff --git a/docs/cache.md b/docs/cache.md index 98dfea4..4b09d06 100644 --- a/docs/cache.md +++ b/docs/cache.md @@ -10,7 +10,7 @@ ```js const cache = { get: function( { key, context } ) { ... }, - set: function( { key, value, maxage, context } ) { ... }, + set: function( { key, value, maxage } ) { ... }, }; ``` diff --git a/docs/cancel.md b/docs/cancel.md index 6c90483..de8dbc6 100644 --- a/docs/cancel.md +++ b/docs/cancel.md @@ -72,11 +72,11 @@ const block = de.block( { ```js options: { after: ( { result, context } ) => { - if ( result.redirect_url ) { + if ( result.redirectUrl ) { const { res } = context; res.statusCode = 302; - res.setHeader( 'location', result.redirect_url ); + res.setHeader( 'location', result.redirectUrl ); res.end(); } }, @@ -97,13 +97,13 @@ options: { const block = de.block( { options: { after: ( { result, cancel } ) => { - if ( result.redirect_url ) { + if ( result.redirectUrl ) { // Не делаем редирект изнутри блока, // но кидаем специальную ошибку о том, что нужно сделать редирект. // cancel.cancel( de.error( { id: 'REDIRECT', - location: result.redirect_url, + location: result.redirectUrl, status_code: 302, } ) ); } diff --git a/docs/context.md b/docs/context.md index 7152352..7bced1d 100644 --- a/docs/context.md +++ b/docs/context.md @@ -53,7 +53,7 @@ const result = de.run( block, { context } ); // Более реалистично // -const api_config = require( '.../api/config' ); +import apiConfig from '.../api/config'; const server = http_.createServer( ( req, res ) => { const params = ...; @@ -65,7 +65,7 @@ const server = http_.createServer( ( req, res ) => { const context = { req: req, res: res, - api: api_config, + api: apiConfig, }; const result = de.run( block, { params, context } ); diff --git a/docs/deps.md b/docs/deps.md index 3ca360e..83f65ce 100644 --- a/docs/deps.md +++ b/docs/deps.md @@ -11,24 +11,24 @@ ## Простой пример ```js -const block = ( { generate_id } ) => { - const foo_id = generate_id(); +const block = ( { generateId } ) => { + const fooId = generateId(); return de.object( { block: { - foo: block_foo( { + foo: blockFoo.extend( { options: { // Выдаем блоку id-шник. // - id: foo_id, + id: fooId, }, } ), - bar: block_bar( { + bar: blockBar.extend( { options: { - // Ждать, пока успешно завершит свою работу блок с id равным foo_id. + // Ждать, пока успешно завершит свою работу блок с id равным fooId. // - deps: foo_id, + deps: fooId, }, } ), }, @@ -37,36 +37,36 @@ const block = ( { generate_id } ) => { }; ``` -Здесь сперва запустится `block_foo`, если он успешно отработает, то затем запустится `block_bar`. -Если `block_foo` завершится ошибкой, то и `block_bar` завершится ошибкой `de.ERROR_ID.DEPS_ERROR`. +Здесь сперва запустится `blockFoo`, если он успешно отработает, то затем запустится `blockBar`. +Если `blockFoo` завершится ошибкой, то и `blockBar` завершится ошибкой `de.ERROR_ID.DEPS_ERROR`. ## Блок может зависить от нескольких блоков ```js -const block = ( { generate_id } ) => { - const foo_id = generate_id(); - const bar_id = generate_id(); +const block = ( { generateId } ) => { + const fooId = generateId(); + const barId = generateId(); return de.object( { block: { - foo: block_foo( { + foo: blockFoo.extend( { options: { - id: foo_id, + id: fooId, }, } ), - bar: block_bar( { + bar: blockBar.extend( { options: { - id: bar_id, + id: barId, }, } ), - quu: block_quu( { + quu: blockQuu.extend( { options: { - // Ждем выполнения и блока block_foo, и блока block_bar. + // Ждем выполнения и блока blockFoo, и блока blockBar. // - deps: [ foo_id, bar_id ], + deps: [ fooId, barId ], }, } ), }, @@ -75,9 +75,9 @@ const block = ( { generate_id } ) => { }; ``` -Аналогично. Сперва запускаются параллельно блоки `block_foo` и `block_bar`. -Когда оба они успешно отработали, запустится блок `block_quu`. -Если хотя бы один из них отработает с ошибкой, `block_quu` опять таки сразу завершится с ошибкой `de.ERROR_ID.DEPS_ERROR`. +Аналогично. Сперва запускаются параллельно блоки `blockFoo` и `blockBar`. +Когда оба они успешно отработали, запустится блок `blockQuu`. +Если хотя бы один из них отработает с ошибкой, `blockQuu` опять таки сразу завершится с ошибкой `de.ERROR_ID.DEPS_ERROR`. ## Как использовать результат зависимостей @@ -86,34 +86,34 @@ const block = ( { generate_id } ) => { Но чаще всего, нам нужно результат одного блока использовать для запуска другого блока. ```js -const block = ( { generate_id } ) => { - const foo_id = generate_id(); +const block = ( { generateId } ) => { + const fooId = generateId(); return de.object( { block: { - foo: block_foo( { + foo: blockFoo.extend(( { options: { - id: foo_id, + id: fooId, }, } ), - bar: block_bar( { + bar: blockBar.extend(( { options: { - deps: foo_id, + deps: fooId, params: ( { deps } ) => { // В deps приходят результаты работы всех блоков, // от которых зависит блок. // - // Достаем результат block_foo. + // Достаем результат blockFoo. // - const foo_result = deps[ foo_id ]; + const fooResult = deps[ fooId ]; - // Используем значение из foo_result в качестве - // параметра запроса блока block_bar. + // Используем значение из fooResult в качестве + // параметра запроса блока blockBar. // return { - foo_id: foo_result.id, + fooId: fooResult.id, }; }, }, @@ -128,29 +128,29 @@ const block = ( { generate_id } ) => { будет приходить объект с результатами этих зависимостей. -## `generate_id()` +## `generateId()` Чтобы установить связь между блоками, нужно использовать какой-то id-шник в `options.id` и `options.deps`. -И этот id-шник не может быть чем-либо, кроме результата работы `generate_id`. +И этот id-шник не может быть чем-либо, кроме результата работы `generateId`. Эта функция приходит в обертку-замыкание или в `de.func`: ```js -const block = ( { generate_id } ) => { - const id = generate_id(); +const block = ( { generateId } ) => { + const id = generateId(); ... }; const block = de.func( { - block: ( { generate_id } ) => { - const id = generate_id(); + block: ( { generateId } ) => { + const id = generateId(); ... }, } ); ``` -id-шники, сгенерированные при помощи `generate_id`, действительны только внутри соответствующего замыкания. +id-шники, сгенерированные при помощи `generateId`, действительны только внутри соответствующего замыкания. И при попытке использовать какое-либо другое значение в качестве id, блок завершится с ошибкой `de.ERROR_ID.INVALID_DEPS_ID`. @@ -170,18 +170,18 @@ id-шники, сгенерированные при помощи `generate_id`, Например: ```js -const block = ( { generate_id } ) => { - const foo_id = generate_id(); +const block = ( { generateId } ) => { + const fooId = generateId(); return de.object( { block: { - foo: block_foo, + foo: blockFoo, - bar: block_bar( { + bar: blockBar.extend(( { options: { // А с таким id никто не запущен! // - deps: foo_id, + deps: fooId, }, } ), }, @@ -194,41 +194,41 @@ const block = ( { generate_id } ) => { * Будет создан и запущен `de.object` - * У этого `de.object` есть два подблока, один — `block_foo` — можно запустить сразу (он не имеет никаких зависимостей), - а `block_bar` ждет окончания работы блока с id `foo_id`. + * У этого `de.object` есть два подблока, один — `blockFoo` — можно запустить сразу (он не имеет никаких зависимостей), + а `blockBar` ждет окончания работы блока с id `fooId`. - * Блок `block_foo` завершит свою работу (неважно, ошибкой или нет). + * Блок `blockFoo` завершит свою работу (неважно, ошибкой или нет). - * Возникнет ситуация, когда ни один блок не активен, но зависимости блока `block_bar` все еще не сошлись. + * Возникнет ситуация, когда ни один блок не активен, но зависимости блока `blockBar` все еще не сошлись. - * Блок `block_bar` упадет с ошибкой `de.ERROR_ID.DEPS_NOT_RESOLVED`. + * Блок `blockBar` упадет с ошибкой `de.ERROR_ID.DEPS_NOT_RESOLVED`. -При этом в целом такая схема, когда на момент старта `block_bar` никакой блок с id `foo_id` не запущен, нормальна. -Могло быть так, что `block_foo` запустит блок, который будет иметь таки `foo_id` и тогда процесс сойдется +При этом в целом такая схема, когда на момент старта `blockBar` никакой блок с id `fooId` не запущен, нормальна. +Могло быть так, что `blockFoo` запустит блок, который будет иметь таки `fooId` и тогда процесс сойдется (или еще сложнее, запустит блок, который запустит блок, который запустит блок ..., который будет иметь нужный id). ```js -const block = ( { generate_id } ) => { - const foo_id = generate_id(); +const block = ( { generateId } ) => { + const fooId = generateId(); return de.object( { block: { foo: de.func( { block: async () => { - await do_something(); + await doSomething(); return de.block( { options: { - id: foo_id, + id: fooId, }, } ); }, } ), - bar: block_bar( { + bar: blockBar.extend(( { options: { - deps: foo_id, + deps: fooId, }, } ), }, diff --git a/docs/first_block.md b/docs/first_block.md index 3654925..996ca30 100644 --- a/docs/first_block.md +++ b/docs/first_block.md @@ -3,33 +3,33 @@ Иногда нужно выполнить несколько блоков последовательно, до тех пор, пока какой-нибудь из них не отработает успешно. ```js -const block_foo = require( '.../blocks/foo' ); -const block_bar = require( '.../blocks/bar' ); -const block_quu = require( '.../blocks/quu' ); +import blockFoo from '.../blocks/foo'; +import blockBar from '.../blocks/bar'; +import blockQuu from '.../blocks/quu'; const block = de.first( { block: [ - block_foo, - block_bar, - block_quu, + blockFoo, + blockBar, + blockQuu, ], } ); ``` -В этом примере сперва выполняется `block_foo`. Если он отрабатыет успешно то выполнение `de.first` завершается и его результатом будет результат работы `block_foo`. -Если `block_foo` завершается ошибкой, то дальше выполняется `block_bar` и т.д. +В этом примере сперва выполняется `blockFoo`. Если он отрабатыет успешно то выполнение `de.first` завершается и его результатом будет результат работы `blockFoo`. +Если `blockFoo` завершается ошибкой, то дальше выполняется `blockBar` и т.д. Если все блоки завершились ошибкой, то `de.first` завершается ошибкой вида: ```js de.error( { id: 'ALL_BLOCKS_FAILED', - reason: [ error_foo, error_bar, error_quu ], + reason: [ errorFoo, errorBar, errorQuu ], } ) ``` В поле `reason` будет массив с ошибками соответствующих блоков. Кроме того, для каждого блока в поле `deps.prev` будет приходить массив с ошибками всех предыдущих блоков. -Т.е. для `block_bar` будет массив из одного элемента — ошибки `block_foo`, -а для `block_quu` это будет массив из ошибки `block_foo` и ошибки `block_bar`. +Т.е. для `blockBar` будет массив из одного элемента — ошибки `blockFoo`, +а для `blockQuu` это будет массив из ошибки `blockFoo` и ошибки `blockBar`. diff --git a/docs/function_block.md b/docs/function_block.md index 2f9718e..b00cef8 100644 --- a/docs/function_block.md +++ b/docs/function_block.md @@ -31,7 +31,7 @@ const block = de.func( { ```js -const another_block = require( '...' ); +const anotherBlock = require( '...' ); const block = de.func( { @@ -49,7 +49,7 @@ const block = de.func( { } if ( params.foo ) { - return another_block; + return anotherBlock; } return new Promise( ( resolve ) => { @@ -68,27 +68,27 @@ const block = de.func( { ```js const block = ( { params } ) => { - return ( params.foo ) ? block_foo : block_bar; + return ( params.foo ) ? blockFoo : blockBar; }; ``` -## `generate_id` +## `generateId` И в `de.func`, и в сокращенную версию блока приходит -функция `generate_id`, чтобы устанавливать [зависимости](./deps.md) между блоками. +функция `generateId`, чтобы устанавливать [зависимости](./deps.md) между блоками. ```js const block = de.func( { - block: ( { params, generate_id } ) => { - const id = generate_id(); + block: ( { params, generateId } ) => { + const id = generateId(); ... }, } ); -const block = ( { params, generate_id } ) => { - const id = generate_id(); +const block = ( { params, generateId } ) => { + const id = generateId(); ... }; diff --git a/docs/http_block.md b/docs/http_block.md index 799bd5b..a9de787 100644 --- a/docs/http_block.md +++ b/docs/http_block.md @@ -3,7 +3,7 @@ Этот блок, как следует из названия, делает http-запросы. ```js -const de = require( 'descript' ); +import * as de from 'descript'; const block = de.http( { @@ -295,23 +295,23 @@ block: { Полученный агент будет закэширован: ```js -const agent_options = { +const agentOptions = { keepAlive: true, maxSockets: 16, }; -const block_1 = de.http( { +const block1 = de.http( { ... - agent: agent_options, + agent: agentOptions, } ); -const block_2 = de.http( { +const block2 = de.http( { ... - agent: agent_options, + agent: agentOptions, } ); ``` -Для `block_1` и `block_2` будет использован один и тот же агент. +Для `block1` и `block2` будет использован один и тот же агент. ## `timeout` @@ -333,8 +333,8 @@ block: { ```js block: { timeout: 1000, - max_retries: 2, - retry_timeout: 500, + maxRetries: 2, + retryTimeout: 500, }, ``` @@ -351,54 +351,56 @@ options: { ``` -## `is_json` +## `isJson` Если в ответе на http-запрос будет заголовок `content-type: application/json`, то descript попытается автоматически распарсить ответ при помощи `JSON.parse`. Но если так вышло, что мы точно знаем, что в ответе должен вернуться json, а заголовок по каким-то причинам отсутствует, -можно выставить флаг `is_json: true`: +можно выставить флаг `isJson: true`: ```js block: { - is_json: true, + isJson: true, }, ``` В этом случае, ответ всегда будет парситься через `JSON.parse`. -## `is_error` +## `isError` По-дефолту, если статус ответа больше или равен 400, то это считается ошибкой. Это поведение можно переопределить. Например, иногда полезно не считать такие ответы ошибкой. Или же наоборот, ответ с кодом 200, но каким-то специальным заголовком, считать ошибкой. -Можно переопределить параметр `is_error`. -Дефолтный `is_error` доступен как `de.request.DEFAULT_OPTIONS.is_error`. +Можно переопределить параметр `isError`. +Дефолтный `isError` доступен как `de.request.DEFAULT_OPTIONS.isError`. Не считать вообще ничего ошибкой: ```js block: { - is_error: () => false, + isError: () => false, }, ``` Считать ответ со кодом ответа 200 и специальным заголовком ошибкой: ```js -const de = require( 'descript' ); +impoer * as de from 'descript'; const block = de.http( { - is_error: ( error, request_options ) => { - if ( error.error.status_code === 200 && error.error.headers[ 'x-foo' ] === 'FOO' ) { - return true; - } - - // Все остальное отправляем в дефолтный is_error. - // - return de.request.DEFAULT_OPTIONS.is_error( error, request_options ); + block: { + isError: ( error, requestOptions ) => { + if ( error.error.statusCode === 200 && error.error.headers[ 'x-foo' ] === 'FOO' ) { + return true; + } + + // Все остальное отправляем в дефолтный isError. + // + return de.request.DEFAULT_OPTIONS.isError( error, requestOptions ); + }, }, } ); ``` @@ -409,41 +411,41 @@ const block = de.http( { Если мы сделали запрос и получили ошибку, иногда можно и нужно сделать повторный запрос (или несколько). Перезапросы настраиваются тремя параметрами: - * `is_retry_allowed` — функция (похожая на `is_error`), которая сообщает, можно ли этот конкретный запрос ретраить. - * `max_retries` — сколько можно сделать ретраев (по-дефолту 0). - * `retry_timeout` — пауза между ретраями в миллисекундах (по-дефолту 100). + * `isRetryAllowed` — функция (похожая на `isError`), которая сообщает, можно ли этот конкретный запрос ретраить. + * `maxRetries` — сколько можно сделать ретраев (по-дефолту 0). + * `retryTimeout` — пауза между ретраями в миллисекундах (по-дефолту 100). ```js block: { - is_retry_allowed: ( error, request_options ) => { - if ( error.error.status_code === 404 ) { + isRetryAllowed: ( error, requestOptions ) => { + if ( error.error.statusCode === 404 ) { // У нас странный бэкенд. return true; } - return de.request.DEFAULT_OPTIONS.is_retry_allowed( error, request_options ); + return de.request.DEFAULT_OPTIONS.isRetryAllowed( error, requestOptions ); }, // Т.е. всего будет сделано максимум 3 запроса, прежде чем мы окончательно сдадимся // и завершим работу с ошибкой. // - max_retries: 2, + maxRetries: 2, - retry_timeout: 500, + retryTimeout: 500, }, ``` -## `prepare_request_options` +## `prepareRequestOptions` Чорная магия. ```js block: { - prepare_request_options: ( request_options ) => { - request_options.headers[ 'x-foo' ] = 'FOO'; + prepareRequestOptions: ( requestOptions ) => { + requestOptions.headers[ 'x-foo' ] = 'FOO'; - return request_options; + return requestOptions; }, }, ``` @@ -474,7 +476,7 @@ options: { ```js const parent = de.http( ... ); -const child = parent( { +const child = parent.extend( { block: { ... }, @@ -498,7 +500,7 @@ const parent = de.http( { }, } ); -const child = parent( { +const child = parent.extend( { block: { // Все эти параметры перетирают соответствующие значения параметров parent. // @@ -515,7 +517,7 @@ const child = parent( { `query` и `headers` не перезатирают значения родителя, но дополняют: ```js -const child = parent( { +const child = parent.extend( { block: { // Добавляем к тому, что отправляет в query родитель еще один параметр. diff --git a/docs/inheritance.md b/docs/inheritance.md index 4095caf..bdbc868 100644 --- a/docs/inheritance.md +++ b/docs/inheritance.md @@ -2,9 +2,9 @@ На самом деле это не совсем наследование, а скорее расширение и дополнение функциональности блока. - const parent_block = de.block( ... ); + const parentBlock = de.block( ... ); - const child_block = parent_block( { + const childBlock = parentBlock.extend( { block: { // Модификации блока. }, diff --git a/docs/logs.md b/docs/logs.md index 98a853e..2e1f63a 100644 --- a/docs/logs.md +++ b/docs/logs.md @@ -5,26 +5,26 @@ Что такое логгер. Это объект с одним методом `log`, в который приходят события: ```js -const de = require( 'descript' ); +import * as de from 'descript'; const logger = { log: function( event, context ) { switch ( event.type ) { - case de.Logger.EVENT.REQUEST_START: { - const { request_options } = event; + case de.EVENT.REQUEST_START: { + const { requestOptions } = event; ... break; } - case de.Logger.EVENT.REQUEST_SUCCESS: { - const { request_options, result, timestamps } = event; + case de.EVENT.REQUEST_SUCCESS: { + const { requestOptions, result, timestamps } = event; ... break; } - case de.Logger.EVENT.REQUEST_ERROR: { - const { request_options, error, timestamps } = event; + case de.EVENT.REQUEST_ERROR: { + const { requestOptions, error, timestamps } = event; ... break; } @@ -34,8 +34,8 @@ const logger = { }; ``` -В поле `event.request_options` содержится много всего, описывающего http-запрос, который мы логируем. -В частности, в `event.request_options.http_options` содержится объект, +В поле `event.requestOptions` содержится много всего, описывающего http-запрос, который мы логируем. +В частности, в `event.requestOptions.http_options` содержится объект, который был отправлен в нодовский метод `http.request` (или `https.request`). diff --git a/docs/object_block.md b/docs/object_block.md index f37c790..896dec5 100644 --- a/docs/object_block.md +++ b/docs/object_block.md @@ -4,30 +4,30 @@ из других блоков: ```js -const block_foo = require( '.../blocks/foo' ); -const block_bar = require( '.../blocks/bar' ); +import blockFoo from '.../blocks/foo'; +import blockBar from '.../blocks/bar'; ... const block = de.object( { block: { - foo: block_foo, - bar: block_bar, + foo: blockFoo, + bar: blockBar, ... }, } ); ``` -Когда мы запускаем `de.object`, запускаются все его непосредственные подблоки (`block_foo`, `block_bar`, ...) +Когда мы запускаем `de.object`, запускаются все его непосредственные подблоки (`blockFoo`, `blockBar`, ...) и из их результатов составляется результат `de.object`'а: ```js const result = { - // Результат работы block_foo - foo: result_foo, - // Результат работы block_bar - bar: result_bar, + // Результат работы blockFoo + foo: resultFoo, + // Результат работы blockBar + bar: resultBar, ... }; ``` @@ -42,10 +42,10 @@ const result = { ```js const result = { - // Ошибка block_foo - foo: error_foo, - // Результат работы block_bar - bar: result_bar, + // Ошибка blockFoo + foo: errorFoo, + // Результат работы blockBar + bar: resultBar, ... }; ``` @@ -60,12 +60,12 @@ const block = de.object( { block: { // Если этот блок зафейлится, то и весь de.object так же зафейлится. - foo: block_foo( { + foo: blockFoo( { options: { required: true, }, } ), - bar: block_bar, + bar: blockBar, ... }, @@ -79,10 +79,10 @@ const block = de.object( { ```js const block = de.object( { block: { - foo: block_foo, + foo: blockFoo, bar: de.object( { block: { - quu: block_quu, + quu: blockQuu, ... }, } ), @@ -95,9 +95,9 @@ const block = de.object( { ```js const result = { - foo: result_foo, + foo: resultFoo, bar: { - quu: result_quu, + quu: resultQuu, ... }, ... diff --git a/docs/options.md b/docs/options.md index 867762c..a888d97 100644 --- a/docs/options.md +++ b/docs/options.md @@ -30,8 +30,8 @@ de.block( { name: 'my_api.my_method', // Зависимости между блоками. - id: some_id, - deps: [ some_id_1, some_id_2, ... ], + id: someId, + deps: [ someId1, someId2, ... ], // Возможность вычислить новые параметры для блока. params: ..., diff --git a/docs/options_after.md b/docs/options_after.md index e72ae08..62bb318 100644 --- a/docs/options_after.md +++ b/docs/options_after.md @@ -57,7 +57,7 @@ options: { options: { after: ( { result } ) => { if ( !result.foo ) { - return another_block; + return anotherBlock; } return result; @@ -115,7 +115,7 @@ const parent = de.block( { }, } ); -const child = parent( { +const child = parent.extend( { options: { // Если родительский after отработал без ошибок и вернул что-то, // то это что-то приходит в after потомка в качестве результата. diff --git a/docs/options_before.md b/docs/options_before.md index 2c40a39..e6b29c5 100644 --- a/docs/options_before.md +++ b/docs/options_before.md @@ -157,7 +157,7 @@ const parent = de.block( { }, } ); -const child = parent( { +const child = parent.extend( { options: { // Сперва вызовется этот before. // Если он вернет что-то или бросит ошибку, diff --git a/docs/options_params.md b/docs/options_params.md index ffc469f..8629dcc 100644 --- a/docs/options_params.md +++ b/docs/options_params.md @@ -3,16 +3,16 @@ Позволяет изменить переданные блоку сверху параметры: ```js -const orig_params = { +const origParams = { foo: 42, }; const block = de.block( { options: { - // Вот сюда в params придет orig_params. + // Вот сюда в params придет origParams. // params: ( { params } ) => { - console.log( params, params === orig_params ); + console.log( params, params === origParams ); // { foo: 42 }, true return { @@ -34,7 +34,7 @@ const block = de.block( { // Запускаем блок с какими-то параметрами: // const result = await de.run( block, { - params: orig_params, + params: origParams, } ); ``` @@ -80,9 +80,9 @@ const parent = de.block( { }, } ); -const child = parent( { +const child = parent.extend( { options: { - // Сперва вызовется эта функция, в params придет orig_params. + // Сперва вызовется эта функция, в params придет origParams. // params: ( { params } ) => { // И дальше везде в работе блока будет использоваться этот объект. @@ -96,9 +96,9 @@ const child = parent( { }, } ); -const orig_params = { +const origParams = { foo: 42, }; -de.run( child, { params: orig_params } ); +de.run( child, { params: origParams } ); ``` diff --git a/docs/options_required.md b/docs/options_required.md index 55bc521..bc48c11 100644 --- a/docs/options_required.md +++ b/docs/options_required.md @@ -6,9 +6,9 @@ const block = de.object( { block: { - foo: block_foo, + foo: blockFoo, - bar: block_bar( { + bar: blockBar.extend( { options: { required: true, }, @@ -18,7 +18,7 @@ const block = de.object( { } ); ``` -Если в процессе выполнения этого блока `block_foo` завершится ошибкой, а `block_bar` нет, +Если в процессе выполнения этого блока `blockFoo` завершится ошибкой, а `blockBar` нет, то результатом работы `block` будет что-то типа: ```js @@ -35,9 +35,9 @@ const block = de.object( { } ``` -Т.е. несмотря на ошибку в `block_foo`, сам блок `block` завершится удачно. +Т.е. несмотря на ошибку в `blockFoo`, сам блок `block` завершится удачно. -Если же теперь наоборот, `block_foo` завершается успешно, а `block_bar` ошибкой. +Если же теперь наоборот, `blockFoo` завершается успешно, а `blockBar` ошибкой. В этом случае, блок `block` тоже завершится вот такой ошибкой: ```js diff --git a/docs/pipe_block.md b/docs/pipe_block.md index c408048..b2f7a14 100644 --- a/docs/pipe_block.md +++ b/docs/pipe_block.md @@ -3,35 +3,35 @@ Иногда нужно выполнить несколько блоков последовательно, передавая результат предыдущего блока в следующий. ```js -const block_foo = require( '.../blocks/foo' ); -const block_bar = require( '.../blocks/bar' ); -const block_quu = require( '.../blocks/quu' ); +import blockFoo from '.../blocks/foo'; +import blockBar from '.../blocks/bar'; +import blockQuu from '.../blocks/quu'; const block = de.pipe( { block: [ - block_foo, + blockFoo, - block_bar( { + blockBar.extend( { options: { params: ( { deps } ) => { - const foo_result = deps.prev[ 0 ]; + const fooResult = deps.prev[ 0 ]; return { - foo_id: foo_result.id, + fooId: fooResult.id, }; }, }, } ), - block_quu( { + blockQuu.extend( { options: { params: ( { deps } ) => { - const foo_result = deps.prev[ 0 ]; - const bar_result = deps.prev[ 1 ]; + const fooResult = deps.prev[ 0 ]; + const barResult = deps.prev[ 1 ]; return { - foo_id: foo_result.id, - bar_id: bar_result.id, + fooId: fooResult.id, + barId: barResult.id, }; }, }, @@ -40,10 +40,10 @@ const block = de.pipe( { } ); ``` -В этом примере сперва выполняется `block_foo`, затем `block_bar`, затем `block_quu`. -Если все блоки успешно отработали, то результатом работы `de.pipe` будет результат из последнего в цепочке блока (`block_quu`). +В этом примере сперва выполняется `blockFoo`, затем `blockBar`, затем `blockQuu`. +Если все блоки успешно отработали, то результатом работы `de.pipe` будет результат из последнего в цепочке блока (`blockQuu`). Если на любом этапе произошла ошибка, то этой же ошибкой заканчивается выполнение всего `de.pipe`. Результаты выполнения предыдущих блоков доступны в `deps.prev`. Это массив. -Для `block_foo` он, очевидно, пустой, для `block_bar` он содержит один элемент — результат работы `block_foo`, -для `block_quu` он содержит два элемента — результат `block_foo` и результат `block_bar`. +Для `blockFoo` он, очевидно, пустой, для `blockBar` он содержит один элемент — результат работы `blockFoo`, +для `blockQuu` он содержит два элемента — результат `blockFoo` и результат `blockBar`. diff --git a/docs/run.md b/docs/run.md index 8729d48..3b14dd1 100644 --- a/docs/run.md +++ b/docs/run.md @@ -43,12 +43,12 @@ de.run( block, { Более-менее приближенный к реальности пример. ```js -const http_ = require( 'http' ); -const url_ = require( 'url' ); +import http_ from 'http'; +import url_ from 'url'; // Роутер. Превращает урл в пару { page_id, page_params }. // -const router = require( '.../router' ); +import router from '.../router'; // Словарь страничных блоков. // Каждому page_id соответствует какой-то дескриптовый блок. diff --git a/docs/typescript-examples/array.ts b/docs/typescript-examples/array.ts index 3668f39..b3babf1 100644 --- a/docs/typescript-examples/array.ts +++ b/docs/typescript-examples/array.ts @@ -1,91 +1,159 @@ -import {DescriptBlockParams} from '../../lib'; +/* eslint-disable no-console */ import * as de from '../../lib'; // --------------------------------------------------------------------------------------------------------------- // -interface Context { - is_mobile: boolean; -} - // --------------------------------------------------------------------------------------------------------------- // interface ParamsIn1 { - id_1: string; + id1: string; } -const block_1 = de.http( { +const block1 = de.http({ block: {}, options: { - params: ( { params }: { params: ParamsIn1 } ) => { + params: ({ params }: { params: ParamsIn1 }) => { return { - s1: params.id_1, + s1: params.id1, }; }, - after: ( { params } ) => { + after: ({ params }) => { return { + b1: 1, a: params.s1, }; }, }, -} ); +}); // --------------------------------------------------------------------------------------------------------------- // interface ParamsIn2 { - id_2: number; + id2: number; } -const block_2 = de.http( { +const block2 = de.http({ block: {}, options: { - params: ( { params }: { params: ParamsIn2 }) => { + + params: ({ params }: { params: ParamsIn2 }) => { return params; }, - after: ( { params } ) => { + after: ({ params }) => { return { - b: params.id_2, + p: params.id2, + b2: 2, }; }, }, -} ); +}); // --------------------------------------------------------------------------------------------------------------- // -const block_3 = de.array( { - block: [ block_1, block_2 ] as const, +const block3 = de.array({ + block: [ + block1, + block2, + de.func({ + block: () => 1, + options: {}, + }), + ] as const, options: { - after: ( { result } ) => { - return [ result[ 0 ].a, result[ 1 ].b ] as const; + params: ({ params }) => { + return params; + }, + after: ({ result }) => { + return [ 'a' in result[ 0 ] ? result[ 0 ].a : '', 'b2' in result[ 1 ] ? result[ 1 ].b2 : '' ] as const; }, }, -} ); +}); -de.run( block_3, { +de.run(block3, { params: { - id_1: '12345', - id_2: 67890, + id1: '12345', + id2: 67890, }, -} ) - .then( ( result ) => { - console.log( result[ 0 ], result[ 1 ] ); - } ); +}) + .then((result) => { + console.log(result[ 0 ], result[ 1 ]); + }); -const block_4 = block_3( { +const block4 = block3.extend({ options: { - after: ( { result } ) => { + after: ({ result }) => { return result[ 0 ]; }, }, -} ); +}); + +de.run(block4, { + params: { + id1: '12345', + id2: 67890, + }, +}) + .then((result) => { + console.log(result); + }); + + +const bfn1 = de.func({ + block: ({ params }: { params: { p1: number } }) => { + return { + b1: params.p1, + }; + }, + options: { + after: ({ result }) => { + return { + r: 1, + b1: result.b1, + }; + }, + }, +}); + + +const bfn2 = de.func({ + block: ({ params }: { params: { p2: string } }) => { + return { + b2: params.p2, + }; + }, + options: { + after: ({ result }) => { + return { + r2: 1, + b2: result.b2, + }; + }, + }, +}); + +const bfn3 = de.array({ + block: [ + bfn1, + bfn2, + ], + options: { + params: ({ params }) => { + return params; + }, + after: ({ result }) => { + return [ !de.isError(result[ 0 ]) ? result[ 0 ].b1 : '', !de.isError(result[ 1 ]) ? result[ 1 ].b2 : '' ]; + }, + }, +}); -de.run( block_4, { +de.run(bfn3, { params: { - id_1: '12345', - id_2: 67890, + p2: '12345', + p1: 67890, }, -} ) - .then( ( result ) => { - console.log( result ); - } ); +}) + .then((result) => { + console.log(result[ 0 ], result[ 1 ]); + }); diff --git a/docs/typescript-examples/error.ts b/docs/typescript-examples/error.ts index 2dcf5ec..32d3a8a 100644 --- a/docs/typescript-examples/error.ts +++ b/docs/typescript-examples/error.ts @@ -1,19 +1,18 @@ -import * as de from "../../lib"; -import {DescriptError} from "../../lib"; +import * as de from '../../lib'; +import type { DescriptHttpBlockResult } from '../../lib/types'; -interface ResultRaw { - result: string | DescriptError -} - -const block1 = de.http( { +const block1 = de.http({ block: {}, options: { - after: ( { result }: ResultRaw ): string => { - if (de.is_error(result)) { + //TODO как указать тип blockResult? + after: ({ result }: { result: DescriptHttpBlockResult }) => { + if (de.isError(result)) { return result.error.id; - } else{ - return result.slice(0, 10); + } else { + return result.result.slice(0, 10); } }, }, -} ); +}); + +block1; diff --git a/docs/typescript-examples/first.ts b/docs/typescript-examples/first.ts new file mode 100644 index 0000000..f0fc9c2 --- /dev/null +++ b/docs/typescript-examples/first.ts @@ -0,0 +1,168 @@ +/* eslint-disable no-console */ +import * as de from '../../lib'; + +// --------------------------------------------------------------------------------------------------------------- // + +// --------------------------------------------------------------------------------------------------------------- // + +interface ParamsIn1 { + id1: string; +} + +const block1 = de.http({ + block: {}, + options: { + params: ({ params }: { params: ParamsIn1 }) => { + return { + s1: params.id1, + }; + }, + + after: ({ params }) => { + return { + b1: 1, + a: params.s1, + }; + }, + }, +}); + +// --------------------------------------------------------------------------------------------------------------- // + +interface ParamsIn2 { + id2: number; +} + +const block2 = de.http({ + block: {}, + options: { + + params: ({ params }: { params: ParamsIn2 }) => { + return params; + }, + + after: ({ params }) => { + return { + p: params.id2, + b2: '2', + }; + }, + }, +}); + +// --------------------------------------------------------------------------------------------------------------- // + +const block3 = de.first({ + block: [ + block1, + block2, + de.func({ + block: () => 1, + options: {}, + }), + ] as const, + options: { + params: ({ params }) => { + return params; + }, + after: ({ result }) => { + if (typeof result === 'number') { + return result; + } else if ('b1' in result) { + return result.b1; + } else if ('b2' in result) { + return result.b2; + } + }, + }, +}); + +de.run(block3, { + params: { + id1: '12345', + id2: 67890, + }, +}) + .then((result) => { + console.log(result); + }); + +const block4 = block3.extend({ + options: { + after: ({ result }) => { + return result; + }, + }, +}); + +de.run(block4, { + params: { + id1: '12345', + id2: 67890, + }, +}) + .then((result) => { + console.log(result); + }); + + +const bfn1 = de.func({ + block: ({ params }: { params: { p1: number } }) => { + return { + b1: params.p1, + }; + }, + options: { + after: ({ result }) => { + return { + r: 1, + b1: result.b1, + }; + }, + }, +}); + + +const bfn2 = de.func({ + block: ({ params }: { params: { p2: string } }) => { + return { + b2: params.p2, + }; + }, + options: { + after: ({ result }) => { + return { + r2: 1, + b2: result.b2, + }; + }, + }, +}); + +const bfn3 = de.first({ + block: [ + bfn1, + bfn2, + ], + options: { + params: ({ params }) => { + return params; + }, + after: ({ result }) => { + if (!de.isError(result)) { + return ('b1' in result ? result.b1 : result.b2); + } + return result; + }, + }, +}); + +de.run(bfn3, { + params: { + p2: '12345', + p1: 67890, + }, +}) + .then((result) => { + console.log(result); + }); diff --git a/docs/typescript-examples/func.ts b/docs/typescript-examples/func.ts index 8d56034..4b8eb74 100644 --- a/docs/typescript-examples/func.ts +++ b/docs/typescript-examples/func.ts @@ -1,67 +1,51 @@ -import {DescriptBlockParams, DescriptBlockResult, DescriptBlockResultJSON} from '../../lib'; +/* eslint-disable no-console */ import * as de from '../../lib'; -// --------------------------------------------------------------------------------------------------------------- // - -interface Context { - is_mobile: boolean; -} - -// --------------------------------------------------------------------------------------------------------------- // - interface ParamsIn1 { id: string; } -interface ParamsOut { - s1: string; -} -type ResultIn = string; -interface ResultOut { - foo: string; -} - -const block_1 = de.func( { - block: ( { params, context, generate_id } ) => { +const block1 = de.func({ + block: ({ params }) => { // Здесь нужно вернуть тот же тип, что указан в after в качестве входящего результата. // Если after нет, то можно ничего не указывать, все выведется. return { - result: params.s1, + result: params.p1, }; }, options: { error: () => { return { - x: 1 - } + e: 1, + }; }, - params: ( { params }: { params: ParamsIn1 } ) => { + params: ({ params }: { params: ParamsIn1 }) => { return { - s1: params.id, + p1: params.id, }; }, - after: ( { params, result }) => { - console.log( params ); + after: ({ params, result }) => { + console.log(params); return { foo: result.result, - } + }; }, }, -} ); +}); -de.run( block_1, { +de.run(block1, { params: { id: 'foo', }, -} ) - .then( ( result ) => { - console.log( result ); +}) + .then((result) => { + console.log(result); }); -const block_2 = de.func({ - block: () => { - const result = { foo: 'bar' }; +const block2 = de.func({ + block: ({ params }: { params: { param: number } }) => { + const result = { foo: 'bar', param: params.param }; return Promise.resolve(result); }, options: { @@ -71,25 +55,63 @@ const block_2 = de.func({ }, }); -de.run( block_2, {} ) - .then( ( result ) => { - console.log( result ); +de.run(block2, { + params: { + param: 1, + }, +}) + .then((result) => { + console.log(result); }); -const block3 = block_2({ +const block3 = block2.extend({ options: { after: ({ result }) => { console.log(result); return { res: result, - } + }; }, - } + }, +}); + + +de.run(block3, { + params: { + param: 2, + }, }) + .then((result) => { + console.log(result); + }); + + +const block4 = de.func({ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + block: ({ params }) => { + //TODO не выводится params. + // eslint-disable-next-line @typescript-eslint/no-unused-vars + //const x = params.id; + return block1; + }, + options: { + after: ({ result, params }) => { + console.log(result); + console.log(params); + return { + res: result, + }; + }, + }, +}); -de.run( block3, {} ) - .then( ( result ) => { - console.log( result ); +de.run(block4, { + params: { + id: '1', + }, +}) + .then((result) => { + console.log(result); }); diff --git a/docs/typescript-examples/http.ts b/docs/typescript-examples/http.ts index 42f1cf7..4835b59 100644 --- a/docs/typescript-examples/http.ts +++ b/docs/typescript-examples/http.ts @@ -1,55 +1,46 @@ +/* eslint-disable no-console */ + import * as de from '../../lib'; -import {DescriptBlockParams, DescriptBlockResult, DescriptBlockResultJSON, DescriptRequestOptions} from '../../lib'; +import type { DescriptHttpBlockResult } from '../../lib/types'; +import { DEFAULT_OPTIONS } from '../../lib/request'; // --------------------------------------------------------------------------------------------------------------- // -interface Context { - is_mobile: boolean; -} - interface ParamsIn1 { - id_1: string; -} - -interface ParamsOut1 { - s1: string; + id1: string; } interface ResultIn1 { foo: string; } -interface ResultOut1 { - a: string; - b: string; -} -const block1 = de.http( { +const block1 = de.http({ block: { - parse_body({ body, headers}) { + parseBody({ body, headers }) { if (!body) { return null; } - if (headers['content-type'].startsWith('application/json')) { + if (headers['content-type']?.startsWith('application/json')) { return JSON.parse(body.toString('utf-8')); } else { return body; } }, - is_error: (error, request_options) => { - const statusCode = error.error.status_code; + isError: (error) => { + const statusCode = error.error.statusCode; if (statusCode && statusCode >= 400 && statusCode <= 499) { return false; } - return de.request.DEFAULT_OPTIONS.is_error(error, request_options); + return DEFAULT_OPTIONS.isError?.(error); }, - is_retry_allowed: (error, request_options) => { - const method = request_options.http_options.method; - if ( method === 'POST' ) { + isRetryAllowed: (error, requestOptions) => { + const method = requestOptions.httpOptions.method; + if (method === 'POST') { const id = error.error.id; - const statusCode = error.error.status_code; + const statusCode = error.error.statusCode; if ( id === de.ERROR_ID.TCP_CONNECTION_TIMEOUT || id === de.ERROR_ID.REQUEST_TIMEOUT || @@ -60,84 +51,79 @@ const block1 = de.http( { return true; } - return de.request.DEFAULT_OPTIONS.is_retry_allowed(error, request_options); + return DEFAULT_OPTIONS.isRetryAllowed?.(error, requestOptions); }, }, options: ({ before: () => { - return { - d: 1 - } + //TODO возврат результата и типизация в after + // return { + // d: 1, + // }; }, error: () => { return { - x: 's' - } + x: 's', + }; }, - params: ( { params }: { params: ParamsIn1 }) => { + params: ({ params }: { params: ParamsIn1 }) => { return { - s1: params.id_1, + s1: params.id1, }; }, - after: ( { params, result }: { params: { s1: ParamsIn1['id_1']}; result: DescriptBlockResultJSON } ) => { + //TODO автовыведение ResultIn1 + after: ({ params, result }: { params: { s1: ParamsIn1['id1']}; result: DescriptHttpBlockResult }) => { return { a: params.s1, b: result.result.foo, }; }, - }) -} ); + }), +}); -de.run( block1, { +de.run(block1, { params: { - id_1: '12345', + id1: '12345', }, -} ) - .then( ( result ) => { - console.log( result ); - } ); +}) + .then((result) => { + console.log(result); + }); interface ParamsIn2 { - id_2: string; -} - -interface ParamsOut2 extends ParamsIn2, ParamsOut1 { - s2: string; -} - -interface ResultOut2 extends ResultOut1 { - c: string; + id2: string; } -//TODO автопроброс входных параметров предыдущих? -const block2 = block1, DescriptBlockResult>( { - block: {}, +const block2 = block1.extend({ options: ({ - params: ( { params }) => { + params: ({ params }: { params: ParamsIn2 & ParamsIn1}) => { return { - ...params, - s2: params.s1, + id1: params.id1, + s2: params.id2, }; }, - after: ( { params, result } ) => { + + after: ({ params, result }) => { return { ...result, - a: params.s1, - c: result.b, + a: params.s2, + c: 'b' in result ? result.b : result.x, }; }, - }) + }), }); -de.run( block2, { +de.run(block2, { + //TODO что за undefined? params: { - id_1: '12345', - id_2: '12345', - } -} ) - .then( ( result ) => { - console.log( result.c ); - } ); + id1: '12345', + id2: '12345', + + }, +}) + .then((result) => { + console.log(result.c); + }); diff --git a/docs/typescript-examples/object.ts b/docs/typescript-examples/object.ts index 2199a8f..d1818d3 100644 --- a/docs/typescript-examples/object.ts +++ b/docs/typescript-examples/object.ts @@ -1,7 +1,7 @@ -import { - inferBlockTypes -} from '../../lib'; +/* eslint-disable no-console */ + import * as de from '../../lib'; +import type { DescriptHttpBlockResult, InferParamsInFromBlock } from '../../lib/types'; // --------------------------------------------------------------------------------------------------------------- // @@ -11,14 +11,14 @@ interface Context { // --------------------------------------------------------------------------------------------------------------- // export interface CreateCardRequest { - added_manually?: boolean; - added_by_identifier?: string; + addedManually?: boolean; + addedByIdentifier?: string; // card: Card; card: Record; } interface ParamsIn1 { - id_1: string; + id1: string; payload: CreateCardRequest; } interface ParamsOut1 { @@ -28,25 +28,25 @@ interface Result1 { r: number; } -const block_1 = de.http( { +const block1 = de.http({ block: { body: ({ params }) => params.payload, }, options: { - params: ( { params }: { params: ParamsIn1, context: Context } ) => { + params: ({ params }: { params: ParamsIn1; context?: Context }) => { return { - s1: params.id_1, - payload: params.payload + s1: params.id1, + payload: params.payload, }; }, - after: ( { params, context, result }: { params: ParamsOut1, context: Context, result: Result1 } ) => { + after: ({ params, result }: { params: ParamsOut1; result: DescriptHttpBlockResult }) => { const a = { - a: 1, + a: result.result.r, }; const b = { b: params.s1, - } + }; if (params.s1 === 'lol') { return a; @@ -55,102 +55,106 @@ const block_1 = de.http( { return b; }, }, -} ); +}); -de.run( block_1, { +de.run(block1, { params: { - id_1: '67890', + id1: '67890', payload: { - card: {} - } + card: {}, + }, }, -} ) - .then( ( result ) => { - console.log( result ); +}) + .then((result) => { + console.log(result); return { foo: 'a' in result ? result.a : result.b, bar: undefined, }; - } ); + }); // --------------------------------------------------------------------------------------------------------------- // interface ParamsIn2 { - id_2: number; -} - -interface ResultOut2 { - b: string; + id2: number; } -const block_2 = de.http( { +const block2 = de.http({ block: {}, options: { - params: ( { params }: {params: ParamsIn2} ) => { + params: ({ params }: {params: ParamsIn2}) => { return params; }, - after: ( { params, result } ) => { + after: ({ params }) => { return { - b: String(params.id_2), + b: String(params.id2), }; }, }, -} ); +}); -const block_2_func = de.func({ - block: (x) => block_2, +const block2Func = de.func({ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + block: ({ params }: { params: InferParamsInFromBlock & { p1: number } }) => block2, + //block: () => block2, options: { - after: ({result}) => { + after: ({ result }) => { console.log(result.b); return { b: result.b + '2', - c: 1 + c: 1, }; - } - } + }, + }, }); -de.run( block_2_func, { +de.run(block2Func, { params: { - id_2: 67890, + id1: '67890', + p1: 1, + payload: { + card: {}, + }, }, -} ) - .then( ( result ) => { - console.log( result ); +}) + .then((result) => { + console.log(result); return { foo: result.c, bar: result.b, }; - } ); + }); // --------------------------------------------------------------------------------------------------------------- // -const block_3 = de.object( { +const block3 = de.object({ block: { - foo: inferBlockTypes(de.http( { + foo: de.http({ block: {}, options: { - params: ( { params }: { params: ParamsIn1, context: Context } ) => { + params: ({ params }: { params: ParamsIn2; context?: Context }) => { return { - s1: params.id_1, + s1: params.id2, }; }, - after: ( { params, context } ) => { + after: ({ params }) => { return { a: params.s1, }; }, }, - } )), - bar: block_2_func, - }, + }), + bar: block2Func, + }, options: { - params: ({params}) => params, - after: ( { result, params } ) => { + params: ({ params }) => params, + after: ({ result, params }) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const x = params.id2; return { foo: { ...result.foo, @@ -160,80 +164,97 @@ const block_3 = de.object( { }; }, }, -} ); +}); -de.run( block_3, { +de.run(block3, { params: { - id_1: '12345', - id_2: 67890, + id1: '12345', + id2: 67890, + p1: 1, payload: { - card: {} - } + card: {}, + }, }, -} ) - .then( ( result ) => { - console.log( result.foo.a, result.bar.b ); +}) + .then((result) => { return { - foo: result.foo.a, - bar: result.bar.b, + foo: 'a' in result.foo ? result.foo.a : result.foo.error, + bar: 'b' in result.bar ? result.bar.b : result.bar.error, }; - } ); + }); -const block_3_func = de.func( { +const block3Func = de.func({ block: () => { - return block_3; + return block3; }, options: { after: ({ result }) => { return { - foo: result.foo.a + '1', - bar: result.bar.b + '1', - } - } - } + foo: 'a' in result.foo ? result.foo.a + 1 : result.foo.error, + bar: 'b' in result.bar ? result.bar.b + 1 : result.bar.error, + }; + }, + }, }); -de.run( block_3_func, { +de.run(block3Func, { params: { - id_1: '12345', - id_2: 67890, + id1: '12345', + id2: 67890, payload: { - card: {} - } + card: {}, + }, }, -} ) - .then( ( result ) => { - console.log( result.foo, result.bar ); - } ); +}) + .then((result) => { + console.log(result.foo, result.bar); + }); -const block_4 = block_3( { +const block4 = block3.extend({ options: { - after: ( { result } ) => { - return result.foo.a + result.bar.b; + after: ({ result }) => { + return (('a' in result.foo ? result.foo.a : result.foo.error.id) || '') + + (('b' in result.bar ? result.bar.b : result.bar.error.id) || ''); }, }, -} ); +}); -const block_5 = block_3( { +const block5 = block3.extend({ options: { - error: ({ cancel, error }) => { + error: ({ error }) => { if (error.error) { throw error; } }, - after: ( { result } ) => result, + after: ({ result }) => result, }, -} ); +}); -de.run( block_4, { +de.run(block4, { params: { - id_1: '12345', - id_2: 67890, + id1: '12345', + id2: 67890, + p1: 1, payload: { - card: {} - } + card: {}, + }, + }, +}) + .then((result) => { + console.log(result); + }); + + +de.run(block5, { + params: { + id1: '12345', + id2: 67890, + p1: 1, + payload: { + card: {}, + }, }, -} ) - .then( ( result ) => { - console.log( result ); - } ); +}) + .then((result) => { + console.log(result); + }); diff --git a/docs/typescript-examples/options.ts b/docs/typescript-examples/options.ts index 6327bf3..e8fc855 100644 --- a/docs/typescript-examples/options.ts +++ b/docs/typescript-examples/options.ts @@ -1,10 +1,11 @@ -import {DescriptBlockParams, DescriptBlockResult, GetDescriptBlockParamsFnIn} from '../../lib'; +/* eslint-disable no-console */ + import * as de from '../../lib'; // --------------------------------------------------------------------------------------------------------------- // interface Context { - is_mobile: boolean; + isMobile: boolean; } // Это параметры, которые приходят в блок извне. @@ -13,19 +14,6 @@ interface ParamsIn { id: string; } -// Это вычисленные параметры, которые блок использует внутри. -// В колбэки before, after и т.д. будут приходить вот эти параметры. -// -interface ParamsOut { - foo: string; -} - -// Это необработанный результат. -// -interface ResultRaw { - a: string; -} - // --------------------------------------------------------------------------------------------------------------- // // Вариант 1. @@ -34,7 +22,7 @@ interface ResultRaw { // * Обрабатываем результат. // * Используем вычисленные параметры. -const block1 = de.http( { +const block1 = de.http({ block: {}, options: { // Имеет смысл сделать явный интерфейс для вычисленных параметров. @@ -44,7 +32,7 @@ const block1 = de.http( { // // Если нам где-то вообще понадобится context, то лучше всего задать его тип здесь. // - params: ( { params, context }:{ params: ParamsIn, context: Context } ): ParamsOut => { + params: ({ params }: { params: ParamsIn; context?: Context }) => { return { foo: params.id, }; @@ -52,8 +40,8 @@ const block1 = de.http( { // Тут тип params уже ParamsOut. // - before: ( { params, context } ) => { - if ( !params.foo ) { + before: ({ params }) => { + if (!params.foo) { // Мы можем вернуть тот же тип, что возвращает options.after. return 'foo'; } @@ -65,60 +53,60 @@ const block1 = de.http( { // Если мы здесь хотим использовать params, то нам приходится прописать тип явно. // Typescript не позволяет частично задавать тип при destructure. // - after: ( { params, result }) => { + after: ({ result }) => { // Тип для обработанного результата нам в принципе не нужен. // Он выведется из того, что мы вернули. // return result; }, }, -} ); +}); -de.run( block1, { +de.run(block1, { params: { id: '12345', }, -} ) - .then( ( result ) => { - console.log( result ); - } ); +}) + .then((result) => { + console.log(result); + }); - // --------------------------------------------------------------------------------------------------------------- // +// --------------------------------------------------------------------------------------------------------------- // // Вариант 2. // * Вычисляем новые параметры. // * Обрабатываем результат, но params нам в after не нужны. -const block2 = de.http( { +const block2 = de.http({ block: {}, options: { - params: ( { params, context }: { params: ParamsIn, context: Context }) => { + params: ({ params }: { params: ParamsIn; context?: Context }) => { return { foo: params.id, }; }, - before: ( { params } ) => { - if ( !params.foo ) { + before: ({ params }) => { + if (!params.foo) { return 'foo'; } }, - after: ( { result } ) => { + after: ({ result }) => { return result; }, }, -} ); +}); -de.run( block2, { +de.run(block2, { params: { id: '12345', }, -} ) - .then( ( result ) => { - console.log( result ); - } ); +}) + .then((result) => { + console.log(result); + }); // --------------------------------------------------------------------------------------------------------------- // @@ -126,11 +114,11 @@ de.run( block2, { // * Не вычисляем новые параметры. -const block3 = de.http( { +const block3 = de.http({ block: {}, options: { - before: ( { params }: { params: ParamsIn } ) => { - if ( !params.id ) { + before: ({ params }: { params: ParamsIn }) => { + if (!params.id) { return 'foo'; } }, @@ -138,19 +126,20 @@ const block3 = de.http( { // Где-то нужно объявить тип входящих params. // Например, в after. Или же в before. В зависимости от того, что есть. // - after: ( { params, result } ) => { + after: ({ params, result }) => { + params.id; return result; }, }, -} ); +}); -de.run( block3, { +de.run(block3, { params: { id: '12345', }, -} ) - .then( ( result ) => { - console.log( result ); +}) + .then((result) => { + console.log(result); return result; - } ); + }); diff --git a/docs/typescript-examples/pipe.ts b/docs/typescript-examples/pipe.ts new file mode 100644 index 0000000..275489a --- /dev/null +++ b/docs/typescript-examples/pipe.ts @@ -0,0 +1,168 @@ +/* eslint-disable no-console */ +import * as de from '../../lib'; + +// --------------------------------------------------------------------------------------------------------------- // + +// --------------------------------------------------------------------------------------------------------------- // + +interface ParamsIn1 { + id1: string; +} + +const block1 = de.http({ + block: {}, + options: { + params: ({ params }: { params: ParamsIn1 }) => { + return { + s1: params.id1, + }; + }, + + after: ({ params }) => { + return { + b1: 1, + a: params.s1, + }; + }, + }, +}); + +// --------------------------------------------------------------------------------------------------------------- // + +interface ParamsIn2 { + id2: number; +} + +const block2 = de.http({ + block: {}, + options: { + + params: ({ params }: { params: ParamsIn2 }) => { + return params; + }, + + after: ({ params }) => { + return { + p: params.id2, + b2: '2', + }; + }, + }, +}); + +// --------------------------------------------------------------------------------------------------------------- // + +const block3 = de.pipe({ + block: [ + block1, + block2, + de.func({ + block: () => 1, + options: {}, + }), + ] as const, + options: { + params: ({ params }) => { + return params; + }, + after: ({ result }) => { + if (typeof result === 'number') { + return result; + } else if ('b1' in result) { + return result.b1; + } else if ('b2' in result) { + return result.b2; + } + }, + }, +}); + +de.run(block3, { + params: { + id1: '12345', + id2: 67890, + }, +}) + .then((result) => { + console.log(result); + }); + +const block4 = block3.extend({ + options: { + after: ({ result }) => { + return result; + }, + }, +}); + +de.run(block4, { + params: { + id1: '12345', + id2: 67890, + }, +}) + .then((result) => { + console.log(result); + }); + + +const bfn1 = de.func({ + block: ({ params }: { params: { p1: number } }) => { + return { + b1: params.p1, + }; + }, + options: { + after: ({ result }) => { + return { + r: 1, + b1: result.b1, + }; + }, + }, +}); + + +const bfn2 = de.func({ + block: ({ params }: { params: { p2: string } }) => { + return { + b2: params.p2, + }; + }, + options: { + after: ({ result }) => { + return { + r2: 1, + b2: result.b2, + }; + }, + }, +}); + +const bfn3 = de.pipe({ + block: [ + bfn1, + bfn2, + ], + options: { + params: ({ params }) => { + return params; + }, + after: ({ result }) => { + if (!de.isError(result)) { + return ('b1' in result ? result.b1 : result.b2); + } + return result; + }, + }, +}); + +de.run(bfn3, { + params: { + p2: '12345', + p1: 67890, + }, +}) + .then((result) => { + console.log(result); + }); diff --git a/docs/typescript-examples/session.ts b/docs/typescript-examples/session.ts index 4760d82..e7c8903 100644 --- a/docs/typescript-examples/session.ts +++ b/docs/typescript-examples/session.ts @@ -1,5 +1,8 @@ -import {inferBlockTypes, DescriptBlockResultJSON} from '../../lib'; +/* eslint-disable no-console */ + +import type { DescriptHttpBlockResult } from '../../lib'; import * as de from '../../lib'; +import type DepsDomain from '../../lib/depsDomain'; interface TRequest extends Request { session?: Session; @@ -15,22 +18,30 @@ interface TDescriptContext { additionalParams?: Record; } +type SessionParams = { + dealerId?: string; +} + type Session = { id: string; username: string; - client_id?: string; + clientId?: string; } -const baseResource = de.http>( { +const baseResource = de.http({ block: {}, -} ); + options: { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + before: ({ context }: { context?: TDescriptContext }) => {}, + }, +}); -const resource = baseResource( { +const resource = baseResource.extend({ block: { agent: { maxSockets: 16, keepAlive: true }, - max_retries: 1, + maxRetries: 1, timeout: 1000, - headers: ( { headers, context } ) => { + headers: ({ headers }) => { return { ...headers, @@ -41,13 +52,13 @@ const resource = baseResource( { options: { name: 'session', }, -} ); +}); -const secondRequest = resource({ - block:{} -}) +const secondRequest = resource.extend({ + block: {}, +}); -const session = resource( { +const session = resource.extend({ block: { method: 'GET', pathname: '/1.0/session/', @@ -56,17 +67,20 @@ const session = resource( { options: { name: 'publicApiAuth:GET /1.0/session', + params: ({ params }: { params: SessionParams }) => params, before: (args) => { const { context } = args; - if (context.req.session) { + if (context?.req.session) { // возвращаем кешированную сессию return context.req.session; } + + throw 's'; }, - after: ({ result }: { result: DescriptBlockResultJSON }) => { - const session = result.result.session; + after: ({ result }: { result: DescriptHttpBlockResult | Session }) => { + const session = 'result' in result ? result.result.session : result; return session; }, @@ -74,124 +88,125 @@ const session = resource( { throw error; }, }, -} ); +}); -function getClientId({ params } : { params: any }) { - return params.client_id; +function getClientId({ params }: { params: any }) { + return params.clientId; } -const x = de.object({ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const temp = de.object({ block: { - secondRequest: inferBlockTypes(secondRequest({block:{}})), - session: inferBlockTypes(session({ + secondRequest: secondRequest.extend({ block: {} }), + session: session.extend({ options: { required: true, }, - })), + }), }, options: { params: (args) => { return { - dealer_id: getClientId(args), + dealerId: getClientId(args), }; }, - after: ({context, params, result}) => { + after: ({ result }) => { const session = result.session; return session; }, - } + }, }); const sessionMethod = de.func({ - block: ({generate_id: generateId}) => { + block: ({ generateId: generateId }) => { const sessionId = generateId(); return de.object({ block: { - session: inferBlockTypes(session({ + session: session.extend({ options: { id: sessionId, required: true, - after: ({params, result}) => { - if ((params as any).dealer_id && result) { - result.client_id = (params as any).dealer_id; + after: ({ params, result }) => { + if (params.dealerId && result) { + result.clientId = params.dealerId; } return result; }, }, - })), + }), }, options: { params: (args) => { return { - dealer_id: getClientId(args), + dealerId: getClientId(args), }; }, - after: ({context, params, result}) => { - const x = result; - const session = result.session as Session & Record; + after: ({ result }) => { + const session = result.session; return session; }, - } + }, }); - } + }, }); type Params = { - param: string + param: string; } const controller = de.func({ - block: ({ generate_id: generateId, params }: { params: Params; generate_id: de.DescriptBlockGenerateId }) => { - const session1 = sessionMethod({ + // eslint-disable-next-line @typescript-eslint/no-unused-vars + block: ({ generateId: generateId, params }: { params: Params; generateId: DepsDomain['generateId'] }) => { + const session1 = sessionMethod.extend({ options: { - after: ({result}) => result - } + after: ({ result }) => result, + }, }); return de.object({ block: { - session:session1, - session2: inferBlockTypes(sessionMethod({ options: { - after: ({result}) => result - }})), + session: session1, + session2: sessionMethod.extend({ options: { + after: ({ result }) => result, + } }), }, options: { - after: ({result}) => result, - } + after: ({ result }) => result, + }, }); }, options: { - after: ({result}) => result, - } -}) + after: ({ result }) => result, + }, +}); de.run(controller, {}) .then(result => { - console.log(result.session, result.session2) - }) + console.log(result.session, result.session2); + }); const sessionMethod2 = de.func({ - block: ({generate_id: generateId}) => { + block: ({ generateId: generateId }) => { const sessionId = generateId(); - const session2 = session({ + const session2 = session.extend({ options: { id: sessionId, // При падении сессии роняем весь блок. // Тогда можно будет в нужных местах написать session({ options: { required: true } }); required: true, - after: ({params, result}) => { - if ((params as any).dealer_id && result) { - result.client_id = (params as any).dealer_id; + after: ({ params, result }) => { + if (params.dealerId && result) { + result.clientId = params.dealerId; } return result; @@ -201,44 +216,46 @@ const sessionMethod2 = de.func({ return de.object({ block: { - session: inferBlockTypes(session({ + session: session.extend({ options: { id: sessionId, // При падении сессии роняем весь блок. // Тогда можно будет в нужных местах написать session({ options: { required: true } }); required: true, - after: ({params, result}) => { - if ((params as any).dealer_id && result) { - result.client_id = (params as any).dealer_id; + after: ({ params, result }) => { + if (params.dealerId && result) { + result.clientId = params.dealerId; } return result; }, }, - })), + }), session2, }, options: { params: (args) => { return { - dealer_id: getClientId(args), + dealerId: getClientId(args), }; }, - after: ({context, params, result}) => { + after: ({ result }) => { const session = result.session; + // eslint-disable-next-line @typescript-eslint/no-unused-vars const session2 = result.session2; return session; }, - } + }, }); - } -} ); + }, +}); de.run(sessionMethod2, {}) .then(result => { - console.log(result.username) - }) - + if (!de.isError(result)) { + console.log(result.username); + } + }); diff --git a/global.d.ts b/global.d.ts new file mode 100644 index 0000000..21465a9 --- /dev/null +++ b/global.d.ts @@ -0,0 +1,13 @@ +declare global { + namespace jest { + interface Matchers { + toBeValidGzip(received?: any): CustomMatcherResult; + toHaveUngzipValue(received: any, value?: any): CustomMatcherResult; + } + + /*interface Expect { + toBeValidGzip(received: any): CustomMatcherResult; + toHaveUngzipValue(received: any, value): CustomMatcherResult; + }*/ + } +} diff --git a/jest.config.js b/jest.config.js index f8986e7..f6d8565 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,14 +1,25 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ module.exports = { coveragePathIgnorePatterns: [ '/node_modules/', - '/tests/expect.js', + '/tests/expect.ts', '/tests/server.js', - '/tests/helpers.js', + '/tests/helpers.ts', ], setupFilesAfterEnv: [ - './tests/expect.js', + './tests/expect.ts', ], + preset: 'ts-jest/presets/js-with-ts', + //preset: 'ts-jest', + + transform: { + '^.+\\.tsx?$': [ + 'ts-jest', + { + useESM: true, + }, + ], + }, testEnvironment: 'node', testRunner: 'jest-circus/runner', }; - diff --git a/lib/arrayBlock.ts b/lib/arrayBlock.ts new file mode 100644 index 0000000..8ef8103 --- /dev/null +++ b/lib/arrayBlock.ts @@ -0,0 +1,145 @@ +import CompositeBlock from './compositeBlock' ; +import { createError, ERROR_ID } from './error' ; +import type { DescriptError } from './error' ; +import type { + BlockResultOut, + First, + InferResultFromBlock, + InferParamsInFromBlock, + Tail, + DescriptBlockOptions, +} from './types'; +import type BaseBlock from './block'; +import type ContextClass from './context'; +import type Cancel from './cancel'; +import type { DescriptBlockDeps } from './depsDomain'; +import type DepsDomain from './depsDomain'; + +export type GetArrayBlockResult< T extends ReadonlyArray> = { + 0: never; + 1: [ InferResultFromBlock< First< T > > | DescriptError ]; + 2: [ InferResultFromBlock< First< T > > | DescriptError, ...GetArrayBlockResult< Tail< T > > ]; +}[ T extends [] ? 0 : T extends ((readonly [ any ]) | [ any ]) ? 1 : 2 ]; + +export type GetArrayBlockParamsUnion< T extends ReadonlyArray> = { + 0: never; + 1: First< T >; + 2: First< T > & GetArrayBlockParamsUnion< Tail< T > >; +}[ T extends [] ? 0 : T extends ((readonly [ any ]) | [ any ]) ? 1 : 2 ]; + +type GetArrayBlockParamsMap< T extends ReadonlyArray> = { + [ P in keyof T ]: InferParamsInFromBlock; +} + +export type GetArrayBlockParams< + T extends ReadonlyArray, + PA extends ReadonlyArray = GetArrayBlockParamsMap, + PU = GetArrayBlockParamsUnion +> = PU; + +export type ArrayBlockDefinition< T > = { + [ P in keyof T ]: T[ P ] extends BaseBlock< + // eslint-disable-next-line @typescript-eslint/no-unused-vars + infer Context, infer CustomBlock, infer ParamsOut, infer ResultOut, infer IntermediateResult, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + infer BlockResult, infer BeforeResultOut, infer AfterResultOut, infer ErrorResultOut, infer Params + > ? T[ P ] : never +} + +class ArrayBlock< + Context, + Block extends ReadonlyArray, + ResultOut extends BlockResultOut, + ParamsOut = GetArrayBlockParams, + BlockResult = GetArrayBlockResult, + + BeforeResultOut = undefined, + AfterResultOut = undefined, + ErrorResultOut = undefined, + Params = GetArrayBlockParams, +> extends CompositeBlock< + Context, + ArrayBlockDefinition, + ParamsOut, + ResultOut, + BlockResult, + BlockResult, + + BeforeResultOut, + AfterResultOut, + ErrorResultOut, + Params + > { + + extend< + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ExtendedResultOut extends + BlockResultOut, + ExtendedParamsOut = Params, + ExtendedParams = Params, + ExtendedBlockResult = ResultOut, + ExtendedBeforeResultOut = undefined, + ExtendedAfterResultOut = undefined, + ExtendedErrorResultOut = undefined, + >({ options }: { + options: DescriptBlockOptions< + Context, Params & ExtendedParamsOut, ExtendedBlockResult, ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams + >; + }) { + return this.extendClass< + ArrayBlock< + Context, + Block, + ExtendedResultOut, + Params & ExtendedParamsOut, + ExtendedBlockResult, + ExtendedBeforeResultOut, + ExtendedAfterResultOut, + ExtendedErrorResultOut, + ExtendedParams + >, + ExtendedBlockResult, + ExtendedParamsOut, + ExtendedParams, + ExtendedBeforeResultOut, + ExtendedAfterResultOut, + ExtendedErrorResultOut + >({ options }); + } + + protected initBlock(array: ArrayBlockDefinition) { + if (!Array.isArray(array)) { + throw createError({ + name: ERROR_ID.INVALID_BLOCK, + message: 'block must be an array', + }); + } + + super.initBlock(array); + + this.blocks = array.map((block, i) => { + return { + block: block, + key: i, + }; + }); + } + + // Сюда еще приходят deps последним параметром, но они не нужны здесь. + // + protected blockAction(args: { + runContext: ContextClass; + blockCancel: Cancel; + cancel: Cancel; + params: ParamsOut; + context?: Context; + deps: DescriptBlockDeps; + nParents: number; + depsDomain?: DepsDomain; + }): Promise { + return this.runBlocks(args) as Promise; + } + +} + +export default ArrayBlock; diff --git a/lib/array_block.js b/lib/array_block.js deleted file mode 100644 index 36fd69f..0000000 --- a/lib/array_block.js +++ /dev/null @@ -1,33 +0,0 @@ -const CompositeBlock = require( './composite_block' ); -const { create_error, ERROR_ID } = require( './error' ); - -class ArrayBlock extends CompositeBlock { - - _init_block( array ) { - if ( !Array.isArray( array ) ) { - throw create_error( { - id: ERROR_ID.INVALID_BLOCK, - message: 'block must be an array', - } ); - } - - super._init_block( array ); - - this._blocks = array.map( ( block, i ) => { - return { - block: block, - key: i, - }; - } ); - } - - // Сюда еще приходят deps последним параметром, но они не нужны здесь. - // - _action( args ) { - return this._run_blocks( args ); - } - -} - -module.exports = ArrayBlock; - diff --git a/lib/block.js b/lib/block.js deleted file mode 100644 index 513a543..0000000 --- a/lib/block.js +++ /dev/null @@ -1,419 +0,0 @@ -const { create_error, ERROR_ID } = require( './error' ); - -const DepsDomain = require( './deps_domain' ); - -// const extend_option = require( './extend_option' ); - -// --------------------------------------------------------------------------------------------------------------- // - -class Block { - - constructor( block, options ) { - const f = function( { block, options } = {} ) { - return new f.constructor( - f._extend_block( block ), - f._extend_options( options ), - ); - }; - - f.__proto__ = this.__proto__; - f._init_block( block ); - f._init_options( options ); - - return f; - } - - _init_block( block ) { - this._block = block; - } - - _init_options( options ) { - this._options = extend_options( {}, options ); - } - - _extend_block( block ) { - return this._block; - } - - _extend_options( options ) { - return extend_options( this._options, options ); - } - - async _run( { run_context, block_cancel, deps_domain, cancel, params, context, prev, n_parents } ) { - let h_timeout = null; - - function clear_timeout() { - if ( h_timeout ) { - clearTimeout( h_timeout ); - h_timeout = null; - } - } - - run_context.n_blocks++; - - let error; - let result; - let deps; - let active; - - try { - deps = await this._do_options_deps( run_context, block_cancel, deps_domain, n_parents ); - - active = true; - - run_context.n_active_blocks++; - - if ( prev !== undefined ) { - deps.prev = prev; - } - - if ( this._options.timeout > 0 ) { - h_timeout = setTimeout( () => { - block_cancel.cancel( { - id: ERROR_ID.BLOCK_TIMED_OUT, - } ); - h_timeout = null; - }, this._options.timeout ); - } - - const lifecycle = this._options.lifecycle; - if ( !lifecycle || !lifecycle.length ) { - // В блоке нет ничего из options.params, options.before, options.after, options.error. - // Просто вызываем экшен. - // - result = await this._do_action( run_context, block_cancel, deps_domain, cancel, params, context, deps, n_parents ); - - } else { - result = await this._do_lifecycle_step( 0, run_context, block_cancel, deps_domain, cancel, params, context, deps, n_parents ); - } - - } catch ( e ) { - error = create_error( e ); - } - - clear_timeout(); - - block_cancel.close(); - - if ( active ) { - run_context.n_active_blocks--; - } - run_context.n_blocks--; - run_context.queue_deps_check(); - - if ( this._options.id ) { - if ( error ) { - run_context.reject_promise( this._options.id, error ); - - } else { - run_context.resolve_promise( this._options.id, result ); - } - } - - if ( error ) { - throw error; - } - - return result; - } - - async _do_options_deps( run_context, block_cancel, deps_domain, n_parents ) { - const deps = this._options.deps; - if ( !deps || !deps.length ) { - return {}; - } - - if ( !deps_domain ) { - throw create_error( { - id: ERROR_ID.INVALID_DEPS_ID, - } ); - } - - const promises = deps.map( ( id ) => { - if ( !deps_domain.is_valid_id( id ) ) { - throw create_error( { - id: ERROR_ID.INVALID_DEPS_ID, - } ); - } - - return run_context.get_promise( id ); - } ); - - run_context.waiting_for_deps.push( { - block_cancel: block_cancel, - n_parents: n_parents, - } ); - - try { - const results = await Promise.race( [ - block_cancel.get_promise(), - Promise.all( promises ), - ] ); - - const r = {}; - - deps.forEach( ( id, i ) => { - r[ id ] = results[ i ]; - } ); - - return r; - - } catch ( error ) { - // FIXME: А зачем вот это тут? - const error_id = error.error.id; - if ( error_id === ERROR_ID.DEPS_NOT_RESOLVED ) { - throw error; - } - - throw create_error( { - id: ERROR_ID.DEPS_ERROR, - reason: error, - } ); - - } finally { - run_context.waiting_for_deps = run_context.waiting_for_deps.filter( ( item ) => item.block_cancel !== block_cancel ); - } - } - - async _do_lifecycle_step( index, run_context, block_cancel, deps_domain, cancel, params, context, deps, n_parents ) { - const lifecycle = this._options.lifecycle; - const step = lifecycle[ index ]; - - try { - let result; - - if ( step.params ) { - if ( typeof step.params !== 'function' ) { - throw create_error( { - id: ERROR_ID.INVALID_OPTIONS_PARAMS, - message: 'options.params must be a function', - } ); - } - - // Тут не нужен cancel. - params = step.params( { params, context, deps } ); - if ( !( params && typeof params === 'object' ) ) { - throw create_error( { - id: ERROR_ID.INVALID_OPTIONS_PARAMS, - message: 'Result of options.params must be an object', - } ); - } - } - - if ( typeof step.before === 'function' ) { - result = await step.before( { cancel, params, context, deps } ); - block_cancel.throw_if_cancelled(); - - if ( result instanceof Block ) { - result = await run_context.run( { - block: result, - block_cancel: block_cancel.create(), - deps_domain: new DepsDomain( deps_domain ), - params: params, - context: context, - cancel: cancel, - n_parents: n_parents + 1, - } ); - } - block_cancel.throw_if_cancelled(); - } - - if ( result === undefined ) { - if ( index < lifecycle.length - 1 ) { - result = await this._do_lifecycle_step( index + 1, run_context, block_cancel, deps_domain, cancel, params, context, deps, n_parents ); - - } else { - result = await this._do_action( run_context, block_cancel, deps_domain, cancel, params, context, deps, n_parents ); - } - } - block_cancel.throw_if_cancelled(); - - if ( typeof step.after === 'function' ) { - result = await step.after( { cancel, params, context, deps, result } ); - block_cancel.throw_if_cancelled(); - - if ( result instanceof Block ) { - result = await run_context.run( { - block: result, - block_cancel: block_cancel.create(), - deps_domain: new DepsDomain( deps_domain ), - params: params, - context: context, - cancel: cancel, - n_parents: n_parents + 1, - } ); - } - block_cancel.throw_if_cancelled(); - } - - return result; - - } catch ( e ) { - const error = create_error( e ); - - // FIXME: А нужно ли уметь options.error делать асинхронным? - // - if ( typeof step.error === 'function' ) { - return step.error( { cancel, params, context, deps, error } ); - } - - throw error; - } - } - - async _do_action( run_context, block_cancel, deps_domain, cancel, params, context, deps, n_parents ) { - let result; - - const cache = this._options.cache; - let key; - const options_key = this._options.key; - if ( cache && options_key ) { - // Тут не нужен cancel. - key = ( typeof options_key === 'function' ) ? options_key( { params, context, deps } ) : options_key; - if ( typeof key !== 'string' ) { - key = null; - } - if ( key ) { - try { - result = await cache.get( { key, context } ); - - } catch ( e ) { - // Do nothing. - } - block_cancel.throw_if_cancelled(); - - if ( result !== undefined ) { - return result; - } - } - } - - result = await this._action( { run_context, block_cancel, deps_domain, cancel, params, context, deps, n_parents } ); - block_cancel.throw_if_cancelled(); - - if ( result !== undefined && key ) { - try { - const promise = cache.set( { - key: key, - value: result, - maxage: this._options.maxage, - context: context, - } ); - // FIXME: А как правильно? cache.set может вернуть промис, а может и нет, - // при этом промис может зафейлиться. Вот так плохо: - // - // await cache.set( ... ) - // - // так как ждать ответа мы не хотим. Но результат хотим проигнорить. - // - if ( promise && typeof promise.catch === 'function' ) { - // It's catchable! - promise.catch( () => { - // Do nothing. - } ); - } - - } catch ( e ) { - // Do nothing. - } - } - - return result; - } - -} - -Block.prototype = Object.create( Function.prototype ); - -module.exports = Block; - -// --------------------------------------------------------------------------------------------------------------- // - -function extend_options( what, by = {} ) { - const options = {}; - - options.name = by.name || what.name; - - options.id = by.id; - options.deps = extend_deps( by.deps ); - - options.lifecycle = extend_lifecycle( what, by ); - - options.timeout = by.timeout || what.timeout; - - options.key = by.key || what.key; - options.maxage = by.maxage || what.maxage; - options.cache = by.cache || what.cache; - - options.required = by.required; - - options.logger = by.logger || what.logger; - - return options; -} - -function extend_deps( deps ) { - if ( !deps ) { - return null; - } - - if ( !Array.isArray( deps ) ) { - deps = [ deps ]; - } - - return ( deps.length ) ? deps : null; -} - -function extend_lifecycle( what, by ) { - if ( by.lifecycle ) { - return ( what.lifecycle ) ? [].concat( by.lifecycle, what.lifecycle ) : [].concat( by.lifecycle ); - - } else if ( by.params || by.before || by.after || by.error ) { - const lifecycle = [ - { - params: by.params, - before: by.before, - after: by.after, - error: by.error, - }, - ]; - - return ( what.lifecycle ) ? lifecycle.concat( what.lifecycle ) : lifecycle; - - } else { - return ( what.lifecycle ) ? [].concat( what.lifecycle ) : undefined; - } -} - -/* -function eval_params_item( item, params, context, deps ) { - const r = {}; - - const callback_args = { params, context, deps }; - - for ( const p_name in item ) { - const p_value = item[ p_name ]; - - let value; - if ( typeof p_value === 'function' ) { - value = p_value( callback_args ); - - } else if ( p_value === null ) { - value = params[ p_name ]; - - } else if ( p_value !== undefined ) { - value = params[ p_name ]; - if ( value === undefined ) { - value = p_value; - } - } - - if ( value !== undefined ) { - r[ p_name ] = value; - } - } - - return r; -} -*/ - diff --git a/lib/block.ts b/lib/block.ts new file mode 100644 index 0000000..bf30d79 --- /dev/null +++ b/lib/block.ts @@ -0,0 +1,656 @@ +import { createError, ERROR_ID } from './error' ; + +import type { DescriptBlockDeps, DescriptBlockId } from './depsDomain'; +import DepsDomain from './depsDomain' ; +import type Cancel from './cancel'; +import type ContextClass from './context'; +import type { BlockResultOut, InferResultOrResult, DescriptBlockOptions, DepsIds } from './types'; + +type BlockOptions< + Context, + ParamsOut, + BlockResult, + + BeforeResultOut, + AfterResultOut, + ErrorResultOut, + Params, +> = + { + + deps?: DepsIds | null; + lifecycle: Array< + Pick< + DescriptBlockOptions, + 'before' | 'after' | 'error' | 'params' + > + >; + } & Partial, + 'before' | 'after' | 'error' | 'params' | 'deps' + >> + + +interface BlockConstructor< + Context, + ParamsOut, + BlockResult, + BeforeResultOut, + AfterResultOut, + ErrorResultOut, + Params, + ClassType, + CustomBlock +> { + new ({ block, options }: { + block?: CustomBlock; + options: DescriptBlockOptions; + }): ClassType; +} + +abstract class BaseBlock< + Context, + CustomBlock, + ParamsOut, + ResultOut extends BlockResultOut, + IntermediateResult, + BlockResult, + BeforeResultOut = undefined, + AfterResultOut = undefined, + ErrorResultOut = undefined, + Params = ParamsOut, +> { + protected block: CustomBlock; + protected options: BlockOptions; + + isRequired(): boolean { + return Boolean(this.options.required); + } + + constructor({ block, options }: { + block?: CustomBlock; + + options?: DescriptBlockOptions | + BlockOptions; + + }) { + // если таки умудрились не передать блок, то кастомный initBlock в большинстве блоков кинет ошибку + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + this.initBlock(block); + this.initOptions(options); + } + + protected extendClass< + ClassType, + ExtendedBlockResult, + ExtendedParamsOut = Params, + ExtendedParams = Params, + ExtendedBeforeResultOut = undefined, + ExtendedAfterResultOut = undefined, + ExtendedErrorResultOut = undefined, + >({ block, options }: { + block?: CustomBlock; + options?: + DescriptBlockOptions< + Context, Params & ExtendedParamsOut, ExtendedBlockResult, ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams + >; + }): ClassType { + + return new (> this.constructor)({ + block: this.extendBlock(block), + options: this.extendOptions(this.options, options), + }); + } + + abstract extend< + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ExtendedResultOut extends + BlockResultOut, + ExtendedParamsOut = Params, + ExtendedParams = Params, + ExtendedBlockResult = ResultOut, + ExtendedBeforeResultOut = undefined, + ExtendedAfterResultOut = undefined, + ExtendedErrorResultOut = undefined, + >({ block, options }: { + block?: CustomBlock; + options?: DescriptBlockOptions< + Context, Params & ExtendedParamsOut, ExtendedBlockResult, ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams + >; + }): unknown + + protected initBlock(block: CustomBlock) { + this.block = block; + } + + protected initOptions(options?: DescriptBlockOptions | BlockOptions) { + this.options = this.extendOptions({ lifecycle: [] }, options); + } + + //eslint-disable-next-line @typescript-eslint/no-unused-vars + protected extendBlock(block?: CustomBlock) { + return this.block; + } + + protected extendOptions< + ExtendedParamsOut, + ExtendedBlockResult, + ExtendedBeforeResultOut, + ExtendedAfterResultOut, + ExtendedErrorResultOut, + ExtendedParams, + >( + what: BlockOptions, + by: DescriptBlockOptions< + Context, ExtendedParamsOut, ExtendedBlockResult, + ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams + > | BlockOptions< + Context, ExtendedParamsOut, ExtendedBlockResult, + ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams + > = {}, + ) { + const options: BlockOptions< + Context, ExtendedParamsOut, ExtendedBlockResult, + ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams + > = { + deps: extendDeps(by.deps), + lifecycle: [], + }; + + options.name = by.name || what.name; + options.id = by.id; + + options.lifecycle = this.extendLifecycle(what, by); + + options.timeout = by.timeout || what.timeout || 0; + + options.key = (by.key || what.key) as BlockOptions< + Context, ExtendedParamsOut, ExtendedBlockResult, + ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams + >['key']; + options.maxage = by.maxage || what.maxage; + options.cache = (by.cache || what.cache) as BlockOptions< + Context, ExtendedParamsOut, ExtendedBlockResult, + ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams + >['cache']; + + options.required = by.required; + + options.logger = by.logger || what.logger; + + return options; + } + + private extendLifecycle< + ExtendedParamsOut, + ExtendedBlockResult, + ExtendedBeforeResultOut = undefined, + ExtendedAfterResultOut = undefined, + ExtendedErrorResultOut = undefined, + ExtendedParams = ExtendedParamsOut, + // eslint-disable-next-line max-len + W extends BlockOptions = + BlockOptions, + B extends DescriptBlockOptions< + Context, ExtendedParamsOut, ExtendedBlockResult, ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams + > | BlockOptions< + Context, ExtendedParamsOut, ExtendedBlockResult, ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams + > = + DescriptBlockOptions< + Context, ExtendedParamsOut, ExtendedBlockResult, ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams + > | BlockOptions< + Context, ExtendedParamsOut, ExtendedBlockResult, ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams + > + >(what: W, by: B): BlockOptions< + Context, ExtendedParamsOut, ExtendedBlockResult, + ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams>['lifecycle'] { + const newArray: BlockOptions['lifecycle'] = []; + + if ('lifecycle' in by && by.lifecycle) { + return ((what.lifecycle) ? newArray.concat(by.lifecycle as typeof newArray, what.lifecycle) : newArray.concat(by.lifecycle as typeof newArray)) as + Array>; + + } else if (!('lifecycle' in by) && (by.params || by.before || by.after || by.error)) { + const lifecycle: Array> = [ + { + params: by.params, + before: by.before, + after: by.after, + error: by.error, + }, + ]; + + return (what.lifecycle) ? lifecycle.concat(what.lifecycle as typeof lifecycle) : lifecycle; + + } else { + return ((what.lifecycle) ? (newArray).concat(what.lifecycle) : []) as Array>; + } + } + + async run( + { runContext, blockCancel, depsDomain, cancel, params, context, prev, nParents }: + { + runContext: ContextClass; + blockCancel: Cancel; + depsDomain?: DepsDomain; + params?: Params; + context?: Context; + cancel: Cancel; + prev?: unknown; + nParents?: number; + }): Promise> { + + let hTimeout: NodeJS.Timeout | null = null; + + function internalClearTimeout() { + if (hTimeout) { + clearTimeout(hTimeout); + hTimeout = null; + } + } + + runContext.incNumberOfBlocks(); + + let error; + let result: InferResultOrResult | undefined = undefined; + let deps; + let active; + + try { + deps = await this.doOptionsDeps(runContext, blockCancel, depsDomain, nParents); + + active = true; + + runContext.incNumberOfActiveBlocks(); + + //TODO типизировать + if (prev !== undefined) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + deps.prev = prev; + } + + if ((this.options.timeout || 0) > 0) { + hTimeout = setTimeout(() => { + blockCancel.cancel(ERROR_ID.BLOCK_TIMED_OUT); + hTimeout = null; + }, this.options.timeout); + } + + const lifecycle = this.options.lifecycle; + if (!lifecycle || !lifecycle.length) { + // В блоке нет ничего из options.params, options.before, options.after, options.error. + // Просто вызываем экшен. + // + // this.blockAction запускает для получения результата + // работает с кешем + result = await this.doAction( + { runContext, blockCancel, cancel, params: params as unknown as ParamsOut, context, deps, nParents: nParents || 0, depsDomain }, + ) as Awaited>; + + } else { + result = await this.doLifecycleStep( + { index: 0, runContext, blockCancel, cancel, params: params as unknown as ParamsOut, context, deps, nParents: nParents || 0, depsDomain }, + ); + } + + } catch (e) { + error = createError(e); + } + + internalClearTimeout(); + + blockCancel.close(); + + if (active) { + runContext.decNumberOfActiveBlocks(); + } + runContext.decNumberOfBlocks(); + runContext.queueDepsCheck(); + + if (this.options.id) { + if (error) { + runContext.rejectPromise(this.options.id, error); + + } else { + runContext.resolvePromise(this.options.id, result as unknown as ResultOut); + } + } + + if (error) { + throw error; + } + + return result as InferResultOrResult; + } + + private async doOptionsDeps( + runContext: ContextClass, + blockCancel: Cancel, + depsDomain?: DepsDomain, + nParents = 0, + ) { + const deps = this.options.deps; + if (!deps || !deps.length) { + return {}; + } + + if (!depsDomain) { + throw createError(ERROR_ID.INVALID_DEPS_ID); + } + + const promises = deps.map((id) => { + if (!depsDomain.isValidId(id)) { + throw createError(ERROR_ID.INVALID_DEPS_ID); + } + + return runContext.getPromise(id); + }); + + runContext.addWaitingDeps({ + blockCancel: blockCancel, + nParents: nParents, + }); + + try { + const results = await Promise.race([ + blockCancel.getPromise(), + Promise.all(promises), + ]) as Array; + + const r: DescriptBlockDeps = {}; + //TODO как это типизировать? + deps.forEach((id, i) => { + r[ id ] = results[ i ]; + }); + + return r; + + } catch (error) { + // FIXME: А зачем вот это тут? + const errorId = error.error.id; + if (errorId === ERROR_ID.DEPS_NOT_RESOLVED) { + throw error; + } + + throw createError({ + id: ERROR_ID.DEPS_ERROR, + reason: error, + }); + + } finally { + runContext.removeWaitingDeps(blockCancel); + } + } + + private async doLifecycleStep({ + index, + runContext, + blockCancel, + cancel, + params, + context, + deps, + nParents, + depsDomain, + }: { + index: number; + runContext: ContextClass; + blockCancel: Cancel; + cancel: Cancel; + params: ParamsOut; + context?: Context; + deps: DescriptBlockDeps; + nParents: number; + depsDomain?: DepsDomain; + }): Promise> { + const lifecycle = this.options.lifecycle; + const step = lifecycle[ index ]; + + let resultBefore: BeforeResultOut | undefined = undefined; + let resultBlock: BlockResult | undefined = undefined; + let resultAfter: AfterResultOut | undefined = undefined; + let errorResult: ErrorResultOut | undefined = undefined; + + try { + + if (step.params) { + if (typeof step.params !== 'function') { + throw createError('options.params must be a function', ERROR_ID.INVALID_OPTIONS_PARAMS); + } + + // Тут не нужен cancel. + params = step.params({ params: params as unknown as Params, context, deps }); + if (!(params && typeof params === 'object')) { + throw createError('Result of options.params must be an object', ERROR_ID.INVALID_OPTIONS_PARAMS); + } + } + + if (typeof step.before === 'function') { + resultBefore = await step.before({ cancel, params, context, deps }); + blockCancel.throwIfCancelled(); + + if (resultBefore instanceof BaseBlock) { + resultBefore = await runContext.run({ + block: resultBefore, + blockCancel: blockCancel.create(), + depsDomain: new DepsDomain(depsDomain), + params: params, + context: context, + cancel: cancel, + nParents: nParents + 1, + }) as BeforeResultOut; + } + blockCancel.throwIfCancelled(); + } + + if (resultBefore === undefined) { + if (index < lifecycle.length - 1) { + resultBlock = await this.doLifecycleStep( + { index: index + 1, runContext, blockCancel, cancel, params, context, deps, nParents, depsDomain }, + ) as BlockResult; + + } else { + resultBlock = await this.doAction({ runContext, blockCancel, cancel, params, context, deps, nParents, depsDomain }); + } + } + blockCancel.throwIfCancelled(); + + if (typeof step.after === 'function') { + resultAfter = await step.after({ cancel, params, context, deps, result: (resultBefore || resultBlock) as any }); + blockCancel.throwIfCancelled(); + + if (resultAfter instanceof BaseBlock) { + resultAfter = await runContext.run({ + block: resultAfter, + blockCancel: blockCancel.create(), + depsDomain: new DepsDomain(depsDomain), + params: params, + context: context, + cancel: cancel, + nParents: nParents + 1, + }) as AfterResultOut; + } + blockCancel.throwIfCancelled(); + } + + } catch (e) { + const error = createError(e); + + // FIXME: А нужно ли уметь options.error делать асинхронным? + // + if (typeof step.error === 'function') { + errorResult = step.error({ cancel, params, context, deps, error }); + } else { + throw error; + } + } + + let result; + + if (errorResult !== undefined) { + result = errorResult; + } else { + if (resultBefore !== undefined) { + result = resultBefore; + } else if (typeof step.after === 'function') { + result = resultAfter; + } else { + result = resultBlock; + } + } + + return result as InferResultOrResult; + } + + protected abstract blockAction({ runContext, blockCancel, cancel, params, context, deps, nParents, depsDomain }: { + runContext: ContextClass; + blockCancel: Cancel; + cancel: Cancel; + params: ParamsOut; + context?: Context; + deps: DescriptBlockDeps; + nParents: number; + depsDomain?: DepsDomain; + }): Promise + + private async doAction({ + runContext, + blockCancel, + cancel, + params, + context, + deps, + depsDomain, + nParents, + }: { + runContext: ContextClass< + BlockResult, + IntermediateResult, + ResultOut, + Context, + BeforeResultOut, + AfterResultOut, + ErrorResultOut + >; + blockCancel: Cancel; + cancel: Cancel; + params: ParamsOut; + context?: Context; + deps: DescriptBlockDeps; + nParents: number; + depsDomain?: DepsDomain; + }): Promise { + let result: BlockResult | undefined = undefined; + + const cache = this.options.cache; + let key; + const optionsKey = this.options.key; + + if (cache && optionsKey) { + // Тут не нужен cancel. + key = (typeof optionsKey === 'function') ? optionsKey({ params, context, deps }) : optionsKey; + if (typeof key !== 'string') { + key = null; + } + if (key) { + try { + result = await cache.get({ key }); + + } catch (e) { + // Do nothing. + } + blockCancel.throwIfCancelled(); + + if (result !== undefined) { + return result; + } + } + } + + result = await this.blockAction({ + runContext, + blockCancel, + cancel, + params, + context, + deps, + nParents, + depsDomain, + }); + blockCancel.throwIfCancelled(); + + if (result !== undefined && key && cache) { + try { + const promise = cache.set({ + key: key, + value: result, + maxage: this.options.maxage, + }) as unknown as object | Promise; + // FIXME: А как правильно? cache.set может вернуть промис, а может и нет, + // при этом промис может зафейлиться. Вот так плохо: + // + // await cache.set( ... ) + // + // так как ждать ответа мы не хотим. Но результат хотим проигнорить. + // + if (promise && 'catch' in promise && typeof promise.catch === 'function') { + // It's catchable! + promise.catch(() => { + // Do nothing. + }); + } + + } catch (e) { + // Do nothing. + } + } + + return result; + } + +} + +//BaseBlock.prototype = Object.create(Function.prototype); + +export default BaseBlock; + +// --------------------------------------------------------------------------------------------------------------- // + +function extendDeps(deps?: DescriptBlockId | DepsIds | null): DepsIds | null { + if (!deps) { + return null; + } + + if (!Array.isArray(deps)) { + deps = [ deps ]; + } + + return (deps.length) ? deps : null; +} diff --git a/lib/cache.js b/lib/cache.js deleted file mode 100644 index ddf7980..0000000 --- a/lib/cache.js +++ /dev/null @@ -1,32 +0,0 @@ -class Cache { - - constructor() { - this._cache = {}; - } - - get( { key, context } ) { - const cache = this._cache; - - const item = cache[ key ]; - if ( item ) { - if ( ( item.expires === 0 ) || ( Date.now() < item.expires ) ) { - return item.value; - } - - delete cache[ key ]; - } - - return undefined; - } - - set( { key, value, maxage = 0, context } ) { - this._cache[ key ] = { - value: value, - expires: ( maxage ) ? Date.now() + maxage : 0, - }; - } - -} - -module.exports = Cache; - diff --git a/lib/cache.ts b/lib/cache.ts new file mode 100644 index 0000000..67d09ee --- /dev/null +++ b/lib/cache.ts @@ -0,0 +1,41 @@ +export type CacheItem = { + expires: number; + value: Result; +} + + +class Cache = CacheItem> { + protected cache: Record; + + constructor() { + this.cache = {}; + } + + async get({ key }: { key: string }): Promise { + const cache = this.cache; + + const item = cache[ key ]; + + if (item) { + if ((item.expires === 0) || (Date.now() < item.expires)) { + return item.value; + } + + delete cache[ key ]; + } + + return undefined; + } + + async set({ key, value, maxage = 0 }: { key: string; value: Result; maxage?: number }): Promise { + this.cache[ key ] = { + value: value, + expires: (maxage) ? Date.now() + maxage : 0, + } as ItemType ; + + return undefined; + } + +} + +export default Cache; diff --git a/lib/cancel.js b/lib/cancel.js deleted file mode 100644 index 9ad1ce7..0000000 --- a/lib/cancel.js +++ /dev/null @@ -1,78 +0,0 @@ -// https://github.com/tc39/proposal-cancellation - -const { create_error } = require( './error' ); - -class Cancel { - - constructor() { - this._reason = null; - this._closed = false; - this._callbacks = []; - } - - cancel( reason ) { - if ( this._reason || this._closed ) { - return; - } - - reason = this._reason = create_error( reason ); - - this._callbacks.forEach( ( callback ) => callback( reason ) ); - this._callbacks = null; - } - - close() { - this._closed = true; - this._callbacks = null; - } - - throw_if_cancelled() { - if ( this._reason ) { - throw this._reason; - } - } - - get_promise() { - if ( this._reason ) { - return Promise.reject( this._reason ); - } - - // Если this._closed, возвращаем промис, который никогда не зарезолвится/реджектится. - // - return new Promise( ( resolve, reject ) => { - this.subscribe( reject ); - } ); - } - - subscribe( callback ) { - if ( this._closed ) { - return; - } - - if ( this._reason ) { - callback( this._reason ); - - } else { - this._callbacks.push( callback ); - } - } - - create() { - const child = new Cancel(); - - if ( this._closed ) { - child.close(); - - } else if ( this._reason ) { - child.cancel( this._reason ); - - } else { - this._callbacks.push( ( reason ) => child.cancel( reason ) ); - } - - return child; - } -} - -module.exports = Cancel; - diff --git a/lib/cancel.ts b/lib/cancel.ts new file mode 100644 index 0000000..0d52749 --- /dev/null +++ b/lib/cancel.ts @@ -0,0 +1,78 @@ +// https://github.com/tc39/proposal-cancellation + +import type { DescriptError, Reason, ERROR_ID } from './error'; +import { createError } from './error' ; + +export type SubscribeCallback = (reason: DescriptError) => unknown; + +class Cancel { + + private _reason: DescriptError | null = null; + private _closed = false; + private _callbacks: Array = []; + + cancel(reason: Reason | ERROR_ID) { + if (this._reason || this._closed) { + return; + } + + this._reason = createError(reason); + + this._callbacks.forEach((callback) => callback(this._reason!)); + this._callbacks = []; + } + + close() { + this._closed = true; + this._callbacks = []; + } + + throwIfCancelled() { + if (this._reason) { + throw this._reason; + } + } + + getPromise() { + if (this._reason) { + return Promise.reject(this._reason); + } + + // Если this._closed, возвращаем промис, который никогда не зарезолвится/реджектится. + // + return new Promise((resolve, reject) => { + this.subscribe(reject); + }); + } + + subscribe(callback: SubscribeCallback) { + if (this._closed) { + return; + } + + if (this._reason) { + callback(this._reason); + + } else { + this._callbacks.push(callback); + } + } + + create() { + const child = new Cancel(); + + if (this._closed) { + child.close(); + + } else if (this._reason) { + child.cancel(this._reason); + + } else { + this._callbacks.push((reason) => child.cancel(reason)); + } + + return child; + } +} + +export default Cancel; diff --git a/lib/compositeBlock.ts b/lib/compositeBlock.ts new file mode 100644 index 0000000..b0c84f4 --- /dev/null +++ b/lib/compositeBlock.ts @@ -0,0 +1,117 @@ +import BaseBlock from './block' ; +import type { DescriptError } from './error'; +import { ERROR_ID, createError } from './error' ; +import type { BlockResultOut, InferResultFromBlock } from './types'; +import type ContextClass from './context'; +import type Cancel from './cancel'; +import type DepsDomain from './depsDomain'; + +type ArrayResults< T > = { + [ P in keyof T ]: T[P] extends { + key: number | string; + block: infer B; + } ? + InferResultFromBlock + : + never +} + +abstract class CompositeBlock< + Context, + CustomBlock, + ParamsOut, + ResultOut extends BlockResultOut, + IntermediateResult, + BlockResultInt, + + BeforeResultOut = undefined, + AfterResultOut = undefined, + ErrorResultOut = undefined, + Params = ParamsOut, +> extends BaseBlock< + Context, + CustomBlock, + ParamsOut, + ResultOut, + IntermediateResult, + BlockResultInt, + + BeforeResultOut, + AfterResultOut, + ErrorResultOut, + Params + > { + + protected blocks: Array<{ + key: string | number; + block: BaseBlock< + Context, CustomBlock, ParamsOut, ResultOut, IntermediateResult, BlockResultInt, BeforeResultOut, AfterResultOut, ErrorResultOut, Params + >; + }>; + + protected runBlocks({ runContext, blockCancel, cancel, params, context, nParents, depsDomain }: { + runContext: ContextClass; + blockCancel: Cancel; + cancel: Cancel; + params: ParamsOut; + context?: Context; + nParents: number; + depsDomain?: DepsDomain; + }) { + const promises = this.blocks.map(({ block, key }) => { + return runContext.run({ + block: block, + blockCancel: blockCancel.create(), + depsDomain: depsDomain, + params: params as unknown as Params, + context: context, + cancel: cancel, + nParents: nParents + 1, + }) + .catch((error) => { + if (block.isRequired()) { + error = createError({ + id: ERROR_ID.REQUIRED_BLOCK_FAILED, + path: getErrorPath(key, error), + reason: error, + }); + blockCancel.cancel(error); + + throw error; + } + + return error; + }); + }); + + return Promise.race([ + Promise.all(promises), + blockCancel.getPromise(), + ]) as Promise>; + } + + /* + block_action_started_in_context( run_context ) { + // Do nothing. + // console.log( 'composite.block_action_started_in_context', run_context._n_blocks, run_context._n_active_blocks ); + } + + block_action_stopped_in_context( context ) { + // Do nothing. + // console.log( 'composite.block_action_stopped_in_context', run_context._n_blocks, run_context._n_active_blocks ); + } + */ +} + +function getErrorPath(key: number | string, error: DescriptError) { + let r = (typeof key === 'number') ? `[ ${ key } ]` : `.${ key }`; + + const path = error.error.path; + if (path) { + r += path; + } + + return r; +} + +export default CompositeBlock; diff --git a/lib/composite_block.js b/lib/composite_block.js deleted file mode 100644 index 8f9f46d..0000000 --- a/lib/composite_block.js +++ /dev/null @@ -1,65 +0,0 @@ -const Block = require( './block' ); -const { ERROR_ID, create_error } = require( './error' ); - -class CompositeBlock extends Block { - - _run_blocks( { run_context, block_cancel, deps_domain, cancel, params, context, n_parents } ) { - const promises = this._blocks.map( ( { block, key } ) => { - return run_context.run( { - block: block, - block_cancel: block_cancel.create(), - deps_domain: deps_domain, - params: params, - context: context, - cancel: cancel, - n_parents: n_parents + 1, - } ) - .catch( ( error ) => { - if ( block._options.required ) { - error = create_error( { - id: ERROR_ID.REQUIRED_BLOCK_FAILED, - path: get_error_path( key, error ), - // FIXME: Что-то тут слишком много create_error. Нужно ли это?! - reason: create_error( error ), - } ); - block_cancel.cancel( error ); - - throw error; - } - - return error; - } ); - } ); - - return Promise.race( [ - Promise.all( promises ), - block_cancel.get_promise(), - ] ); - } - - /* - block_action_started_in_context( run_context ) { - // Do nothing. - // console.log( 'composite.block_action_started_in_context', run_context._n_blocks, run_context._n_active_blocks ); - } - - block_action_stopped_in_context( context ) { - // Do nothing. - // console.log( 'composite.block_action_stopped_in_context', run_context._n_blocks, run_context._n_active_blocks ); - } - */ -} - -function get_error_path( key, error ) { - let r = ( typeof key === 'number' ) ? `[ ${ key } ]` : `.${ key }`; - - const path = error.error.path; - if ( path ) { - r += path; - } - - return r; -} - -module.exports = CompositeBlock; - diff --git a/lib/context.js b/lib/context.js deleted file mode 100644 index 673555a..0000000 --- a/lib/context.js +++ /dev/null @@ -1,107 +0,0 @@ -const { ERROR_ID, create_error } = require( './error' ); - -const Block = require( './block' ); - -const get_deferred = require( './get_deferred' ); - -// --------------------------------------------------------------------------------------------------------------- // - -class Context { - - constructor() { - this.n_blocks = 0; - this.n_active_blocks = 0; - - this.block_promises = {}; - this.block_results = {}; - this.waiting_for_deps = []; - } - - get_promise( id ) { - let deferred = this.block_promises[ id ]; - if ( !deferred ) { - const result = this.block_results[ id ]; - if ( result ) { - if ( result.error ) { - return Promise.reject( result.error ); - - } else { - return Promise.resolve( result.result ); - } - } - - deferred = this.block_promises[ id ] = get_deferred(); - } - - return deferred.promise; - } - - resolve_promise( id, result ) { - this.block_results[ id ] = { - result: result, - }; - - const deferred = this.block_promises[ id ]; - if ( deferred ) { - deferred.resolve( result ); - } - } - - reject_promise( id, error ) { - this.block_results[ id ] = { - error: error, - }; - - const deferred = this.block_promises[ id ]; - if ( deferred ) { - deferred.reject( error ); - } - } - - queue_deps_check() { - process.nextTick( () => { - if ( this.waiting_for_deps.length > 0 ) { - this.waiting_for_deps.forEach( ( { block_cancel, n_parents } ) => { - if ( this.n_blocks > 0 && this.n_active_blocks <= n_parents ) { - // console.log( 'DEPS FAILED', this.n_blocks, this.n_active_blocks, n_parents ); - const error = create_error( { - id: ERROR_ID.DEPS_NOT_RESOLVED, - } ); - block_cancel.cancel( error ); - } - } ); - } - } ); - } - - async run( { block, block_cancel, deps_domain, params, context, cancel, prev, n_parents = 0 } ) { - // FIXME: А может block быть промисом? - if ( block instanceof Block ) { - // На тот случай, когда у нас запускается один блок и у него сразу есть зависимости. - this.queue_deps_check(); - - block = await block._run( { - run_context: this, - block_cancel: block_cancel, - deps_domain: deps_domain, - params: params, - context: context, - cancel: cancel, - prev: prev, - n_parents: n_parents, - } ); - block_cancel.throw_if_cancelled(); - - } else { - block_cancel.close(); - } - - return block; - } - -} - -// --------------------------------------------------------------------------------------------------------------- // - -module.exports = Context; - diff --git a/lib/context.ts b/lib/context.ts new file mode 100644 index 0000000..d545bb9 --- /dev/null +++ b/lib/context.ts @@ -0,0 +1,190 @@ +import type { DescriptError } from './error'; +import { ERROR_ID, createError } from './error' ; + +import BlockClass from './block' ; + +import type { Deffered } from './getDeferred'; +import getDeferred from './getDeferred'; +import type { DescriptBlockId } from './depsDomain'; +import type Cancel from './cancel'; +import type DepsDomain from './depsDomain'; +import type { BlockResultOut, InferResultOrResult } from './types'; +import type BaseBlock from './block'; + +// --------------------------------------------------------------------------------------------------------------- // + +type StoredResult = { + error: Error; +} | { + result: Result; +} + +type Dependency = {blockCancel: Cancel; nParents: number} + +class RunContext< + BlockResult, + IntermediateResult, + ResultOut extends BlockResultOut, + Context, + BeforeResultOut = undefined, + AfterResultOut = undefined, + ErrorResultOut = undefined, +> { + + private nBlocks = 0; + private nActiveBlocks = 0; + + private blockPromises: Record> = {}; + private blockResults: Record> = {}; + + private waitingForDeps: Array = []; + + getWaitingDeps() { + return this.waitingForDeps; + } + + addWaitingDeps(dep: Dependency) { + this.waitingForDeps.push(dep); + } + removeWaitingDeps(blockCancel: Dependency['blockCancel']) { + this.waitingForDeps = this.waitingForDeps.filter(item => item.blockCancel !== blockCancel); + } + + getNumberOfBlocks() { + return this.nBlocks; + } + + incNumberOfBlocks() { + return this.nBlocks++; + } + decNumberOfBlocks() { + return this.nBlocks--; + } + + getNumberOfActiveBlocks() { + return this.nActiveBlocks; + } + + incNumberOfActiveBlocks() { + return this.nActiveBlocks++; + } + decNumberOfActiveBlocks() { + return this.nActiveBlocks--; + } + + getPromise(id: DescriptBlockId) { + let deferred = this.blockPromises[ id ]; + if (!deferred) { + const result = this.blockResults[ id ]; + + if (result) { + if ('error' in result) { + return Promise.reject(result.error); + + } else { + return Promise.resolve(result.result); + } + } + + deferred = this.blockPromises[ id ] = getDeferred(); + } + + return deferred.promise; + } + + resolvePromise(id: DescriptBlockId, result: ResultOut) { + this.blockResults[ id ] = { + result: result, + }; + + const deferred = this.blockPromises[ id ]; + + if (deferred) { + deferred.resolve(result); + } + } + + rejectPromise(id: DescriptBlockId, error: DescriptError) { + this.blockResults[ id ] = { + error: error, + }; + + const deferred = this.blockPromises[ id ]; + if (deferred) { + deferred.reject(error); + } + } + + async run< + CustomBlock, + ParamsOut, + Params = ParamsOut, + >( + { block, blockCancel, depsDomain, params, context, cancel, prev, nParents = 0 }: + { + block: BaseBlock; + blockCancel: Cancel; + depsDomain?: DepsDomain; + params?: Params; + context?: Context; + cancel: Cancel; + prev?: unknown; + nParents?: number; + }): Promise> { + // FIXME: А может block быть промисом? + if (block instanceof BlockClass) { + // На тот случай, когда у нас запускается один блок и у него сразу есть зависимости. + this.queueDepsCheck(); + + const blockResult = await block.run({ + runContext: this, + blockCancel: blockCancel, + depsDomain: depsDomain, + params: params, + context: context, + cancel: cancel, + prev: prev, + nParents: nParents, + }); + blockCancel.throwIfCancelled(); + + return blockResult as Promise>; + + } else { + blockCancel.close(); + } + + return block as Promise>; + } + + queueDepsCheck() { + process.nextTick(() => { + if (this.waitingForDeps.length > 0) { + this.waitingForDeps.forEach(({ blockCancel, nParents }) => { + if (this.nBlocks > 0 && this.nActiveBlocks <= nParents) { + // console.log( 'DEPS FAILED', this.nBlocks, this.nActiveBlocks, nParents ); + const error = createError(ERROR_ID.DEPS_NOT_RESOLVED); + blockCancel.cancel(error); + } + }); + } + }); + } + +} + +// --------------------------------------------------------------------------------------------------------------- // + +export default RunContext; diff --git a/lib/depsDomain.ts b/lib/depsDomain.ts new file mode 100644 index 0000000..bedc2c9 --- /dev/null +++ b/lib/depsDomain.ts @@ -0,0 +1,23 @@ +//TODO как это типизировать any этот? +export type DescriptBlockDeps = Record; +export type DescriptBlockId = symbol; +class DepsDomain { + ids: Record; + constructor(parent: any) { + this.ids = (parent instanceof DepsDomain) ? Object.create(parent.ids) : {}; + + } + + generateId = (label?: string): DescriptBlockId => { + const id = Symbol(label); + this.ids[ id ] = true; + return id; + }; + + isValidId(id: DescriptBlockId) { + return Boolean(this.ids[ id ]); + } + +} + +export default DepsDomain; diff --git a/lib/deps_domain.js b/lib/deps_domain.js deleted file mode 100644 index d13a0b8..0000000 --- a/lib/deps_domain.js +++ /dev/null @@ -1,21 +0,0 @@ -class DepsDomain { - constructor( parent ) { - this.ids = ( parent instanceof DepsDomain ) ? Object.create( parent.ids ) : {}; - - this.generate_id = this.generate_id.bind( this ); - } - - generate_id( label ) { - const id = Symbol( label ); - this.ids[ id ] = true; - return id; - } - - is_valid_id( id ) { - return Boolean( this.ids[ id ] ); - } - -} - -module.exports = DepsDomain; - diff --git a/lib/error.js b/lib/error.js deleted file mode 100644 index 9a7e4dd..0000000 --- a/lib/error.js +++ /dev/null @@ -1,93 +0,0 @@ -class DescriptError { - constructor( error ) { - this.error = error; - } -} - -// --------------------------------------------------------------------------------------------------------------- // - -const ERROR_ID = { - ALL_BLOCKS_FAILED: 'ALL_BLOCKS_FAILED', - BLOCK_TIMED_OUT: 'BLOCK_TIMED_OUT', - DEPS_ERROR: 'DEPS_ERROR', - DEPS_NOT_RESOLVED: 'DEPS_NOT_RESOLVED', - HTTP_REQUEST_ABORTED: 'HTTP_REQUEST_ABORTED', - HTTP_UNKNOWN_ERROR: 'HTTP_UNKNOWN_ERROR', - INCOMPLETE_RESPONSE: 'INCOMPLETE_RESPONSE', - INVALID_BLOCK: 'INVALID_BLOCK', - INVALID_DEPS_ID: 'INVALID_DEPS_ID', - INVALID_JSON: 'INVALID_JSON', - INVALID_OPTIONS_PARAMS: 'INVALID_OPTIONS_PARAMS', - PARSE_BODY_ERROR: 'PARSE_BODY_ERROR', - REQUEST_TIMEOUT: 'REQUEST_TIMEOUT', - REQUIRED_BLOCK_FAILED: 'REQUIRED_BLOCK_FAILED', - TCP_CONNECTION_TIMEOUT: 'TCP_CONNECTION_TIMEOUT', - TOO_MANY_AFTERS_OR_ERRORS: 'TOO_MANY_AFTERS_OR_ERRORS', - UNKNOWN_ERROR: 'UNKNOWN_ERROR', -}; - -// --------------------------------------------------------------------------------------------------------------- // - -function create_error( error, id ) { - if ( is_error( error ) ) { - return error; - } - - if ( error instanceof Error ) { - const _error = { - id: id || error.name, - message: error.message, - stack: error.stack, - }; - - if ( error.errno ) { - _error.errno = error.errno; - } - if ( error.code ) { - _error.code = error.code; - } - if ( error.syscall ) { - _error.syscall = error.syscall; - } - - error = _error; - - } else if ( typeof error === 'string' ) { - error = { - id: error, - }; - } - - if ( !error ) { - error = {}; - } - - if ( !error.id ) { - error.id = ERROR_ID.UNKNOWN_ERROR; - } - - return new DescriptError( error ); -} - -// --------------------------------------------------------------------------------------------------------------- // - -function is_error( error, id ) { - if ( error instanceof DescriptError ) { - if ( id ) { - return ( error.error.id === id ); - } - - return true; - } - - return false; -} - -// --------------------------------------------------------------------------------------------------------------- // - -module.exports = { - ERROR_ID: ERROR_ID, - create_error: create_error, - is_error: is_error, -}; - diff --git a/lib/error.ts b/lib/error.ts new file mode 100644 index 0000000..0a0e3e5 --- /dev/null +++ b/lib/error.ts @@ -0,0 +1,148 @@ +import type { OutgoingHttpHeaders } from 'http'; + +type ArbitraryObject = { [key: string]: unknown }; + +function isErrnoException(error: unknown): error is NodeJS.ErrnoException { + return isArbitraryObject(error) && + error instanceof Error && + (typeof error.errno === 'number' || typeof error.errno === 'undefined') && + (typeof error.code === 'string' || typeof error.code === 'undefined') && + (typeof error.path === 'string' || typeof error.path === 'undefined') && + (typeof error.syscall === 'string' || typeof error.syscall === 'undefined'); +} + +function isArbitraryObject(potentialObject: unknown): potentialObject is ArbitraryObject { + return typeof potentialObject === 'object' && potentialObject !== null; +} + +export enum ERROR_ID { + ALL_BLOCKS_FAILED = 'ALL_BLOCKS_FAILED', + BLOCK_TIMED_OUT = 'BLOCK_TIMED_OUT', + DEPS_ERROR = 'DEPS_ERROR', + DEPS_NOT_RESOLVED = 'DEPS_NOT_RESOLVED', + HTTP_REQUEST_ABORTED = 'HTTP_REQUEST_ABORTED', + HTTP_UNKNOWN_ERROR = 'HTTP_UNKNOWN_ERROR', + INCOMPLETE_RESPONSE = 'INCOMPLETE_RESPONSE', + INVALID_BLOCK = 'INVALID_BLOCK', + INVALID_DEPS_ID = 'INVALID_DEPS_ID', + INVALID_JSON = 'INVALID_JSON', + INVALID_OPTIONS_PARAMS = 'INVALID_OPTIONS_PARAMS', + PARSE_BODY_ERROR = 'PARSE_BODY_ERROR', + REQUEST_TIMEOUT = 'REQUEST_TIMEOUT', + REQUIRED_BLOCK_FAILED = 'REQUIRED_BLOCK_FAILED', + TCP_CONNECTION_TIMEOUT = 'TCP_CONNECTION_TIMEOUT', + TOO_MANY_AFTERS_OR_ERRORS = 'TOO_MANY_AFTERS_OR_ERRORS', + UNKNOWN_ERROR = 'UNKNOWN_ERROR', +} + +export interface ErrorData { + id?: string | ERROR_ID; + message?: string; + // для http-ошибок + body?: Error; + headers?: OutgoingHttpHeaders; + statusCode?: number; + + errno?: number | undefined; + code?: string | undefined; + path?: string | undefined; + syscall?: string | undefined; + + stack?: string; + error?: unknown; + reason?: Reason | Array; +} + + +export type IncomingError = NodeJS.ErrnoException | Error | (ErrorData & {name?: string}) | string + +export class DescriptError { + error: ErrorData; + constructor(error: IncomingError, id?: ERROR_ID) { + if (typeof error === 'string') { + if (!id) { + this.error = { + id: error, + message: '', + }; + } else { + this.error = { + id: id, + message: error, + }; + } + } else if (error instanceof Error) { + const _error: ErrorData = { + id: id || error.name, + message: error.message, + stack: error.stack, + }; + + if (isErrnoException(error)) { + if (error.errno) { + _error.errno = error.errno; + } + if (error.code) { + _error.code = error.code; + } + if (error.syscall) { + _error.syscall = error.syscall; + } + } + + this.error = _error; + } else { + const _error: ErrorData = { + id: id || error.name || error.id, + body: error.body, + headers: error.headers, + statusCode: error.statusCode, + error: error.error, + reason: error.reason, + path: error.path, + code: error.code, + message: error.message, + stack: error.stack, + errno: error.errno, + syscall: error.syscall, + }; + + this.error = _error; + } + + if (!error) { + this.error = { + id: ERROR_ID.UNKNOWN_ERROR, + message: '', + }; + } + + if (!this.error?.id || this.error?.id === 'Error') { + this.error.id = ERROR_ID.UNKNOWN_ERROR; + } + } +} + +export type Reason = DescriptError | string | ERROR_ID | IncomingError; + +export function createError(error: Reason, id?: ERROR_ID): DescriptError { + if (isError(error, id)) { + return error; + } + + return new DescriptError(error, id); +} + +export function isError(error: any, id?: ERROR_ID): error is DescriptError { + if (error instanceof DescriptError) { + if (id) { + return (error.error.id === id); + } + + return true; + } + + return false; +} + +// --------------------------------------------------------------------------------------------------------------- // diff --git a/lib/extend.js b/lib/extend.js deleted file mode 100644 index 0668372..0000000 --- a/lib/extend.js +++ /dev/null @@ -1,16 +0,0 @@ -module.exports = function( dest ) { - for ( let i = 1, l = arguments.length; i < l; i++ ) { - const src = arguments[ i ]; - if ( src ) { - for ( const key in src ) { - const value = src[ key ]; - if ( value !== undefined ) { - dest[ key ] = value; - } - } - } - } - - return dest; -}; - diff --git a/lib/extend.ts b/lib/extend.ts new file mode 100644 index 0000000..b24d856 --- /dev/null +++ b/lib/extend.ts @@ -0,0 +1,16 @@ +import type { UnionToIntersection } from './types'; + +export default function extend(dest: Record, ...srcObjects: Array>) { + for (const src of srcObjects) { + if (src) { + for (const key in src) { + const value = src[ key ]; + if (value !== undefined) { + dest[ key ] = value; + } + } + } + } + + return dest as UnionToIntersection ? O : never)>; +} diff --git a/lib/extendOption.ts b/lib/extendOption.ts new file mode 100644 index 0000000..04810b0 --- /dev/null +++ b/lib/extendOption.ts @@ -0,0 +1,19 @@ +type InferArrayItemOrT = T extends Array ? I : T + +export default function extendOption(what?: T, by?: P): Array | InferArrayItemOrT

> | null { + const newArray: Array = []; + + if (what) { + if (by) { + return newArray.concat(what, by); + } + + return newArray.concat(what); + } + + if (by) { + return newArray.concat(by); + } + + return null; +} diff --git a/lib/extend_option.js b/lib/extend_option.js deleted file mode 100644 index 24e01cc..0000000 --- a/lib/extend_option.js +++ /dev/null @@ -1,16 +0,0 @@ -module.exports = function extend_option( what, by ) { - if ( what ) { - if ( by ) { - return [].concat( what, by ); - } - - return [].concat( what ); - } - - if ( by ) { - return [].concat( by ); - } - - return null; -}; - diff --git a/lib/firstBlock.ts b/lib/firstBlock.ts new file mode 100644 index 0000000..ad472bd --- /dev/null +++ b/lib/firstBlock.ts @@ -0,0 +1,174 @@ +import CompositeBlock from './compositeBlock' ; +import { ERROR_ID, createError } from './error'; +import type { DescriptError } from './error'; +import type BaseBlock from './block'; +import type { + BlockResultOut, + First, + InferResultFromBlock, + InferParamsInFromBlock, + Tail, + DescriptBlockOptions, +} from './types'; + +import type ContextClass from './context'; +import type Cancel from './cancel'; +import type { DescriptBlockDeps } from './depsDomain'; +import type DepsDomain from './depsDomain'; + +type GetFirstBlockParamsMap< T extends ReadonlyArray> = { + [ P in keyof T ]: InferParamsInFromBlock; +} + +export type GetFirstBlockParamsUnion< T extends ReadonlyArray> = { + 0: never; + 1: First< T >; + 2: First< T > & GetFirstBlockParamsUnion< Tail< T > >; +}[ T extends [] ? 0 : T extends ((readonly [ any ]) | [ any ]) ? 1 : 2 ]; + +export type GetFirstBlockParams< + T extends ReadonlyArray, + PA extends ReadonlyArray = GetFirstBlockParamsMap, + PU = GetFirstBlockParamsUnion +> = PU; + + +type GetFirstBlockResultMap< T extends ReadonlyArray> = { + [ P in keyof T ]: InferResultFromBlock; +} + +type GetFirstBlockResultUnion< T extends ReadonlyArray> = { + 0: never; + 1: First< T > | DescriptError; + 2: First< T > | DescriptError | GetFirstBlockResultUnion< Tail< T > >; +}[ T extends [] ? 0 : T extends ((readonly [ any ]) | [ any ]) ? 1 : 2 ]; + +export type GetFirstBlockResult< + T extends ReadonlyArray, + PA extends ReadonlyArray = GetFirstBlockResultMap, + PU = GetFirstBlockResultUnion +> = PU; + +export type FirstBlockDefinition< T > = { + [ P in keyof T ]: T[ P ] extends BaseBlock< + // eslint-disable-next-line @typescript-eslint/no-unused-vars + infer Context, infer CustomBlock, infer ParamsOut, infer ResultOut, infer IntermediateResult, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + infer BlockResult, infer BeforeResultOut, infer AfterResultOut, infer ErrorResultOut, infer Params + > ? T[ P ] : never +} + +class FirstBlock< + Context, + Block extends ReadonlyArray, + ResultOut extends BlockResultOut, + ParamsOut = GetFirstBlockParams, + BlockResult = GetFirstBlockResult, + + BeforeResultOut = undefined, + AfterResultOut = undefined, + ErrorResultOut = undefined, + Params = GetFirstBlockParams, +> extends CompositeBlock< + Context, + FirstBlockDefinition, + ParamsOut, + ResultOut, + BlockResult, + BlockResult, + + BeforeResultOut, + AfterResultOut, + ErrorResultOut, + Params + > { + + extend< + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ExtendedResultOut extends + BlockResultOut, + ExtendedParamsOut = Params, + ExtendedParams = Params, + ExtendedBlockResult = ResultOut, + ExtendedBeforeResultOut = undefined, + ExtendedAfterResultOut = undefined, + ExtendedErrorResultOut = undefined, + >({ options }: { + options: DescriptBlockOptions< + Context, Params & ExtendedParamsOut, ExtendedBlockResult, ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams + >; + }) { + return this.extendClass< + FirstBlock< + Context, + Block, + ExtendedResultOut, + Params & ExtendedParamsOut, + ExtendedBlockResult, + ExtendedBeforeResultOut, + ExtendedAfterResultOut, + ExtendedErrorResultOut, + ExtendedParams + >, + ExtendedBlockResult, + ExtendedParamsOut, + ExtendedParams, + ExtendedBeforeResultOut, + ExtendedAfterResultOut, + ExtendedErrorResultOut + >({ options }); + } + + protected initBlock(block: FirstBlockDefinition) { + if (!Array.isArray(block)) { + throw createError({ + id: ERROR_ID.INVALID_BLOCK, + message: 'block must be an array', + }); + } + + super.initBlock(block); + } + + protected async blockAction({ runContext, blockCancel, cancel, params, context, nParents, depsDomain }: { + runContext: ContextClass; + blockCancel: Cancel; + cancel: Cancel; + params: ParamsOut; + context?: Context; + deps: DescriptBlockDeps; + nParents: number; + depsDomain?: DepsDomain; + }): Promise { + let prev: Array = []; + + for (let i = 0; i < this.block.length; i++) { + const block = this.block[ i ]; + + try { + const result = await runContext.run({ + block: block, + blockCancel: blockCancel.create(), + depsDomain, + params: params, + context: context, + cancel: cancel, + prev: prev, + nParents: nParents + 1, + }); + + return result as Promise; + + } catch (e) { + prev = prev.concat(e); + } + } + + throw createError({ + id: ERROR_ID.ALL_BLOCKS_FAILED, + reason: prev, + }); + } +} + +export default FirstBlock; diff --git a/lib/first_block.js b/lib/first_block.js deleted file mode 100644 index 2610075..0000000 --- a/lib/first_block.js +++ /dev/null @@ -1,51 +0,0 @@ -const Block = require( './block' ); -const { ERROR_ID, create_error } = require( './error' ); - -class FirstBlock extends Block { - - _init_block( array ) { - if ( !Array.isArray( array ) ) { - throw create_error( { - id: ERROR_ID.INVALID_BLOCK, - message: 'block must be an array', - } ); - } - - super._init_block( array ); - } - - async _action( { run_context, block_cancel, deps_domain, cancel, params, context, n_parents } ) { - let prev = []; - - for ( let i = 0; i < this._block.length; i++ ) { - const block = this._block[ i ]; - - try { - const result = await run_context.run( { - block: block, - block_cancel: block_cancel.create(), - deps_domain: deps_domain, - params: params, - context: context, - cancel: cancel, - prev: prev, - n_parents: n_parents + 1, - } ); - - return result; - - } catch ( e ) { - prev = prev.concat( e ); - } - } - - throw create_error( { - id: ERROR_ID.ALL_BLOCKS_FAILED, - reason: prev, - } ); - } - -} - -module.exports = FirstBlock; - diff --git a/lib/functionBlock.ts b/lib/functionBlock.ts new file mode 100644 index 0000000..a326916 --- /dev/null +++ b/lib/functionBlock.ts @@ -0,0 +1,133 @@ +import BaseBlock from './block'; +import type { DescriptBlockDeps } from './depsDomain'; +import DepsDomain from './depsDomain'; +import { createError, ERROR_ID } from './error'; +import type { BlockResultOut, DescriptBlockOptions } from './types'; +import type ContextClass from './context'; +import type Cancel from './cancel'; + +export type FunctionBlockDefinition< + Context, + Params, + BlockResult, +> = (args: { + params: Params; + context: Context; + deps: DescriptBlockDeps; + generateId: DepsDomain['generateId']; + cancel: Cancel; + blockCancel: Cancel; +}) => Promise | BlockResult; + +class FunctionBlock< + Context, + ParamsOut, + BlockResult, + ResultOut extends BlockResultOut, + BeforeResultOut = undefined, + AfterResultOut = undefined, + ErrorResultOut = undefined, + Params = ParamsOut +> extends BaseBlock< + Context, + FunctionBlockDefinition, + ParamsOut, + ResultOut, + BlockResult, + BlockResult, + + BeforeResultOut, + AfterResultOut, + ErrorResultOut, + Params + > { + + protected initBlock(block: FunctionBlockDefinition) { + if (typeof block !== 'function') { + throw createError({ + id: ERROR_ID.INVALID_BLOCK, + message: 'block must be a function', + }); + } + + super.initBlock(block); + } + + protected async blockAction({ runContext, blockCancel, cancel, params, context, deps, nParents, depsDomain }: { + runContext: ContextClass; + blockCancel: Cancel; + cancel: Cancel; + params: ParamsOut; + context: Context; + deps: DescriptBlockDeps; + nParents: number; + depsDomain?: DepsDomain; + }): Promise { + depsDomain = new DepsDomain(depsDomain); + + const result = await Promise.race([ + this.block({ + blockCancel: blockCancel, + cancel: cancel, + params: params, + context: context, + deps: deps, + generateId: depsDomain.generateId, + }), + blockCancel.getPromise(), + ]) as BlockResult; + + if (result instanceof BaseBlock) { + return await runContext.run({ + block: result, + blockCancel: blockCancel.create(), + depsDomain: depsDomain, + cancel: cancel, + params: params, + context: context, + nParents: nParents + 1, + }) as BlockResult; + } + + return result; + } + + extend< + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ExtendedResultOut extends + BlockResultOut, + ExtendedParamsOut = Params, + ExtendedParams = Params, + ExtendedBlockResult = ResultOut, + ExtendedBeforeResultOut = undefined, + ExtendedAfterResultOut = undefined, + ExtendedErrorResultOut = undefined, + >({ options }: { + options: DescriptBlockOptions< + Context, Params & ExtendedParamsOut, ExtendedBlockResult, ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams + >; + }) { + return this.extendClass< + FunctionBlock< + Context, + //FNBlock, + //FunctionBlockDefinition, + Params & ExtendedParamsOut, + ExtendedBlockResult, + ExtendedResultOut, + ExtendedBeforeResultOut, + ExtendedAfterResultOut, + ExtendedErrorResultOut, + ExtendedParams + >, + ExtendedBlockResult, + Params & ExtendedParamsOut, + ExtendedParams, + ExtendedBeforeResultOut, + ExtendedAfterResultOut, + ExtendedErrorResultOut + >({ options }); + } +} + +export default FunctionBlock; diff --git a/lib/function_block.js b/lib/function_block.js deleted file mode 100644 index 9b73f5e..0000000 --- a/lib/function_block.js +++ /dev/null @@ -1,51 +0,0 @@ -const Block = require( './block' ); -const DepsDomain = require( './deps_domain' ); -const { create_error, ERROR_ID } = require( './error' ); - -class FunctionBlock extends Block { - - _init_block( block ) { - if ( typeof block !== 'function' ) { - throw create_error( { - id: ERROR_ID.INVALID_BLOCK, - message: 'block must be a function', - } ); - } - - super._init_block( block ); - } - - async _action( { run_context, block_cancel, deps_domain, cancel, params, context, deps, n_parents } ) { - deps_domain = new DepsDomain( deps_domain ); - - const result = await Promise.race( [ - this._block( { - block_cancel: block_cancel, - cancel: cancel, - params: params, - context: context, - deps: deps, - generate_id: deps_domain.generate_id, - } ), - block_cancel.get_promise(), - ] ); - - if ( result instanceof Block ) { - return run_context.run( { - block: result, - block_cancel: block_cancel.create(), - deps_domain: deps_domain, - cancel: cancel, - params: params, - context: context, - n_parents: n_parents + 1, - } ); - } - - return result; - } - -} - -module.exports = FunctionBlock; - diff --git a/lib/getDeferred.ts b/lib/getDeferred.ts new file mode 100644 index 0000000..9e1438f --- /dev/null +++ b/lib/getDeferred.ts @@ -0,0 +1,26 @@ +type CustomPromise = { + catch( + onrejected?: ((reason: F) => TResult | PromiseLike) | undefined | null + ): Promise; +} & Promise; + +export type Deffered = { + promise: CustomPromise; + resolve: (value: R | PromiseLike) => void; + reject: (reason?: C) => void; +} + +export default function getDeferred(): Deffered { + let resolve: ((value: R | PromiseLike) => void) | null = null; + let reject: ((reason?: C) => void) | null = null; + const promise = new Promise(function(_resolve, _reject) { + resolve = _resolve; + reject = _reject; + }); + + return { + promise: promise, + resolve: resolve as NonNullable, + reject: reject as NonNullable, + }; +} diff --git a/lib/get_deferred.js b/lib/get_deferred.js deleted file mode 100644 index 41f17c9..0000000 --- a/lib/get_deferred.js +++ /dev/null @@ -1,15 +0,0 @@ -module.exports = function get_deferred() { - let resolve; - let reject; - const promise = new Promise( function( _resolve, _reject ) { - resolve = _resolve; - reject = _reject; - } ); - - return { - promise: promise, - resolve: resolve, - reject: reject, - }; -}; - diff --git a/lib/httpBlock.ts b/lib/httpBlock.ts new file mode 100644 index 0000000..f394192 --- /dev/null +++ b/lib/httpBlock.ts @@ -0,0 +1,580 @@ +import Block from './block' ; +import { ERROR_ID, createError } from './error' ; + +import type { DescriptRequestOptions } from './request'; +import request from './request' ; + +import extend from './extend' ; +import extendOption from './extendOption' ; +import stripNullAndUndefinedValues from './stripNullAndUndefinedValues' ; +import type { DescriptBlockDeps } from './depsDomain'; +import type { BlockResultOut, DescriptHttpBlockResult, DescriptBlockOptions, DescriptHttpResult, DescriptHttpBlockHeaders, DescriptJSON } from './types'; +import type ContextClass from './context'; +import type Cancel from './cancel'; +import type DepsDomain from './depsDomain'; +import type Logger from './logger'; + +// --------------------------------------------------------------------------------------------------------------- // + +const rxIsJson = /^application\/json(?:;|\s|$)/; + +type DescriptHttpBlockDescriptionCallback< T, Params, Context > = T | ((args: { + params: Params; + context: Context; + deps: DescriptBlockDeps; +}) => T); + +type DescriptHttpBlockQueryValue = string | number | boolean | undefined | null | Array; +type DescriptHttpBlockQuery = Record< string, DescriptHttpBlockQueryValue >; + +type HttpQuery = Record< +string, +DescriptHttpBlockQueryValue | +((args: { + params: Params; + context: Context; + deps: DescriptBlockDeps; + query: DescriptHttpBlockQuery; +}) => DescriptHttpBlockQueryValue) +> | +( + (args: { + params: Params; + context: Context; + deps: DescriptBlockDeps; + query: DescriptHttpBlockQuery; + }) => DescriptHttpBlockQuery +); + +type HttpHeaders = Record< string, +string | +((args: { + params: Params; + context: Context; + deps: DescriptBlockDeps; + headers: DescriptHttpBlockHeaders; +}) => string) +> | +( + (args: { + params: Params; + context: Context; + deps: DescriptBlockDeps; + headers: DescriptHttpBlockHeaders; + }) => DescriptHttpBlockHeaders +); + +type HttpBody = string | +Buffer | +DescriptJSON | +((args: { + params: Params; + context: Context; + deps: DescriptBlockDeps; +}) => string | Buffer | DescriptJSON); + +export interface DescriptHttpBlockDescription< + Params, + Context, + HTTPResult +> extends Pick< + DescriptRequestOptions, + 'bodyCompress' | 'timeout' | 'isError' | 'isRetryAllowed' | 'maxRetries' | 'retryTimeout' | 'agent' | + 'auth' | 'ca' | 'cert' | 'ciphers' | 'key' | 'passphrase' | 'pfx' | 'rejectUnauthorized' | 'secureProtocol' | 'servername' + > { + protocol?: DescriptHttpBlockDescriptionCallback< string, Params, Context >; + hostname?: DescriptHttpBlockDescriptionCallback< string, Params, Context >; + port?: DescriptHttpBlockDescriptionCallback< number, Params, Context >; + method?: DescriptHttpBlockDescriptionCallback< string, Params, Context >; + pathname?: DescriptHttpBlockDescriptionCallback< string, Params, Context >; + + family?: DescriptHttpBlockDescriptionCallback< number, Params, Context >; + + query?: HttpQuery | Array>; + + headers?: HttpHeaders | Array>; + + + body?: HttpBody; + + isJson?: boolean; + + prepareRequestOptions?: (options: DescriptRequestOptions) => DescriptRequestOptions; + + parseBody?: (result: {body: DescriptHttpResult['body']; headers: DescriptHttpResult['headers']}, context: Context) => + HTTPResult; + +} + +const EVALUABLE_PROPS: Array> = [ + 'agent', + 'auth', + 'bodyCompress', + 'ca', + 'cert', + 'ciphers', + 'family', + 'hostname', + 'key', + 'maxRetries', + 'method', + 'passphrase', + 'pathname', + 'pfx', + 'port', + 'protocol', + 'rejectUnauthorized', + 'secureProtocol', + 'servername', + 'timeout', +]; + +type CallbackArgs = { + params: Params; + context: Context; + deps: DescriptBlockDeps; +} + +// --------------------------------------------------------------------------------------------------------------- // + +class HttpBlock< + Context, + ParamsOut, + HttpResult, + ResultOut extends BlockResultOut, + BlockResult = DescriptHttpBlockResult, + BeforeResultOut = undefined, + AfterResultOut = undefined, + ErrorResultOut = undefined, + Params = ParamsOut +> extends Block< + Context, + DescriptHttpBlockDescription, + ParamsOut, + ResultOut, + HttpResult, + BlockResult, + BeforeResultOut, + AfterResultOut, + ErrorResultOut, + Params + > { + + extend< + ExtendedResultOut extends + BlockResultOut, + ExtendedParamsOut = Params, + ExtendedParams = Params, + + ExtendedBlockResult = ResultOut, + ExtendedBeforeResultOut = undefined, + ExtendedAfterResultOut = undefined, + ExtendedErrorResultOut = undefined, + >({ options, block }: { + block?: DescriptHttpBlockDescription; + options?: DescriptBlockOptions< + Context, Params & ExtendedParamsOut, ExtendedBlockResult, ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams + >; + }) { + return this.extendClass< + HttpBlock< + Context, + ExtendedParamsOut, + HttpResult, + Params & ExtendedResultOut, + ExtendedBlockResult, + ExtendedBeforeResultOut, + ExtendedAfterResultOut, + ExtendedErrorResultOut, + ExtendedParams + >, + ExtendedBlockResult, + Params & ExtendedParamsOut, + ExtendedParams, + ExtendedBeforeResultOut, + ExtendedAfterResultOut, + ExtendedErrorResultOut + >({ options, block }); + } + + protected logger: Logger; + + constructor({ block, options }: { + block?: DescriptHttpBlockDescription; + + options?: DescriptBlockOptions; + + }) { + super({ block, options }); + + if (options && options.logger) { + this.logger = options.logger; + } + } + + protected initBlock(block: DescriptHttpBlockDescription) { + super.initBlock(block); + + // this._compiled_props = compile_props( this.block ); + } + + protected extendBlock(by: DescriptHttpBlockDescription = {}) { + const what = this.block; + const headers = extendOption(what.headers, by.headers); + const query = extendOption(what.query, by.query); + + const block = extend({}, what, by); + + if (headers) { + block.headers = headers; + } + if (query) { + block.query = query; + } + + return block; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + protected async blockAction({ runContext, blockCancel, cancel, params, context, deps, nParents, depsDomain }: { + runContext: ContextClass; + blockCancel: Cancel; + cancel: Cancel; + params: ParamsOut; + context: Context; + deps: DescriptBlockDeps; + nParents: number; + depsDomain?: DepsDomain; + }): Promise { + const block = this.block; + + const callbackArgs: CallbackArgs = { params, context, deps }; + + let options: DescriptRequestOptions = { + isError: block.isError, + isRetryAllowed: block.isRetryAllowed, + retryTimeout: block.retryTimeout, + body: null, + ...( + EVALUABLE_PROPS.reduce((ret, prop) => { + let value = block[prop]; + + if (typeof value === 'function') { + value = value(callbackArgs); + } + + if (value !== null) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + ret[ prop ] = value; + } + + return ret; + }, {} as { + [P in typeof EVALUABLE_PROPS extends Array ? K : never]: + DescriptRequestOptions[P] extends undefined ? never : DescriptRequestOptions[P] + }) + ), + }; + + + // TODO: Надо пострелять, чтобы понять, стоит ли городить эту оптимизацию. + // Блоки часто будут создаваться динамически, внутри замыкания с generate_id, + // так что стоимость компиляции может быть больше, чем просто тупой цикл. + // + // this._compiled_props( options, callbackArgs ); + + if (options.method) { + options.method = options.method.toUpperCase(); + } + + if (block.headers) { + options.headers = evalHeaders(block.headers, callbackArgs); + } + + if (block.query) { + options.query = evalQuery(block.query, callbackArgs); + } + + if (block.body !== undefined) { + options.body = evalBody(block.body, callbackArgs); + } + + if (this.options.name) { + options.extra = { + name: this.options.name, + }; + } + + if (typeof block.prepareRequestOptions === 'function') { + options = block.prepareRequestOptions(options); + } + + let result: DescriptHttpResult | undefined = undefined; + let headers; + let error; + + try { + result = await request(options, this.logger, blockCancel); + headers = result.headers; + + } catch (e) { + error = e.error; + headers = error.headers; + } + + if (error || !result) { + if (error.body) { + const result = { body: error.body, headers: headers }; + if (typeof block.parseBody === 'function') { + try { + error.body = block.parseBody(result, context); + + } catch (e) { + // Do nothing + } + + } else { + error.body = this.parseErrorBody(result); + } + } + + throw createError(error); + } + + let body: HttpResult | null = null; + if (typeof block.parseBody === 'function') { + try { + body = block.parseBody(result!, context); + + } catch (e) { + throw createError(e, ERROR_ID.PARSE_BODY_ERROR); + } + + } else if (result.body) { + body = this.parseBody(result); + } + + return { + statusCode: result.statusCode, + headers: result.headers, + requestOptions: result.requestOptions, + result: body, + } as BlockResult; + } + + protected parseBody({ body, headers }: {body: DescriptHttpResult['body']; headers: DescriptHttpResult['headers']}): HttpResult { + const isJson = this.isJsonResponse(headers); + if (isJson) { + try { + return JSON.parse(body as unknown as string); + + } catch (e) { + throw createError(e, ERROR_ID.INVALID_JSON); + } + } + + return String(body) as HttpResult; + } + + protected parseErrorBody({ body, headers }: { body: string | Buffer; headers: DescriptHttpBlockHeaders }) { + const isJson = this.isJsonResponse(headers); + if (isJson) { + try { + return JSON.parse(body.toString()); + + } catch (e) { + // Do nothing. + } + } + + if (Buffer.isBuffer(body)) { + return body.toString(); + } + + return body; + } + + private isJsonResponse(headers: DescriptHttpBlockHeaders) { + let isJson = this.block.isJson; + if (!isJson && headers) { + const contentType = headers[ 'content-type' ]; + + if (contentType) { + isJson = rxIsJson.test(contentType); + } + } + + return isJson; + } +} + +// --------------------------------------------------------------------------------------------------------------- // + +export default HttpBlock; + +// --------------------------------------------------------------------------------------------------------------- // + +function evalHeaders( + objects: HttpHeaders | Array>, + callbackArgs: CallbackArgs, +) { + let headers = {}; + + if (Array.isArray(objects)) { + objects.forEach((object) => { + headers = evalHeadersObject(headers, object, callbackArgs); + }); + + } else { + headers = evalHeadersObject(headers, objects, callbackArgs); + } + + return headers; +} + +function evalHeadersObject( + headers: DescriptHttpBlockHeaders, + object: HttpHeaders, + callbackArgs: CallbackArgs, +) { + const extendedCallbackArgs = { ...callbackArgs, headers }; + + if (typeof object === 'function') { + const newHeaders = object(extendedCallbackArgs); + if (newHeaders && typeof newHeaders === 'object') { + headers = newHeaders; + } + + } else { + headers = {}; + + for (const key in object) { + const value = object[ key ]; + headers[ key ] = (typeof value === 'function') ? value(extendedCallbackArgs) : value; + } + } + + return headers; +} + +// --------------------------------------------------------------------------------------------------------------- // + +function evalQuery( + objects: HttpQuery | Array>, + callbackArgs: CallbackArgs, +) { + let query = {}; + + if (Array.isArray(objects)) { + objects.forEach((object) => { + query = evalQueryObject(query, object, callbackArgs); + }); + + } else { + query = evalQueryObject(query, objects, callbackArgs); + } + + return query; +} + +function evalQueryObject( + query: DescriptHttpBlockQuery, + object: HttpQuery, + callbackArgs: CallbackArgs, +) { + const params = callbackArgs.params; + + const extendedCallbackArgs = { ...callbackArgs, query }; + + if (typeof object === 'function') { + const newQuery = object(extendedCallbackArgs); + if (newQuery && typeof newQuery === 'object') { + query = stripNullAndUndefinedValues(newQuery); + } + + } else { + query = {}; + + for (const key in object) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const pValue = params[ key ]; + const oValue = object[ key ]; + + let value; + if (oValue === null) { + value = pValue; + + } else if (typeof oValue === 'function') { + value = oValue(extendedCallbackArgs); + + } else if (oValue !== undefined) { + value = (pValue === undefined) ? oValue : pValue; + } + + if (value !== undefined) { + query[ key ] = value; + } + } + } + + return query; +} + +// --------------------------------------------------------------------------------------------------------------- // + +function evalBody(body: HttpBody, callbackArgs: CallbackArgs) { + if (typeof body === 'string' || Buffer.isBuffer(body)) { + return body; + } + + if (typeof body === 'function') { + return body(callbackArgs); + } + + return String(body); +} + +/* +function compile_props( block ) { + let js = 'var v;'; + PROPS.forEach( ( prop ) => { + const value = block[ prop ]; + if ( value != null ) { + if ( typeof value === 'function' ) { + js += `v=b["${ prop }"](a);`; + js += `if (v!=null){o["${ prop }"]=v}`; + + } else { + js += `o["${ prop }"]=b["${ prop }"];`; + } + } + } ); + + const compiled = Function( 'b', 'o', 'a', js ); + + return function( options, callbackArgs ) { + return compiled( block, options, callbackArgs ); + }; +} +*/ diff --git a/lib/http_block.js b/lib/http_block.js deleted file mode 100644 index 98b8ae7..0000000 --- a/lib/http_block.js +++ /dev/null @@ -1,350 +0,0 @@ -const Block = require( './block' ); -const { ERROR_ID, create_error } = require( './error' ); - -const request = require( './request' ); - -const extend = require( './extend' ); -const extend_option = require( './extend_option' ); -const strip_null_and_undefined_values = require( './strip_null_and_undefined_values' ); - -// --------------------------------------------------------------------------------------------------------------- // - -const rx_is_json = /^application\/json(?:;|\s|$)/; - -// FIXME: Нужен разделить список на статический и динамический. -// -const PROPS = [ - 'agent', - 'auth', - 'body_compress', - 'ca', - 'cert', - 'ciphers', - 'family', - 'hostname', - 'key', - 'max_retries', - 'method', - 'passphrase', - 'pathname', - 'pfx', - 'port', - 'protocol', - 'rejectUnauthorized', - 'secureProtocol', - 'servername', - 'timeout', -]; - -// --------------------------------------------------------------------------------------------------------------- // - -class HttpBlock extends Block { - - _init_block( block ) { - super._init_block( block ); - - // this._compiled_props = compile_props( this._block ); - } - - _extend_block( by = {} ) { - const what = this._block; - const headers = extend_option( what.headers, by.headers ); - const query = extend_option( what.query, by.query ); - - const block = extend( {}, what, by ); - if ( headers ) { - block.headers = headers; - } - if ( query ) { - block.query = query; - } - - return block; - } - - async _action( { run_context, block_cancel, deps_domain, cancel, params, context, deps } ) { - const block = this._block; - - const callback_args = { params, context, deps }; - - let options = { - is_error: block.is_error, - is_retry_allowed: block.is_retry_allowed, - retry_timeout: block.retry_timeout, - }; - - PROPS.forEach( ( prop ) => { - let value = block[ prop ]; - if ( typeof value === 'function' ) { - value = value( callback_args ); - } - if ( value != null ) { - options[ prop ] = value; - } - } ); - // TODO: Надо пострелять, чтобы понять, стоит ли городить эту оптимизацию. - // Блоки часто будут создаваться динамически, внутри замыкания с generate_id, - // так что стоимость компиляции может быть больше, чем просто тупой цикл. - // - // this._compiled_props( options, callback_args ); - - if ( options.method ) { - options.method = options.method.toUpperCase(); - } - - if ( block.headers ) { - options.headers = eval_headers( block.headers, callback_args ); - } - - if ( block.query ) { - options.query = eval_query( block.query, callback_args ); - } - - if ( block.body !== undefined ) { - options.body = eval_body( block.body, callback_args ); - } - - if ( this._options.name ) { - options.extra = { - name: this._options.name, - }; - } - - if ( typeof block.prepare_request_options === 'function' ) { - options = block.prepare_request_options( options ); - } - - let result; - let headers; - let error; - - try { - const logger = this._options.logger; - - result = await request( options, logger, context, block_cancel ); - headers = result.headers; - - } catch ( e ) { - error = e.error; - headers = error.headers; - } - - if ( error ) { - if ( error.body ) { - const result = { body: error.body, headers: headers }; - if ( typeof block.parse_body === 'function' ) { - try { - error.body = block.parse_body( result, context ); - - } catch ( e ) { - // Do nothing - } - - } else { - error.body = this._parse_error_body( result ); - } - } - - throw create_error( error ); - } - - let body = null; - if ( typeof block.parse_body === 'function' ) { - try { - body = block.parse_body( result, context ); - - } catch ( e ) { - throw create_error( e, ERROR_ID.PARSE_BODY_ERROR ); - } - - } else if ( result.body ) { - body = this._parse_body( result ); - } - - return { - status_code: result.status_code, - headers: result.headers, - request_options: result.request_options, - result: body, - }; - } - - _parse_body( { body, headers } ) { - const is_json = this._is_json_response( headers ); - if ( is_json ) { - try { - return JSON.parse( body ); - - } catch ( e ) { - throw create_error( e, ERROR_ID.INVALID_JSON ); - } - } - - return String( body ); - } - - _parse_error_body( { body, headers } ) { - const is_json = this._is_json_response( headers ); - if ( is_json ) { - try { - return JSON.parse( body ); - - } catch ( e ) { - // Do nothing. - } - } - - if ( Buffer.isBuffer( body ) ) { - return body.toString(); - } - - return body; - } - - _is_json_response( headers ) { - let is_json = this._block.is_json; - if ( !is_json && headers ) { - const content_type = headers[ 'content-type' ]; - if ( content_type ) { - is_json = rx_is_json.test( content_type ); - } - } - - return is_json; - } -} - -// --------------------------------------------------------------------------------------------------------------- // - -module.exports = HttpBlock; - -// --------------------------------------------------------------------------------------------------------------- // - -function eval_headers( objects, callback_args ) { - let headers = {}; - - if ( Array.isArray( objects ) ) { - objects.forEach( ( object ) => { - headers = eval_headers_object( headers, object, callback_args ); - } ); - - } else { - headers = eval_headers_object( headers, objects, callback_args ); - } - - return headers; -} - -function eval_headers_object( headers, object, callback_args ) { - callback_args = { ...callback_args, headers }; - - if ( typeof object === 'function' ) { - const new_headers = object( callback_args ); - if ( new_headers && typeof new_headers === 'object' ) { - headers = new_headers; - } - - } else { - headers = {}; - - for ( const key in object ) { - const value = object[ key ]; - headers[ key ] = ( typeof value === 'function' ) ? value( callback_args ) : value; - } - } - - return headers; -} - -// --------------------------------------------------------------------------------------------------------------- // - -function eval_query( objects, callback_args ) { - let query = {}; - - if ( Array.isArray( objects ) ) { - objects.forEach( ( object ) => { - query = eval_query_object( query, object, callback_args ); - } ); - - } else { - query = eval_query_object( query, objects, callback_args ); - } - - return query; -} - -function eval_query_object( query, object, callback_args ) { - const params = callback_args.params; - - callback_args = { ...callback_args, query }; - - if ( typeof object === 'function' ) { - const new_query = object( callback_args ); - if ( new_query && typeof new_query === 'object' ) { - query = strip_null_and_undefined_values( new_query ); - } - - } else { - query = {}; - - for ( const key in object ) { - const p_value = params[ key ]; - const o_value = object[ key ]; - - let value; - if ( o_value === null ) { - value = p_value; - - } else if ( typeof o_value === 'function' ) { - value = o_value( callback_args ); - - } else if ( o_value !== undefined ) { - value = ( p_value === undefined ) ? o_value : p_value; - } - - if ( value !== undefined ) { - query[ key ] = value; - } - } - } - - return query; -} - -// --------------------------------------------------------------------------------------------------------------- // - -function eval_body( body, callback_args ) { - if ( typeof body === 'string' || Buffer.isBuffer( body ) ) { - return body; - } - - if ( typeof body === 'function' ) { - return body( callback_args ); - } - - return String( body ); -} - -/* -function compile_props( block ) { - let js = 'var v;'; - PROPS.forEach( ( prop ) => { - const value = block[ prop ]; - if ( value != null ) { - if ( typeof value === 'function' ) { - js += `v=b["${ prop }"](a);`; - js += `if (v!=null){o["${ prop }"]=v}`; - - } else { - js += `o["${ prop }"]=b["${ prop }"];`; - } - } - } ); - - const compiled = Function( 'b', 'o', 'a', js ); - - return function( options, callback_args ) { - return compiled( block, options, callback_args ); - }; -} -*/ diff --git a/lib/index.d.ts b/lib/index.d2.jtxt similarity index 67% rename from lib/index.d.ts rename to lib/index.d2.jtxt index 86b9f2b..fdacaa9 100644 --- a/lib/index.d.ts +++ b/lib/index.d2.jtxt @@ -1,4 +1,4 @@ -import { ZlibOptions } from 'node:zlib'; +import type { ZlibOptions } from 'node:zlib'; type First< T > = T extends readonly [ infer First, ...infer Rest ] | [ infer First, ...infer Rest ] ? First : never; @@ -6,21 +6,14 @@ type First< T > = type Tail< T > = T extends readonly [ infer First, ...infer Rest ] | [ infer First, ...infer Rest ] ? Rest : never; -type Equal< A, B > = A extends B ? ( B extends A ? A : never ) : never; +type Equal< A, B > = A extends B ? (B extends A ? A : never) : never; -type UnionToIntersection< U > = ( - U extends any ? - ( k: U ) => void : - never - ) extends ( - ( k: infer I ) => void - ) ? I : never; -type InferContext = Type extends DescriptBlock< infer Context, infer ParamsIn, infer ResultIn, infer ParamsOut, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer ResultOut > ? Context : never; -type InferParamsIn = Type extends DescriptBlock< infer Context, infer ParamsIn, infer ResultIn, infer ParamsOut, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer ResultOut > ? ParamsIn : never; -type InferParamsOut = Type extends DescriptBlock< infer Context, infer ParamsIn, infer ResultIn, infer ParamsOut, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer ResultOut > ? ParamsOut : never; -type InferResultIn = Type extends DescriptBlock< infer Context, infer ParamsIn, infer ResultIn, infer ParamsOut, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer ResultOut > ? ResultIn : never; -type InferResultOut = Type extends DescriptBlock< infer Context, infer ParamsIn, infer ResultIn, infer ParamsOut, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer ResultOut > ? ResultOut : never; +type InferContext = Type extends DescriptBlock< infer Context, infer ParamsIn, infer ResultIn, infer ParamsOut, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer ResultOut > ? Context : never; +type InferParamsIn = Type extends DescriptBlock< infer Context, infer ParamsIn, infer ResultIn, infer ParamsOut, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer ResultOut > ? ParamsIn : never; +type InferParamsOut = Type extends DescriptBlock< infer Context, infer ParamsIn, infer ResultIn, infer ParamsOut, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer ResultOut > ? ParamsOut : never; +type InferResultIn = Type extends DescriptBlock< infer Context, infer ParamsIn, infer ResultIn, infer ParamsOut, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer ResultOut > ? ResultIn : never; +type InferResultOut = Type extends DescriptBlock< infer Context, infer ParamsIn, infer ResultIn, infer ParamsOut, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer ResultOut > ? ResultOut : never; type DescriptBlockParams = { @@ -40,23 +33,23 @@ type GetDescriptBlockResultIn = T extends DescriptBlockResult = T extends DescriptBlockResult ? ResultOut : T; type InferResultInFromBlockOrReturnResultIn = Type extends DescriptBlock< infer Context, infer ParamsIn, infer Result, infer ParamsOut, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer ResultOut > ? Result : Type; -type InferResultOutFromBlockOrReturnResultOut = Type extends DescriptBlock< infer Context, infer ParamsIn, infer Result, infer ParamsOut, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer ResultOut > ? ResultOut : Type; -type InferParamsFromBlockOrReturnParams = Block extends DescriptBlock< infer Context, infer ParamsIn, infer Result, infer ParamsOut, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer ResultOut > ? ParamsIn : Params; +type InferResultOutFromBlockOrReturnResultOut = Type extends DescriptBlock< infer Context, infer ParamsIn, infer Result, infer ParamsOut, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer ResultOut > ? ResultOut : Type; +type InferParamsFromBlockOrReturnParams = Block extends DescriptBlock< infer Context, infer ParamsIn, infer Result, infer ParamsOut, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer ResultOut > ? ParamsIn : Params; -type InferResultInFromBlocks = Block extends DescriptBlock< infer Context, infer ParamsIn, infer Result, infer ParamsOut, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer ResultOut > ? +type InferResultInFromBlocks = Block extends DescriptBlock< infer Context, infer ParamsIn, infer Result, infer ParamsOut, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer ResultOut > ? { [ K in keyof Result ]: InferResultInFromBlocks } : Block; type NotUnknown = NonNullable; -type InferResultOutFromBlocks = Block extends DescriptBlock< infer Context, infer ParamsIn, infer Result, infer ParamsOut, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer ResultOut > ? +type InferResultOutFromBlocks = Block extends DescriptBlock< infer Context, infer ParamsIn, infer Result, infer ParamsOut, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer ResultOut > ? { [ K in keyof ResultOut ]: InferResultOutFromBlocks } : Block; // --------------------------------------------------------------------------------------------------------------- // -import { OutgoingHttpHeaders } from 'http'; -import { +import type { OutgoingHttpHeaders } from 'http'; +import type { RequestOptions as HttpsRequestOptions, Agent as HttpsAgent, AgentOptions as HttpsAgentOptions, @@ -84,20 +77,20 @@ interface DescriptError { body?: Error; headers?: OutgoingHttpHeaders; status_code?: number; - } + }; } // --------------------------------------------------------------------------------------------------------------- // declare class Cancel { - cancel( reason: DescriptError ): void; + cancel(reason: DescriptError): void; } // --------------------------------------------------------------------------------------------------------------- // declare class Cache< Result, Context > { - get( args: { key: string, context: Context } ): Result | Promise< Result >; - set( args: { key: string, value: Result, maxage: number, context: Context } ): void; + get(args: { key: string; context: Context }): Result | Promise< Result >; + set(args: { key: string; value: Result; maxage: number; context: Context }): void; } // --------------------------------------------------------------------------------------------------------------- // @@ -128,7 +121,7 @@ interface DescriptHttpResult { } interface LoggerEvent { - type: Logger.EVENT; + type: EVENT; request_options: DescriptRequestOptions; @@ -147,7 +140,7 @@ interface LoggerEvent { } interface DescriptLogger< Context > { - log( event: LoggerEvent, context: Context ): void; + log(event: LoggerEvent, context: Context): void; } declare namespace Logger { @@ -183,42 +176,42 @@ interface DescriptBlockOptions< id?: DescriptBlockId; deps?: DescriptBlockId | Array< DescriptBlockId >; - params?: ( args: { - params: Params, - context: Context, - deps: DescriptBlockDeps, - } ) => ParamsOut; - - before?: ( args: { - params: ParamsOut, - context: Context, - deps: DescriptBlockDeps, - cancel: Cancel, - } ) => ComplexResult - - after?: ( args: { - params: ParamsOut, - context: Context, - deps: DescriptBlockDeps, - cancel: Cancel, - result: AfterResultIn, - } ) => ComplexResult - - error?: ( args: { - params: ParamsOut, - context: Context, - deps: DescriptBlockDeps, - cancel: Cancel, - error: DescriptError, - } ) => ComplexResult + params?: (args: { + params: Params; + context: Context; + deps: DescriptBlockDeps; + }) => ParamsOut; + + before?: (args: { + params: ParamsOut; + context: Context; + deps: DescriptBlockDeps; + cancel: Cancel; + }) => ComplexResult; + + after?: (args: { + params: ParamsOut; + context: Context; + deps: DescriptBlockDeps; + cancel: Cancel; + result: AfterResultIn; + }) => ComplexResult; + + error?: (args: { + params: ParamsOut; + context: Context; + deps: DescriptBlockDeps; + cancel: Cancel; + error: DescriptError; + }) => ComplexResult; timeout?: number; - key?: string | ( ( args: { - params: ParamsOut, - context: Context, - deps: DescriptBlockDeps, - } ) => string ); + key?: string | ((args: { + params: ParamsOut; + context: Context; + deps: DescriptBlockDeps; + }) => string); maxage?: number; cache?: Cache< ResultOut | GetDescriptBlockResultOut, Context >; @@ -230,11 +223,11 @@ interface DescriptBlockOptions< // --------------------------------------------------------------------------------------------------------------- // // HttpBlock -type DescriptHttpBlockDescriptionCallback< T, Params, Context > = T | ( ( args: { - params: Params, - context: Context, - deps: DescriptBlockDeps, -} ) => T ); +type DescriptHttpBlockDescriptionCallback< T, Params, Context > = T | ((args: { + params: Params; + context: Context; + deps: DescriptBlockDeps; +}) => T); type DescriptHttpBlockQueryValue = string | number | boolean | undefined | Array; type DescriptHttpBlockQuery = Record< string, DescriptHttpBlockQueryValue >; @@ -249,52 +242,52 @@ interface DescriptHttpBlockDescription< Params, Context > { pathname?: DescriptHttpBlockDescriptionCallback< string, Params, Context >; query?: - Record< string, - DescriptHttpBlockQueryValue | - null | - ( ( args: { - params: Params, - context: Context, - deps: DescriptBlockDeps, - query: DescriptHttpBlockQuery, - } ) => DescriptHttpBlockQueryValue ) - > | - ( - ( args: { - params: Params, - context: Context, - deps: DescriptBlockDeps, - query: DescriptHttpBlockQuery, - } ) => DescriptHttpBlockQuery - ); + Record< string, + DescriptHttpBlockQueryValue | + null | + ((args: { + params: Params; + context: Context; + deps: DescriptBlockDeps; + query: DescriptHttpBlockQuery; + }) => DescriptHttpBlockQueryValue) + > | + ( + (args: { + params: Params; + context: Context; + deps: DescriptBlockDeps; + query: DescriptHttpBlockQuery; + }) => DescriptHttpBlockQuery + ); headers?: - Record< string, - string | - ( ( args: { - params: Params, - context: Context, - deps: DescriptBlockDeps, - headers: DescriptHttpBlockHeaders, - } ) => string ) - > | - ( - ( args: { - params: Params, - context: Context, - deps: DescriptBlockDeps, - headers: DescriptHttpBlockHeaders, - } ) => DescriptHttpBlockHeaders - ); + Record< string, + string | + ((args: { + params: Params; + context: Context; + deps: DescriptBlockDeps; + headers: DescriptHttpBlockHeaders; + }) => string) + > | + ( + (args: { + params: Params; + context: Context; + deps: DescriptBlockDeps; + headers: DescriptHttpBlockHeaders; + }) => DescriptHttpBlockHeaders + ); body?: - string | - Buffer | - ( ( args: { - params: Params, - context: Context, - deps: DescriptBlockDeps, - } ) => string | Buffer | DescriptJSON ); + string | + Buffer | + ((args: { + params: Params; + context: Context; + deps: DescriptBlockDeps; + }) => string | Buffer | DescriptJSON); body_compress?: boolean | ZlibOptions; @@ -302,13 +295,13 @@ interface DescriptHttpBlockDescription< Params, Context > { timeout?: number; - is_error?: ( error: DescriptError, request_options: DescriptRequestOptions ) => boolean; + is_error?: (error: DescriptError, request_options: DescriptRequestOptions) => boolean; - is_retry_allowed?: ( error: DescriptError, request_options: DescriptRequestOptions ) => boolean; + is_retry_allowed?: (error: DescriptError, request_options: DescriptRequestOptions) => boolean; max_retries?: number; retry_timeout?: number; - prepare_request_options?: ( options: HttpsRequestOptions ) => HttpsRequestOptions; + prepare_request_options?: (options: HttpsRequestOptions) => HttpsRequestOptions; parse_body?: (result: { headers: Record< string, string >; body?: Buffer }, context: Context) => {}; family?: DescriptHttpBlockDescriptionCallback< number, Params, Context >; @@ -348,23 +341,23 @@ interface DescriptHttpBlock< ExtendedAfterResultIn = ExtendedResult, ExtendedAfterResultOut = ExtendedAfterResultIn, ExtendedResultOut = Exclude, - >( args: { - block?: DescriptHttpBlockDescription< GetDescriptBlockParamsFnOut, Context >, + >(args: { + block?: DescriptHttpBlockDescription< GetDescriptBlockParamsFnOut, Context >; options?: DescriptBlockOptions< - Context, - GetDescriptBlockParamsFnIn, - GetDescriptBlockResultIn, - GetDescriptBlockParamsFnOut, - ExtendedBeforeResultOut, - ExtendedErrorResultOut, - GetDescriptBlockResultIn, - ExtendedAfterResultOut, - ExtendedResultOut - >, - } ): DescriptHttpBlock< Context, - DescriptBlockParams, GetDescriptBlockParamsFnOut>, - GetDescriptBlockResultOut> + GetDescriptBlockParamsFnIn, + GetDescriptBlockResultIn, + GetDescriptBlockParamsFnOut, + ExtendedBeforeResultOut, + ExtendedErrorResultOut, + GetDescriptBlockResultIn, + ExtendedAfterResultOut, + ExtendedResultOut + >; + }): DescriptHttpBlock< + Context, + DescriptBlockParams, GetDescriptBlockParamsFnOut>, + GetDescriptBlockResultOut> >; } @@ -380,23 +373,23 @@ declare function http< ResultOut = Exclude, > ( args: { - block: DescriptHttpBlockDescription< GetDescriptBlockParamsFnOut, Context >, + block: DescriptHttpBlockDescription< GetDescriptBlockParamsFnOut, Context >; options?: DescriptBlockOptions< - Context, - GetDescriptBlockParamsFnIn, - GetDescriptBlockResultIn, - GetDescriptBlockParamsFnOut, - BeforeResultOut, - ErrorResultOut, - GetDescriptBlockResultIn, - AfterResultOut, - ResultOut - >, + Context, + GetDescriptBlockParamsFnIn, + GetDescriptBlockResultIn, + GetDescriptBlockParamsFnOut, + BeforeResultOut, + ErrorResultOut, + GetDescriptBlockResultIn, + AfterResultOut, + ResultOut + >; }, ): DescriptHttpBlock< - Context, - DescriptBlockParams, GetDescriptBlockParamsFnOut>, - GetDescriptBlockResultOut +Context, +DescriptBlockParams, GetDescriptBlockParamsFnOut>, +GetDescriptBlockResultOut >; // --------------------------------------------------------------------------------------------------------------- // @@ -406,15 +399,15 @@ type DescriptFuncBlockDescription< Context, Params, Result, -> = ( args: { - params: Params, - context: Context, - deps: DescriptBlockDeps, - generate_id: DescriptBlockGenerateId, - cancel: Cancel, -} ) => - Result | - Promise; +> = (args: { + params: Params; + context: Context; + deps: DescriptBlockDeps; + generate_id: DescriptBlockGenerateId; + cancel: Cancel; +}) => +Result | +Promise; interface DescriptFuncBlock< Context, @@ -436,22 +429,22 @@ interface DescriptFuncBlock< ExtendedAfterResultIn = ExtendedResult, ExtendedAfterResultOut = ExtendedAfterResultIn, ExtendedResultOut = ExtendedBeforeResultOut | ExtendedErrorResultOut | ExtendedAfterResultOut, - >( args: { - block?: DescriptFuncBlockDescription< Context, GetDescriptBlockParamsFnOut, GetDescriptBlockResultIn>, + >(args: { + block?: DescriptFuncBlockDescription< Context, GetDescriptBlockParamsFnOut, GetDescriptBlockResultIn>; options?: DescriptBlockOptions< - Context, - GetDescriptBlockParamsFnIn, - GetDescriptBlockResultIn>, - GetDescriptBlockParamsFnOut, - ExtendedBeforeResultOut, - ExtendedErrorResultOut, - GetDescriptBlockResultIn>, - ExtendedAfterResultOut, - ExtendedResultOut - >, - } ): DescriptFuncBlock< Context, - DescriptBlockParams>, GetDescriptBlockParamsFnOut>>, - GetDescriptBlockResultOut> + Context, + GetDescriptBlockParamsFnIn, + GetDescriptBlockResultIn>, + GetDescriptBlockParamsFnOut, + ExtendedBeforeResultOut, + ExtendedErrorResultOut, + GetDescriptBlockResultIn>, + ExtendedAfterResultOut, + ExtendedResultOut + >; + }): DescriptFuncBlock< Context, + DescriptBlockParams>, GetDescriptBlockParamsFnOut>>, + GetDescriptBlockResultOut> >; } @@ -467,44 +460,44 @@ declare function func< ResultOut = Exclude, > ( args: { - block: DescriptFuncBlockDescription< Context, GetDescriptBlockParamsFnOut, GetDescriptBlockResultIn >, + block: DescriptFuncBlockDescription< Context, GetDescriptBlockParamsFnOut, GetDescriptBlockResultIn >; options?: DescriptBlockOptions< - Context, - GetDescriptBlockParamsFnIn, - GetDescriptBlockResultIn>, - GetDescriptBlockParamsFnOut, - BeforeResultOut, - ErrorResultOut, - GetDescriptBlockResultIn>, - AfterResultOut, - ResultOut - >, + Context, + GetDescriptBlockParamsFnIn, + GetDescriptBlockResultIn>, + GetDescriptBlockParamsFnOut, + BeforeResultOut, + ErrorResultOut, + GetDescriptBlockResultIn>, + AfterResultOut, + ResultOut + >; }, ): DescriptFuncBlock< Context, - DescriptBlockParams>, GetDescriptBlockParamsFnOut>>, - GetDescriptBlockResultOut> +DescriptBlockParams>, GetDescriptBlockParamsFnOut>>, +GetDescriptBlockResultOut> >; // --------------------------------------------------------------------------------------------------------------- // // ArrayBlock type GetArrayBlockResult< T > = { - 0: never, - 1: [ GetDescriptBlockResult< First< T > > ], - 2: [ GetDescriptBlockResult< First< T > >, ...GetArrayBlockResult< Tail< T > > ], -}[ T extends [] ? 0 : T extends ( ( readonly [ any ] ) | [ any ] ) ? 1 : 2 ]; + 0: never; + 1: [ GetDescriptBlockResult< First< T > > ]; + 2: [ GetDescriptBlockResult< First< T > >, ...GetArrayBlockResult< Tail< T > > ]; +}[ T extends [] ? 0 : T extends ((readonly [ any ]) | [ any ]) ? 1 : 2 ]; type GetArrayBlockParams< T > = { - 0: never, - 1: GetDescriptBlockParams< First< T > >, - 2: GetDescriptBlockParams< First< T > > & GetArrayBlockParams< Tail< T > >, -}[ T extends [] ? 0 : T extends ( ( readonly [ any ] ) | [ any ] ) ? 1 : 2 ]; + 0: never; + 1: GetDescriptBlockParams< First< T > >; + 2: GetDescriptBlockParams< First< T > > & GetArrayBlockParams< Tail< T > >; +}[ T extends [] ? 0 : T extends ((readonly [ any ]) | [ any ]) ? 1 : 2 ]; type GetArrayBlockContext< T > = { - 0: never, - 1: GetDescriptBlockContext< First< T > >, - 2: Equal< GetDescriptBlockContext< First< T > >, GetArrayBlockContext< Tail< T > > >, -}[ T extends [] ? 0 : T extends ( ( readonly [ any ] ) | [ any ] ) ? 1 : 2 ]; + 0: never; + 1: GetDescriptBlockContext< First< T > >; + 2: Equal< GetDescriptBlockContext< First< T > >, GetArrayBlockContext< Tail< T > > >; +}[ T extends [] ? 0 : T extends ((readonly [ any ]) | [ any ]) ? 1 : 2 ]; type DescriptArrayBlockDescription< T > = { [ P in keyof T ]: T[ P ] extends DescriptBlock< infer Context, infer Params, infer ResultIn, infer ParamsOut, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer Result > ? T[ P ] : never @@ -530,22 +523,22 @@ interface DescriptArrayBlock< ExtendedAfterResultIn = ExtendedResultIn, ExtendedAfterResultOut = ExtendedAfterResultIn, ExtendedResultOut = Exclude, - >( args: { + >(args: { options?: DescriptBlockOptions< - Context, - GetDescriptBlockParamsFnIn, - ExtendedResultIn, - GetDescriptBlockParamsFnOut, - ExtendedBeforeResultOut, - ExtendedErrorResultOut, - ExtendedAfterResultIn, - ExtendedAfterResultOut, - ExtendedResultOut - >, - } ): DescriptArrayBlock< Context, - DescriptBlockParams, GetDescriptBlockParamsFnOut>, - GetDescriptBlockResultOut> + GetDescriptBlockParamsFnIn, + ExtendedResultIn, + GetDescriptBlockParamsFnOut, + ExtendedBeforeResultOut, + ExtendedErrorResultOut, + ExtendedAfterResultIn, + ExtendedAfterResultOut, + ExtendedResultOut + >; + }): DescriptArrayBlock< + Context, + DescriptBlockParams, GetDescriptBlockParamsFnOut>, + GetDescriptBlockResultOut> >; } @@ -562,23 +555,23 @@ declare function array< ResultOut = Exclude, > ( args: { - block: DescriptArrayBlockDescription< Block >, + block: DescriptArrayBlockDescription< Block >; options?: DescriptBlockOptions< - Context, - GetDescriptBlockParamsFnIn, - ResultIn, - GetDescriptBlockParamsFnOut, - BeforeResultOut, - ErrorResultOut, - AfterResultIn, - AfterResultOut, - ResultOut - >, + Context, + GetDescriptBlockParamsFnIn, + ResultIn, + GetDescriptBlockParamsFnOut, + BeforeResultOut, + ErrorResultOut, + AfterResultIn, + AfterResultOut, + ResultOut + >; }, ): DescriptArrayBlock< - Context, - DescriptBlockParams, GetDescriptBlockParamsFnOut>, - GetDescriptBlockResultOut> +Context, +DescriptBlockParams, GetDescriptBlockParamsFnOut>, +GetDescriptBlockResultOut> >; // --------------------------------------------------------------------------------------------------------------- // @@ -612,9 +605,9 @@ type GetObjectBlockParams< PI = GetObjectBlockParamsFnInMap< T >, PO = GetObjectBlockParamsFnOutMap< T >, > = DescriptBlockParams< - UnionToIntersection, - UnionToIntersection, - UnionToIntersection +UnionToIntersection, +UnionToIntersection, +UnionToIntersection >; type GetObjectBlockContextMap< T extends {} > = { @@ -624,11 +617,11 @@ type GetObjectBlockContextMap< T extends {} > = { type GetObjectBlockContext< T extends {}, M = GetObjectBlockContextMap< T > > = UnionToIntersection< M[ keyof M ] >; type DescriptObjectBlockDescription< T extends {} > = { - [ P in keyof T ]: T[ P ] extends DescriptBlock< infer Context, infer Params, infer ResultIn, infer ParamsOut, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer ResultOut > ? T[ P ] : never + [ P in keyof T ]: T[ P ] extends DescriptBlock< infer Context, infer Params, infer ResultIn, infer ParamsOut, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer ResultOut > ? T[ P ] : never } type DescriptObjectBlockDescriptionResults< T extends {} > = { - [ P in keyof T ]: T[ P ] extends DescriptBlock< infer Context, infer Params, infer ResultIn, infer ParamsOut, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer ResultOut > ? ResultOut : never + [ P in keyof T ]: T[ P ] extends DescriptBlock< infer Context, infer Params, infer ResultIn, infer ParamsOut, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer ResultOut > ? ResultOut : never } interface DescriptObjectBlock< @@ -651,22 +644,22 @@ interface DescriptObjectBlock< ExtendedAfterResultIn = ExtendedResultIn, ExtendedAfterResultOut = ExtendedAfterResultIn, ExtendedResultOut = Exclude, - >( args: { + >(args: { options?: DescriptBlockOptions< - Context, - GetDescriptBlockParamsFnIn, - GetDescriptBlockResultIn>, - GetDescriptBlockParamsFnOut, - ExtendedBeforeResultOut, - ExtendedErrorResultOut, - ExtendedAfterResultIn, - ExtendedAfterResultOut, - ExtendedResultOut - >, - } ): DescriptObjectBlock< Context, - DescriptBlockParams, GetDescriptBlockParamsFnOut>, - GetDescriptBlockResultOut> + GetDescriptBlockParamsFnIn, + GetDescriptBlockResultIn>, + GetDescriptBlockParamsFnOut, + ExtendedBeforeResultOut, + ExtendedErrorResultOut, + ExtendedAfterResultIn, + ExtendedAfterResultOut, + ExtendedResultOut + >; + }): DescriptObjectBlock< + Context, + DescriptBlockParams, GetDescriptBlockParamsFnOut>, + GetDescriptBlockResultOut> >; } @@ -684,28 +677,29 @@ declare function object< ResultOut = Exclude, > ( args: { - block: DescriptObjectBlockDescription< Block >, + block: DescriptObjectBlockDescription< Block >; options?: DescriptBlockOptions< - Context, - GetDescriptBlockParamsFnIn, - GetDescriptBlockResultIn>, - GetDescriptBlockParamsFnOut, - BeforeResultOut, - ErrorResultOut, - GetDescriptBlockResultIn>, - AfterResultOut, - ResultOut - /*GetDescriptBlockResultIn>, + Context, + GetDescriptBlockParamsFnIn, + GetDescriptBlockResultIn>, + GetDescriptBlockParamsFnOut, + BeforeResultOut, + ErrorResultOut, + GetDescriptBlockResultIn>, + AfterResultOut, + ResultOut + + /*GetDescriptBlockResultIn>, GetDescriptBlockResultIn>, GetDescriptBlockResultIn>, InferResultOutFromBlocks, InferResultOutFromBlocks*/ - >, + >; }, ): DescriptObjectBlock< - Context, - DescriptBlockParams, GetDescriptBlockParamsFnOut>, - GetDescriptBlockResultOut> +Context, +DescriptBlockParams, GetDescriptBlockParamsFnOut>, +GetDescriptBlockResultOut> >; // --------------------------------------------------------------------------------------------------------------- // @@ -739,13 +733,13 @@ type DescriptBlock< Context, Params, Result, ParamsOut = Params, BeforeResultOut // --------------------------------------------------------------------------------------------------------------- // export type GetDescriptBlockResult< T > = - T extends DescriptBlock< infer Context, infer Params, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer Result > ? Result : never; + T extends DescriptBlock< infer Context, infer Params, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer Result > ? Result : never; export type GetDescriptBlockParams< T > = - T extends DescriptBlock< infer Context, infer Params, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer Result > ? Params : never; + T extends DescriptBlock< infer Context, infer Params, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer Result > ? Params : never; export type GetDescriptBlockContext< T > = - T extends DescriptBlock< infer Context, infer Params, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer Result > ? Context : never; + T extends DescriptBlock< infer Context, infer Params, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer Result > ? Context : never; // --------------------------------------------------------------------------------------------------------------- // @@ -761,30 +755,30 @@ declare function run< ResultOut = Exclude, > ( block: DescriptBlock< - Context, - GetDescriptBlockParamsFnIn, - GetDescriptBlockResultIn>, - GetDescriptBlockParamsFnOut, - BeforeResultOut, - ErrorResultOut, - AfterResultIn, - AfterResultOut, - ResultOut + Context, + GetDescriptBlockParamsFnIn, + GetDescriptBlockResultIn>, + GetDescriptBlockParamsFnOut, + BeforeResultOut, + ErrorResultOut, + AfterResultIn, + AfterResultOut, + ResultOut >, args: { - params?: GetDescriptBlockParamsBlock, - context?: Context, + params?: GetDescriptBlockParamsBlock; + context: Context; }, ): Promise< GetDescriptBlockResultOut>> ; // --------------------------------------------------------------------------------------------------------------- // -declare function is_block( block: any ): boolean; +declare function is_block(block: any): boolean; // --------------------------------------------------------------------------------------------------------------- // -declare function error( error: { id: string; [ key: string ]: any } ): DescriptError; -declare function is_error( error: any ): error is DescriptError; +declare function error(error: { id: string; [ key: string ]: any }): DescriptError; +declare function is_error(error: any): error is DescriptError; declare enum ERROR_ID { ALL_BLOCKS_FAILED = 'ALL_BLOCKS_FAILED', @@ -810,8 +804,8 @@ declare enum ERROR_ID { declare namespace request { interface DefaultOptions { - is_error: ( error: DescriptError, request_options: DescriptRequestOptions ) => boolean, - is_retry_allowed: ( error: DescriptError, request_options: DescriptRequestOptions ) => boolean, + is_error: (error: DescriptError, request_options: DescriptRequestOptions) => boolean; + is_retry_allowed: (error: DescriptError, request_options: DescriptRequestOptions) => boolean; } export const DEFAULT_OPTIONS: DefaultOptions; diff --git a/lib/index.js b/lib/index.js deleted file mode 100644 index e98e070..0000000 --- a/lib/index.js +++ /dev/null @@ -1,72 +0,0 @@ -const Context = require( './context' ); -const { ERROR_ID, create_error, is_error } = require( './error' ); - -const Cancel = require( './cancel' ); -const Logger = require( './logger' ); -const Cache = require( './cache' ); - -const request = require( './request' ); - -const Block = require( './block' ); -const ArrayBlock = require( './array_block' ); -const ObjectBlock = require( './object_block' ); -const FunctionBlock = require( './function_block' ); -const HttpBlock = require( './http_block' ); -const PipeBlock = require( './pipe_block' ); -const FirstBlock = require( './first_block' ); - -const de = {}; - -de.Logger = Logger; -de.Cache = Cache; - -de.request = request; - -de.ERROR_ID = ERROR_ID; -de.error = create_error; -de.is_error = is_error; - -de.Cancel = Cancel; - -de.func = function( { block, options } = {} ) { - return new FunctionBlock( block, options ); -}; -de.array = function( { block, options } = {} ) { - return new ArrayBlock( block, options ); -}; -de.object = function( { block, options } = {} ) { - return new ObjectBlock( block, options ); -}; -de.http = function( { block, options } = {} ) { - return new HttpBlock( block, options ); -}; -de.pipe = function( { block, options } = {} ) { - return new PipeBlock( block, options ); -}; -de.first = function( { block, options } = {} ) { - return new FirstBlock( block, options ); -}; - -de.is_block = function( block ) { - return ( block instanceof Block ); -}; - -de.run = function( block, { params, context, cancel } = {} ) { - const run_context = new Context(); - - if ( !( params && typeof params === 'object' ) ) { - params = {}; - } - if ( !cancel ) { - cancel = new Cancel(); - } - - const block_cancel = cancel.create(); - - return run_context.run( { block, block_cancel, params, cancel, context } ); -}; - -de.inferBlockTypes = ( block ) => block; - -module.exports = de; - diff --git a/lib/index.ts b/lib/index.ts new file mode 100644 index 0000000..403c4b6 --- /dev/null +++ b/lib/index.ts @@ -0,0 +1,187 @@ +import RunContext from './context'; +import { ERROR_ID, createError, isError, DescriptError } from './error'; + +import Cancel from './cancel'; +import Logger from './logger'; +import Cache from './cache'; + +import request from './request'; + +import Block from './block'; +import ArrayBlock from './arrayBlock'; +import ObjectBlock from './objectBlock'; +import type { FunctionBlockDefinition } from './functionBlock'; +import FunctionBlock from './functionBlock'; +import HttpBlock from './httpBlock'; +import FirstBlock from './firstBlock'; + +import type { DescriptHttpBlockResult, BlockResultOut, DescriptBlockOptions } from './types'; +import type BaseBlock from './block'; +import type { DescriptHttpBlockDescription } from './httpBlock'; +import type { GetObjectBlockParams, GetObjectBlockResult, ObjectBlockDefinition } from './objectBlock'; +import type { GetArrayBlockParams, GetArrayBlockResult, ArrayBlockDefinition } from './arrayBlock'; +import type { GetFirstBlockParams, GetFirstBlockResult, FirstBlockDefinition } from './firstBlock'; +import type { GetPipeBlockParams, GetPipeBlockResult, PipeBlockDefinition } from './pipeBlock'; +import PipeBlock from './pipeBlock'; + +const func = function< + Context, + ParamsOut, + BlockResult, + ResultOut extends BlockResultOut, + BeforeResultOut = undefined, + AfterResultOut = undefined, + ErrorResultOut = undefined, + Params = ParamsOut +>({ block, options }: { + block: FunctionBlockDefinition; + options?: DescriptBlockOptions< + Context, ParamsOut, BlockResult, BeforeResultOut, AfterResultOut, ErrorResultOut, Params + >; +}) { + return new FunctionBlock< + Context, ParamsOut, BlockResult, ResultOut, BeforeResultOut, AfterResultOut, ErrorResultOut, Params + >({ block, options }); +}; +const array = function< + Context, + Block extends ReadonlyArray, + ResultOut extends BlockResultOut, + ParamsOut = GetArrayBlockParams, + BlockResult = GetArrayBlockResult, + BeforeResultOut = undefined, + AfterResultOut = undefined, + ErrorResultOut = undefined, + Params = GetArrayBlockParams, +>({ block, options }: { + block: ArrayBlockDefinition; + options?: DescriptBlockOptions; +}) { + return new ArrayBlock({ block, options }); +}; +const object = function< + Context, + Blocks extends Record, + ResultOut extends BlockResultOut, + ParamsOut = GetObjectBlockParams, + BlockResult = GetObjectBlockResult, + + BeforeResultOut = undefined, + AfterResultOut = undefined, + ErrorResultOut = undefined, + Params = GetObjectBlockParams, +>({ block, options }: { + block?: ObjectBlockDefinition; + options?: DescriptBlockOptions; +} = {}) { + return new ObjectBlock({ block, options }); +}; +const http = function< + Context, + ParamsOut, + ResultOut extends BlockResultOut, + IntermediateResult, + BlockResult extends DescriptHttpBlockResult, + + BeforeResultOut = undefined, + AfterResultOut = undefined, + ErrorResultOut = undefined, + Params = ParamsOut, +>({ block, options }: { + block?: DescriptHttpBlockDescription; + options?: DescriptBlockOptions; +}) { + return new HttpBlock< + Context, ParamsOut, IntermediateResult, ResultOut, BlockResult, BeforeResultOut, AfterResultOut, ErrorResultOut, Params + >({ block, options }); +}; + +const first = function< + Context, + Block extends ReadonlyArray, + ResultOut extends BlockResultOut, + ParamsOut = GetFirstBlockParams, + BlockResult = GetFirstBlockResult, + BeforeResultOut = undefined, + AfterResultOut = undefined, + ErrorResultOut = undefined, + Params = GetFirstBlockParams, +>({ block, options }: { + block: FirstBlockDefinition; + options?: DescriptBlockOptions; +}) { + return new FirstBlock({ block, options }); +}; + +const pipe = function< + Context, + Block extends ReadonlyArray, + ResultOut extends BlockResultOut, + ParamsOut = GetPipeBlockParams, + BlockResult = GetPipeBlockResult, + BeforeResultOut = undefined, + AfterResultOut = undefined, + ErrorResultOut = undefined, + Params = GetPipeBlockParams, +>({ block, options }: { + block: PipeBlockDefinition; + options?: DescriptBlockOptions; +}) { + return new PipeBlock({ block, options }); +}; + +const isBlock = function(block: any) { + return (block instanceof Block); +}; + +const run = function< + Context, + CustomBlock, + ParamsOut, + ResultOut extends BlockResultOut, + IntermediateResult, + BlockResult, + BeforeResultOut = undefined, + AfterResultOut = undefined, + ErrorResultOut = undefined, + Params = ParamsOut, +>( + block: BaseBlock, + //block: FunctionBlock, + { params, context, cancel }: { + params?: Params; context?: Context; cancel?: Cancel; + } = {}) { + + const runContext = new RunContext(); + + if (!(params && typeof params === 'object')) { + params = {} as Params; + } + if (!cancel) { + cancel = new Cancel(); + } + + const blockCancel = cancel.create(); + //return block.run({ runContext, blockCancel, cancel, params }); + return runContext.run({ block, blockCancel, params, cancel, context });// as ReturnType; +}; + +export { + Logger, + Cache, + request, + ERROR_ID, + createError as error, + isError, + Cancel, + func, + array, + object, + http, + first, + pipe, + isBlock, + run, + DescriptError, + DescriptHttpBlockResult, +}; diff --git a/lib/is_plain_object.js b/lib/isPlainObject.ts similarity index 65% rename from lib/is_plain_object.js rename to lib/isPlainObject.ts index 6acfb28..6634d03 100644 --- a/lib/is_plain_object.js +++ b/lib/isPlainObject.ts @@ -1,14 +1,13 @@ // Нет смысла заморачиваться на какую-нибудь экзотическую дичь. // Object literal определяется таким образом и этого достаточно. // -module.exports = function is_plain_object( object ) { - if ( object && typeof object === 'object' ) { +export default function isPlainObject(object: any): object is object { + if (object && typeof object === 'object') { const proto = object.__proto__; - if ( !proto || proto === Object.prototype ) { + if (!proto || proto === Object.prototype) { return true; } } return false; -}; - +} diff --git a/lib/logger.js b/lib/logger.js deleted file mode 100644 index 30e7540..0000000 --- a/lib/logger.js +++ /dev/null @@ -1,99 +0,0 @@ -class Logger { - - constructor( config ) { - config = config || {}; - - this._debug = config.debug; - } - - log( event, context ) { - switch ( event.type ) { - case Logger.EVENT.REQUEST_START: { - if ( this._debug ) { - const message = `[DEBUG] ${ event.request_options.http_options.method } ${ event.request_options.url }`; - - log_to_stream( process.stdout, message, context ); - } - - break; - } - - case Logger.EVENT.REQUEST_SUCCESS: { - let message = `${ event.result.status_code } ${ total( event ) } ${ event.request_options.http_options.method } ${ event.request_options.url }`; - const body = event.request_options.body; - if ( body ) { - if ( Buffer.isBuffer( body ) ) { - message += ' ' + String( body ); - - } else { - message += ' ' + body; - } - } - - log_to_stream( process.stdout, message, context ); - - break; - } - - case Logger.EVENT.REQUEST_ERROR: { - const error = event.error.error; - - let message = '[ERROR] '; - if ( error.status_code > 0 ) { - message += error.status_code; - - } else { - if ( error.stack ) { - message += ' ' + error.stack; - - } else { - message += error.id; - if ( error.message ) { - message += ': ' + error.message; - } - } - } - message += ` ${ total( event ) } ${ event.request_options.http_options.method } ${ event.request_options.url }`; - - log_to_stream( process.stderr, message, context ); - - break; - } - } - } - -} - -// --------------------------------------------------------------------------------------------------------------- // - -Logger.EVENT = { - REQUEST_START: 'REQUEST_START', - REQUEST_SUCCESS: 'REQUEST_SUCCESS', - REQUEST_ERROR: 'REQUEST_ERROR', -}; - -// --------------------------------------------------------------------------------------------------------------- // - -function log_to_stream( stream, message, context ) { - const date = new Date().toISOString(); - - stream.write( `${ date } ${ message }\n` ); -} - -// --------------------------------------------------------------------------------------------------------------- // - -function total( event ) { - let total = `${ event.timestamps.end - event.timestamps.start }ms`; - - const retries = event.request_options.retries; - if ( retries > 0 ) { - total += ` (retry #${ retries })`; - } - - return total; -} - -// --------------------------------------------------------------------------------------------------------------- // - -module.exports = Logger; - diff --git a/lib/logger.ts b/lib/logger.ts new file mode 100644 index 0000000..4390878 --- /dev/null +++ b/lib/logger.ts @@ -0,0 +1,142 @@ +import type { DescriptHttpResult } from './types'; +import type { RequestOptions } from './request'; +import type { DescriptError } from './error'; +import type http from 'node:http'; + +type Config = { + debug?: boolean; +} + +export enum EVENT { + REQUEST_START= 'REQUEST_START', + REQUEST_SUCCESS = 'REQUEST_SUCCESS', + REQUEST_ERROR = 'REQUEST_ERROR', +} + +export type EventTimestamps = { + start?: number; + socket?: number; + tcpConnection?: number; + firstByte?: number; + body?: number; + requestEnd?: number; + end?: number; +}; + +interface BaseLoggerEvent { + requestOptions: RequestOptions; + + timestamps?: EventTimestamps; + + request?: http.ClientRequest; + +} + +interface SuccessLoggerEvent extends BaseLoggerEvent { + type: EVENT.REQUEST_SUCCESS; + result: DescriptHttpResult; +} + + +interface ErrorLoggerEvent extends BaseLoggerEvent { + type: EVENT.REQUEST_ERROR; + error: DescriptError; +} + +interface StartLoggerEvent extends BaseLoggerEvent { + type: EVENT.REQUEST_START; +} + +export type LoggerEvent = SuccessLoggerEvent | ErrorLoggerEvent | StartLoggerEvent; + + +class Logger { + private _debug = false; + + constructor(config: Config) { + config = config || {}; + + this._debug = config.debug || false; + } + + log(event: LoggerEvent) { + switch (event.type) { + case EVENT.REQUEST_START: { + if (this._debug) { + const message = `[DEBUG] ${ event.requestOptions.httpOptions.method } ${ event.requestOptions.url }`; + + logToStream(process.stdout, message); + } + + break; + } + + case EVENT.REQUEST_SUCCESS: { + let message = `${ event.result.statusCode } ${ total(event) } ${ event.requestOptions.httpOptions.method } ${ event.requestOptions.url }`; + const body = event.requestOptions.body; + if (body) { + if (Buffer.isBuffer(body)) { + message += ' ' + String(body); + + } else { + message += ' ' + body; + } + } + + logToStream(process.stdout, message); + + break; + } + + case EVENT.REQUEST_ERROR: { + const error = event.error.error; + + let message = '[ERROR] '; + if ((error.statusCode || 0) > 0) { + message += error.statusCode; + + } else { + if (error.stack) { + message += ' ' + error.stack; + + } else { + message += error.id; + if (error.message) { + message += ': ' + error.message; + } + } + } + message += ` ${ total(event) } ${ event.requestOptions.httpOptions.method } ${ event.requestOptions.url }`; + + logToStream(process.stderr, message); + + break; + } + } + } + +} + +// --------------------------------------------------------------------------------------------------------------- // + + +// --------------------------------------------------------------------------------------------------------------- // + +function logToStream(stream: typeof process.stderr | typeof process.stdout, message: string) { + const date = new Date().toISOString(); + + stream.write(`${ date } ${ message }\n`); +} + +function total(event: LoggerEvent) { + let total = `${ (event.timestamps?.end || 0) - (event.timestamps?.start || 0) }ms`; + + const retries = event.requestOptions.retries; + if (retries > 0) { + total += ` (retry #${ retries })`; + } + + return total; +} + +export default Logger; diff --git a/lib/objectBlock.ts b/lib/objectBlock.ts new file mode 100644 index 0000000..fecb11a --- /dev/null +++ b/lib/objectBlock.ts @@ -0,0 +1,149 @@ +import CompositeBlock from './compositeBlock'; +import type { DescriptError } from './error'; +import { createError, ERROR_ID } from './error'; +import type BaseBlock from './block'; +import type { InferParamsInFromBlock, InferResultFromBlock, DescriptBlockOptions, BlockResultOut, UnionToIntersection } from './types'; +import type ContextClass from './context'; +import type Cancel from './cancel'; +import type { DescriptBlockDeps } from './depsDomain'; +import type DepsDomain from './depsDomain'; + + +export type InferResultFromObjectBlocks = Block extends BaseBlock< +// eslint-disable-next-line @typescript-eslint/no-unused-vars +infer Context, infer CustomBlock, infer ParamsOut, infer ResultOut, infer IntermediateResult, +// eslint-disable-next-line @typescript-eslint/no-unused-vars +infer BlockResult, infer BeforeResultOut, infer AfterResultOut, infer ErrorResultOut, infer Params +> ? + { [ K in keyof BlockResult ]: InferResultFromObjectBlocks } : + Block; + +export type GetObjectBlockResult< T extends Record > = { + [ P in keyof T ]: InferResultFromBlock | DescriptError +} + +export type GetObjectBlockParams< + T extends Record, + PB = GetObjectBlockParamsMap< T >, +> = UnionToIntersection + + +type GetObjectBlockParamsMap< T extends Record > = { + [ P in keyof T ]: unknown extends InferParamsInFromBlock ? object : InferParamsInFromBlock; +} + + +export type ObjectBlockDefinition< T extends Record > = { + [ P in keyof T ]: T[ P ] extends BaseBlock< + // eslint-disable-next-line @typescript-eslint/no-unused-vars + infer Context, infer CustomBlock, infer ParamsOut, infer ResultOut, infer BlockResult, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + infer IntermediateResult, infer BeforeResultOut, infer AfterResultOut, infer ErrorResultOut, infer Params + > ? T[ P ] : never +} + +class ObjectBlock< + Context, + Blocks extends Record, + ResultOut extends BlockResultOut, + ParamsOut = GetObjectBlockParams, + BlockResult = GetObjectBlockResult, + + BeforeResultOut = undefined, + AfterResultOut = undefined, + ErrorResultOut = undefined, + Params = GetObjectBlockParams, +> extends CompositeBlock< + Context, + ObjectBlockDefinition, + ParamsOut, + ResultOut, + BlockResult, + BlockResult, + + BeforeResultOut, + AfterResultOut, + ErrorResultOut, + Params + > { + + protected initBlock(object: ObjectBlockDefinition) { + if (!(object && typeof object === 'object')) { + throw createError({ + id: ERROR_ID.INVALID_BLOCK, + message: 'block must be an object', + }); + } + + super.initBlock(object); + + this.blocks = Object.keys(object).reduce((ret, key) => { + const block = object[key]; + + ret.push({ + block, + key, + }); + return ret; + }, [] as typeof this.blocks); + } + + protected async blockAction(args: { + runContext: ContextClass; + blockCancel: Cancel; + cancel: Cancel; + params: ParamsOut; + context: Context; + deps: DescriptBlockDeps; + nParents: number; + depsDomain?: DepsDomain; + }): Promise { + const results = await this.runBlocks(args); + + const r: Record = {}; + const blocks = this.blocks; + results.forEach((result, i) => { + r[ blocks[ i ].key ] = result; + }); + return r as unknown as Promise; + } + + extend< + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ExtendedResultOut extends + BlockResultOut, + ExtendedParamsOut = Params, + ExtendedParams = Params, + ExtendedBlockResult = ResultOut, + ExtendedBeforeResultOut = undefined, + ExtendedAfterResultOut = undefined, + ExtendedErrorResultOut = undefined, + >({ options }: { + options: DescriptBlockOptions< + Context, Params & ExtendedParamsOut, ExtendedBlockResult, ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams + >; + }) { + return this.extendClass< + ObjectBlock< + Context, + Blocks, + ExtendedResultOut, + Params & ExtendedParamsOut, + ExtendedBlockResult, + ExtendedBeforeResultOut, + ExtendedAfterResultOut, + ExtendedErrorResultOut, + ExtendedParams + >, + ExtendedBlockResult, + Params & ExtendedParamsOut, + ExtendedParams, + ExtendedBeforeResultOut, + ExtendedAfterResultOut, + ExtendedErrorResultOut + >({ options }); + } + +} + +export default ObjectBlock; diff --git a/lib/object_block.js b/lib/object_block.js deleted file mode 100644 index cd841bc..0000000 --- a/lib/object_block.js +++ /dev/null @@ -1,40 +0,0 @@ -const CompositeBlock = require( './composite_block' ); -const { create_error, ERROR_ID } = require( './error' ); - -class ObjectBlock extends CompositeBlock { - - _init_block( object ) { - if ( !( object && typeof object === 'object' ) ) { - throw create_error( { - id: ERROR_ID.INVALID_BLOCK, - message: 'block must be an object', - } ); - } - - super._init_block( object ); - - const blocks = this._blocks = []; - - for ( const key in object ) { - blocks.push( { - block: object[ key ], - key: key, - } ); - } - } - - async _action( args ) { - const results = await this._run_blocks( args ); - - const r = {}; - const blocks = this._blocks; - results.forEach( ( result, i ) => { - r[ blocks[ i ].key ] = result; - } ); - return r; - } - -} - -module.exports = ObjectBlock; - diff --git a/lib/pipeBlock.ts b/lib/pipeBlock.ts new file mode 100644 index 0000000..4ad0749 --- /dev/null +++ b/lib/pipeBlock.ts @@ -0,0 +1,170 @@ +import CompositeBlock from './compositeBlock' ; +import { ERROR_ID, createError } from './error'; +import type { DescriptError } from './error'; +import type BaseBlock from './block'; +import type { + BlockResultOut, + First, + InferResultFromBlock, + InferParamsInFromBlock, + Tail, + DescriptBlockOptions, +} from './types'; + +import type ContextClass from './context'; +import type Cancel from './cancel'; +import type { DescriptBlockDeps } from './depsDomain'; +import type DepsDomain from './depsDomain'; + +type GetPipeBlockParamsMap< T extends ReadonlyArray> = { + [ P in keyof T ]: InferParamsInFromBlock; +} + +export type GetPipeBlockParamsUnion< T extends ReadonlyArray> = { + 0: never; + 1: First< T >; + 2: First< T > & GetPipeBlockParamsUnion< Tail< T > >; +}[ T extends [] ? 0 : T extends ((readonly [ any ]) | [ any ]) ? 1 : 2 ]; + +export type GetPipeBlockParams< + T extends ReadonlyArray, + PA extends ReadonlyArray = GetPipeBlockParamsMap, + PU = GetPipeBlockParamsUnion +> = PU; + + +type GetPipeBlockResultUnion< T extends ReadonlyArray> = { + 0: never; + 1: First< T > | DescriptError; + 2: First< T > | DescriptError | GetPipeBlockResultUnion< Tail< T > >; +}[ T extends [] ? 0 : T extends ((readonly [ any ]) | [ any ]) ? 1 : 2 ]; + +type GetPipeBlockResultMap< T extends ReadonlyArray> = { + [ P in keyof T ]: InferResultFromBlock; +} + +export type GetPipeBlockResult< + T extends ReadonlyArray, + PA extends ReadonlyArray = GetPipeBlockResultMap, + PU = GetPipeBlockResultUnion +> = PU; + +export type PipeBlockDefinition< T > = { + [ P in keyof T ]: T[ P ] extends BaseBlock< + // eslint-disable-next-line @typescript-eslint/no-unused-vars + infer Context, infer CustomBlock, infer ParamsOut, infer ResultOut, infer IntermediateResult, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + infer BlockResult, infer BeforeResultOut, infer AfterResultOut, infer ErrorResultOut, infer Params + > ? T[ P ] : never +} + +class PipeBlock< + Context, + Block extends ReadonlyArray, + ResultOut extends BlockResultOut, + ParamsOut = GetPipeBlockParams, + BlockResult = GetPipeBlockResult, + + BeforeResultOut = undefined, + AfterResultOut = undefined, + ErrorResultOut = undefined, + Params = GetPipeBlockParams, +> extends CompositeBlock< + Context, + PipeBlockDefinition, + ParamsOut, + ResultOut, + BlockResult, + BlockResult, + + BeforeResultOut, + AfterResultOut, + ErrorResultOut, + Params + > { + + extend< + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ExtendedResultOut extends + BlockResultOut, + ExtendedParamsOut = Params, + ExtendedParams = Params, + ExtendedBlockResult = ResultOut, + ExtendedBeforeResultOut = undefined, + ExtendedAfterResultOut = undefined, + ExtendedErrorResultOut = undefined, + >({ options }: { + options: DescriptBlockOptions< + Context, Params & ExtendedParamsOut, ExtendedBlockResult, ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams + >; + }) { + return this.extendClass< + PipeBlock< + Context, + Block, + ExtendedResultOut, + Params & ExtendedParamsOut, + ExtendedBlockResult, + ExtendedBeforeResultOut, + ExtendedAfterResultOut, + ExtendedErrorResultOut, + ExtendedParams + >, + ExtendedBlockResult, + ExtendedParamsOut, + ExtendedParams, + ExtendedBeforeResultOut, + ExtendedAfterResultOut, + ExtendedErrorResultOut + >({ options }); + } + + + protected initBlock(block: PipeBlockDefinition) { + if (!Array.isArray(block)) { + throw createError({ + id: ERROR_ID.INVALID_BLOCK, + message: 'block must be an array', + }); + } + + super.initBlock(block); + } + + + protected async blockAction({ runContext, blockCancel, cancel, params, context, nParents, depsDomain }: { + runContext: ContextClass; + blockCancel: Cancel; + cancel: Cancel; + params: ParamsOut; + context?: Context; + deps: DescriptBlockDeps; + nParents: number; + depsDomain?: DepsDomain; + }): Promise { + let result; + let prev: any = []; + + for (let i = 0; i < this.block.length; i++) { + const block = this.block[ i ]; + + result = await runContext.run({ + block: block, + blockCancel: blockCancel.create(), + depsDomain, + params: params, + context: context, + cancel: cancel, + prev: prev, + nParents: nParents + 1, + }); + + prev = prev.concat(result); + } + + return result as BlockResult; + } + +} + +export default PipeBlock; diff --git a/lib/pipe_block.js b/lib/pipe_block.js deleted file mode 100644 index 106bb23..0000000 --- a/lib/pipe_block.js +++ /dev/null @@ -1,45 +0,0 @@ -const Block = require( './block' ); -const { ERROR_ID, create_error } = require( './error' ); - -class PipeBlock extends Block { - - _init_block( array ) { - if ( !Array.isArray( array ) ) { - throw create_error( { - id: ERROR_ID.INVALID_BLOCK, - message: 'block must be an array', - } ); - } - - super._init_block( array ); - } - - - async _action( { run_context, block_cancel, deps_domain, cancel, params, context, n_parents } ) { - let result; - let prev = []; - - for ( let i = 0; i < this._block.length; i++ ) { - const block = this._block[ i ]; - - result = await run_context.run( { - block: block, - block_cancel: block_cancel.create(), - deps_domain: deps_domain, - params: params, - context: context, - cancel: cancel, - prev: prev, - n_parents: n_parents + 1, - } ); - - prev = prev.concat( result ); - } - - return result; - } - -} - -module.exports = PipeBlock; - diff --git a/lib/request.js b/lib/request.js deleted file mode 100644 index 787f8fd..0000000 --- a/lib/request.js +++ /dev/null @@ -1,560 +0,0 @@ -const http_ = require( 'http' ); -const https_ = require( 'https' ); -const qs_ = require( 'querystring' ); -const url_ = require( 'url' ); -const { createGzip, createUnzip } = require( 'node:zlib' ); -const { decompress } = require( '@fengkx/zstd-napi' ); -const { Transform } = require( 'stream' ); - -const Logger = require( './logger' ); - -const get_deferred = require( './get_deferred' ); -const { create_error, ERROR_ID } = require( './error' ); -const is_plain_object = require( './is_plain_object' ); - -const extend = require( './extend' ); - -// --------------------------------------------------------------------------------------------------------------- // - -// FIXME: А зачем тут WeakMap, а не просто Map? -// -const agents_cache_http = new WeakMap(); -const agents_cache_https = new WeakMap(); - -const RX_IS_JSON = /^application\/json(?:;|\s|$)/; - -const DEFAULT_OPTIONS = { - method: 'GET', - protocol: 'http:', - hostname: 'localhost', - pathname: '/', - - is_error: function( error, request_options ) { - const id = error.error.id; - const status_code = error.error.status_code; - - return ( - id === ERROR_ID.TCP_CONNECTION_TIMEOUT || - id === ERROR_ID.REQUEST_TIMEOUT || - status_code >= 400 - ); - }, - - is_retry_allowed: function( error, request_options ) { - const method = request_options.http_options.method; - if ( method === 'POST' || method === 'PATCH' ) { - return false; - } - - const id = error.error.id; - const status_code = error.error.status_code; - - return ( - id === ERROR_ID.TCP_CONNECTION_TIMEOUT || - id === ERROR_ID.REQUEST_TIMEOUT || - status_code === 408 || - status_code === 429 || - status_code === 500 || - ( status_code >= 502 && status_code <= 504 ) - ); - }, - - max_retries: 0, - - retry_timeout: 100, -}; - -// --------------------------------------------------------------------------------------------------------------- // - -class RequestOptions { - - constructor( options ) { - // NOTE: Тут не годится Object.assign, так как ключи с undefined перезатирают все. - options = extend( {}, DEFAULT_OPTIONS, options ); - - this.is_error = options.is_error; - this.is_retry_allowed = options.is_retry_allowed; - this.max_retries = options.max_retries; - this.retry_timeout = options.retry_timeout; - - this.retries = 0; - - this.timeout = options.timeout; - this.body_compress = options.body_compress; - - this.http_options = {}; - - this.http_options.protocol = options.protocol; - this.http_options.hostname = options.hostname; - this.http_options.port = options.port; - if ( options.family ) { - this.http_options.family = options.family; - } - if ( !this.http_options.port ) { - this.http_options.port = ( this.http_options.protocol === 'https:' ) ? 443 : 80; - } - - let pathname = options.pathname; - if ( pathname.charAt( 0 ) !== '/' ) { - pathname = '/' + pathname; - } - - this.http_options.path = url_.format( { - pathname: pathname, - query: options.query, - } ); - - if ( options.auth ) { - this.http_options.auth = options.auth; - } - - // Нужно для логов. - this.url = url_.format( { - ...this.http_options, - // url.format игнорит свойство path, но смотрит на pathname + query/search. - pathname: pathname, - query: options.query, - } ); - - this.http_options.headers = {}; - if ( options.headers ) { - for ( const name in options.headers ) { - this.http_options.headers[ name.toLowerCase() ] = options.headers[ name ]; - } - } - // Add gzip headers. - if ( this.http_options.headers[ 'accept-encoding' ] ) { - this.http_options.headers[ 'accept-encoding' ] = 'gzip,deflate,' + this.http_options.headers[ 'accept-encoding' ]; - - } else { - this.http_options.headers[ 'accept-encoding' ] = 'gzip,deflate'; - } - - const method = this.http_options.method = options.method.toUpperCase(); - - this.body = null; - if ( options.body && ( method === 'POST' || method === 'PUT' || method === 'PATCH' || method === 'DELETE' ) ) { - if ( Buffer.isBuffer( options.body ) ) { - this.body = options.body; - this.set_content_type( 'application/octet-stream' ); - - } else if ( typeof options.body !== 'object' ) { - this.body = String( options.body ); - this.set_content_type( 'text/plain' ); - - } else if ( RX_IS_JSON.test( this.http_options.headers[ 'content-type' ] ) ) { - this.body = JSON.stringify( options.body ); - - } else { - this.body = qs_.stringify( options.body ); - this.set_content_type( 'application/x-www-form-urlencoded' ); - } - - if ( this.body_compress ) { - this.http_options.headers[ 'content-encoding' ] = 'gzip'; - - } else { - this.http_options.headers[ 'content-length' ] = Buffer.byteLength( this.body ); - } - } - - const is_https = ( this.http_options.protocol === 'https:' ); - this.request_module = ( is_https ) ? https_ : http_; - - if ( options.agent != null ) { - if ( is_plain_object( options.agent ) ) { - const agents_cache = ( is_https ) ? agents_cache_https : agents_cache_http; - - let agent = agents_cache.get( options.agent ); - if ( !agent ) { - agent = new this.request_module.Agent( options.agent ); - agents_cache.set( options.agent, agent ); - } - this.http_options.agent = agent; - - } else { - // Здесь может быть либо `false`, либо `instanceof Agent`. - // Либо еще что-нибудь, инстанс какого-то левого агента типа TunnelingAgent. - // - this.http_options.agent = options.agent; - } - } - - if ( this.http_options.protocol === 'https:' ) { - this.http_options.pfx = options.pfx; - this.http_options.key = options.key; - this.http_options.passphrase = options.passphrase; - this.http_options.cert = options.cert; - this.http_options.ca = options.ca; - this.http_options.ciphers = options.ciphers; - this.http_options.rejectUnauthorized = options.rejectUnauthorized; - this.http_options.secureProtocol = options.secureProtocol; - this.http_options.servername = options.servername; - } - - this.extra = options.extra; - } - - set_content_type( content_type ) { - if ( !this.http_options.headers[ 'content-type' ] ) { - this.http_options.headers[ 'content-type' ] = content_type; - } - } - -} - -// --------------------------------------------------------------------------------------------------------------- // - -class Request { - - constructor( options, logger, context, cancel ) { - this.options = options; - this.logger = logger; - this.context = context; - this.cancel = cancel; - - this.timestamps = {}; - this.h_timeout = null; - this.req = null; - - this.is_resolved = false; - } - - start() { - this.log( { - type: Logger.EVENT.REQUEST_START, - request_options: this.options, - } ); - - this.timestamps.start = Date.now(); - - this.deferred = get_deferred(); - - this.cancel.subscribe( ( error ) => this.do_cancel( create_error( { - id: ERROR_ID.HTTP_REQUEST_ABORTED, - reason: error, - } ) ) ); - this.set_timeout(); - - try { - this.req = this.options.request_module.request( this.options.http_options, async ( res ) => { - try { - const result = await this.request_handler( res ); - this.do_done( result ); - - } catch ( error ) { - this.do_fail( error ); - } - } ); - - let on_connect; - - this.req.on( 'socket', ( socket ) => { - this.timestamps.socket = Date.now(); - - if ( !socket.connecting ) { - // Это сокет из пула, на нем не будет события 'connect'. - this.timestamps.tcp_connection = this.timestamps.socket; - - } else { - on_connect = () => { - this.timestamps.tcp_connection = Date.now(); - }; - - socket.once( 'connect', on_connect ); - } - } ); - - this.req.on( 'error', ( error ) => { - if ( on_connect && this.req.socket ) { - this.req.socket.removeListener( 'connect', on_connect ); - - on_connect = null; - } - - if ( this.req.aborted ) { - // FIXME: правда ли нет ситуация, когда это приведет к повисанию запроса? - return; - } - if ( this.is_resolved ) { - return; - } - - error = { - id: ERROR_ID.HTTP_UNKNOWN_ERROR, - message: error.message, - }; - this.destroy_request_socket(); - - this.do_fail( error ); - } ); - - - if ( this.options.body_compress && this.options.body ) { - const gzip_stream = createGzip( this.options.body_compress ); - gzip_stream.pipe( this.req, { end: true } ); - gzip_stream.end( this.options.body, () => { - this.timestamps.body = Date.now(); - this.timestamps.request_end = Date.now(); - } ); - - } else { - if ( this.options.body ) { - this.req.write( this.options.body, () => { - this.timestamps.body = Date.now(); - } ); - } - - this.req.end( () => { - this.timestamps.request_end = Date.now(); - } ); - } - } catch ( e ) { - this.do_fail( e ); - } - - return this.deferred.promise; - } - - do_done( result ) { - if ( this.is_resolved ) { - return; - } - - this.clear_timeout(); - - this.timestamps.end = this.timestamps.end || Date.now(); - - this.log( { - type: Logger.EVENT.REQUEST_SUCCESS, - request_options: this.options, - result: result, - timestamps: this.timestamps, - } ); - - this.is_resolved = true; - - this.deferred.resolve( result ); - } - - do_fail( error ) { - if ( this.is_resolved ) { - return; - } - - this.clear_timeout(); - - this.timestamps.end = this.timestamps.end || Date.now(); - - error = create_error( error ); - - this.log( { - type: Logger.EVENT.REQUEST_ERROR, - request_options: this.options, - error: error, - timestamps: this.timestamps, - } ); - - this.is_resolved = true; - - this.deferred.reject( error ); - } - - do_cancel( error ) { - if ( this.req ) { - this.req.abort(); - } - - this.do_fail( error ); - } - - set_timeout() { - if ( this.options.timeout > 0 ) { - this.h_timeout = setTimeout( () => { - let error; - if ( !this.timestamps.tcp_connection ) { - // Не смогли к этому моменту установить tcp-соединение. - error = { - id: ERROR_ID.TCP_CONNECTION_TIMEOUT, - }; - - } else { - // Тут просто слишком долго выполняли запрос целиком. - error = { - id: ERROR_ID.REQUEST_TIMEOUT, - }; - } - - this.do_cancel( error ); - - }, this.options.timeout ); - } - } - - clear_timeout() { - if ( this.h_timeout ) { - clearTimeout( this.h_timeout ); - this.h_timeout = null; - } - } - - destroy_request_socket() { - if ( this.req && this.req.socket ) { - this.req.socket.destroy(); - } - } - - async request_handler( res ) { - res.once( 'readable', () => { - this.timestamps.first_byte = Date.now(); - } ); - - const unzipped = decompress_response( res ); - - const buffers = []; - let received_length = 0; - - // NOTE: nodejs v15+ - // [8a6fab02ad] - (SEMVER-MAJOR) http: emit 'error' on aborted server request (Robert Nagy) #33172 - // https://github.com/nodejs/node/pull/33172 - try { - for await ( const chunk of unzipped ) { - this.cancel.throw_if_cancelled(); - - buffers.push( chunk ); - received_length += chunk.length; - } - } catch ( error ) { - const is_aborted = error.code === 'ECONNRESET'; - if ( !is_aborted ) { - throw error; - } - } - - if ( !res.complete ) { - const error = create_error( { - id: ERROR_ID.INCOMPLETE_RESPONSE, - } ); - throw error; - } - - const status_code = res.statusCode; - const body = ( received_length ) ? Buffer.concat( buffers, received_length ) : null; - const headers = res.headers; - - const error = create_error( { - id: 'HTTP_' + status_code, - status_code: status_code, - headers: headers, - body: body, - message: http_.STATUS_CODES[ status_code ], - } ); - if ( this.options.is_error( error, this.options ) ) { - throw error; - } - - return { - status_code: status_code, - request_options: this.options, - headers: headers, - timestamps: this.timestamps, - body: body, - }; - } - - log( event ) { - if ( this.logger ) { - event.request = this.req; - this.logger.log( event, this.context ); - } - } - -} - -// --------------------------------------------------------------------------------------------------------------- // - -async function request( options, logger, context, cancel ) { - const request_options = new RequestOptions( options ); - - while ( true ) { - const req = new Request( request_options, logger, context, cancel ); - - try { - const result = await req.start(); - - return result; - - } catch ( error ) { - if ( error.error.status_code === 429 || error.error.status_code >= 500 ) { - // Удаляем сокет, чтобы не залипать на отвечающем ошибкой бекэнде. - req.destroy_request_socket(); - } - - if ( request_options.retries < request_options.max_retries && request_options.is_retry_allowed( error, request_options ) ) { - request_options.retries++; - - if ( request_options.retry_timeout > 0 ) { - await wait_for( request_options.retry_timeout ); - } - - } else { - throw error; - } - } - } -} - -request.DEFAULT_OPTIONS = DEFAULT_OPTIONS; - -// --------------------------------------------------------------------------------------------------------------- // - -function wait_for( timeout ) { - return new Promise( ( resolve ) => { - setTimeout( resolve, timeout ); - } ); -} - -class ZstdDecompress extends Transform { - constructor( options ) { - super( options ); - - this._receivedChunks = []; - this._receivedLength = 0; - } - - _transform( chunk, encoding, callback ) { - this._receivedChunks.push( chunk ); - this._receivedLength += chunk.length; - callback(); - } - - async _flush( callback ) { - try { - this.push( - await decompress( Buffer.concat( this._receivedChunks, this._receivedLength ) ), - ); - callback(); - } catch ( e ) { - callback( e ); - } - } -} - -function decompress_response( res ) { - const content_encoding = res.headers[ 'content-encoding' ]; - - if ( content_encoding === 'zstd' ) { - return res.pipe( new ZstdDecompress() ); - } - - if ( content_encoding === 'gzip' || content_encoding === 'deflate' ) { - return res.pipe( createUnzip() ); - } - - return res; -} - -// --------------------------------------------------------------------------------------------------------------- // - -module.exports = request; - diff --git a/lib/request.ts b/lib/request.ts new file mode 100644 index 0000000..c9e9022 --- /dev/null +++ b/lib/request.ts @@ -0,0 +1,638 @@ +import http_ from 'http'; +import type { + Agent as HttpsAgent, + AgentOptions as HttpsAgentOptions, + RequestOptions as HttpsRequestOptions, +} from 'https'; +import https_ from 'https'; +import type { ParsedUrlQueryInput } from 'querystring'; +import qs_ from 'querystring'; +import url_ from 'url'; +import type { ZlibOptions } from 'node:zlib'; +import { createGzip, createUnzip } from 'node:zlib'; +import { decompress } from '@fengkx/zstd-napi'; +import type { TransformOptions, TransformCallback } from 'stream'; +import { Transform } from 'stream'; + +import type { EventTimestamps, LoggerEvent } from './logger'; +import type Logger from './logger'; +import { EVENT } from './logger'; + +import type { Deffered } from './getDeferred'; +import getDeferred from './getDeferred'; +import type Cancel from './cancel'; +import type { DescriptError, Reason } from './error'; +import { createError, ERROR_ID } from './error'; +import is_plain_object from './isPlainObject'; + +import extend from './extend'; +import type http from 'node:http'; +import type { DescriptHttpResult, DescriptJSON } from './types'; + +// --------------------------------------------------------------------------------------------------------------- // + +// FIXME: А зачем тут WeakMap, а не просто Map? +// +const agentsCacheHttp = new WeakMap(); +const agentsCacheHttps = new WeakMap(); + +const RX_IS_JSON = /^application\/json(?:;|\s|$)/; + +export interface DescriptRequestOptions { + protocol?: HttpsRequestOptions['protocol']; + hostname?: HttpsRequestOptions['hostname']; + port?: HttpsRequestOptions['port']; + method?: HttpsRequestOptions['method']; + pathname?: string; + family?: HttpsRequestOptions['family']; + auth?: HttpsRequestOptions['auth']; + headers?: HttpsRequestOptions['headers']; + + pfx?: HttpsRequestOptions['pfx']; + key?: HttpsRequestOptions['key']; + passphrase?: HttpsRequestOptions['passphrase']; + cert?: HttpsRequestOptions['cert']; + ca?: HttpsRequestOptions['ca']; + ciphers?: HttpsRequestOptions['ciphers']; + rejectUnauthorized?: HttpsRequestOptions['rejectUnauthorized']; + secureProtocol?: HttpsRequestOptions['secureProtocol']; + servername?: HttpsRequestOptions['servername']; + + query?: string | ParsedUrlQueryInput; + + isError?: (error: DescriptError) => boolean; + + isRetryAllowed?: (error: DescriptError, requestOptions: RequestOptions) => boolean; + body?: string | + Buffer | + DescriptJSON; + + maxRetries?: number; + retryTimeout?: number; + + retries?: number; + + extra?: { + name: string; + }; + + timeout?: number; + + bodyCompress?: ZlibOptions; + + agent?: HttpsAgent | HttpsAgentOptions | false | null; +} + +// --------------------------------------------------------------------------------------------------------------- // + +export const DEFAULT_OPTIONS = { + method: 'GET', + protocol: 'http:', + hostname: 'localhost', + pathname: '/', + + isError: function(error: DescriptError) { + const id = error.error.id; + const statusCode = Number(error.error.statusCode); + + return ( + id === ERROR_ID.TCP_CONNECTION_TIMEOUT || + id === ERROR_ID.REQUEST_TIMEOUT || + statusCode >= 400 + ); + }, + + isRetryAllowed: function(error: DescriptError, requestOptions: RequestOptions) { + const method = requestOptions.httpOptions.method; + if (method === 'POST' || method === 'PATCH') { + return false; + } + + const id = error.error.id; + const statusCode = Number(error.error.statusCode); + + return ( + id === ERROR_ID.TCP_CONNECTION_TIMEOUT || + id === ERROR_ID.REQUEST_TIMEOUT || + statusCode === 408 || + statusCode === 429 || + statusCode === 500 || + (statusCode >= 502 && statusCode <= 504) + ); + }, + + maxRetries: 0, + + retryTimeout: 100, +}; + +export class RequestOptions { + + isError: DescriptRequestOptions['isError']; + isRetryAllowed: DescriptRequestOptions['isRetryAllowed']; + maxRetries: number; + retries: number; + retryTimeout: number; + timeout: DescriptRequestOptions['timeout']; + bodyCompress: DescriptRequestOptions['bodyCompress']; + httpOptions: HttpsRequestOptions; + body: DescriptRequestOptions['body']; + extra: DescriptRequestOptions['extra']; + + url: string; + + requestModule: typeof https_ | typeof http_; + + constructor(options: DescriptRequestOptions) { + // NOTE: Тут не годится Object.assign, так как ключи с undefined перезатирают все. + options = extend({}, DEFAULT_OPTIONS, options); + + this.isError = options.isError; + this.isRetryAllowed = options.isRetryAllowed; + this.maxRetries = options.maxRetries || 0; + this.retryTimeout = options.retryTimeout || 0; + + this.retries = options.retries || 0; + + this.timeout = options.timeout; + this.bodyCompress = options.bodyCompress; + + this.httpOptions = {}; + + this.httpOptions.protocol = options.protocol; + this.httpOptions.hostname = options.hostname; + this.httpOptions.port = options.port; + + if (options.family) { + this.httpOptions.family = options.family; + } + if (!this.httpOptions.port) { + this.httpOptions.port = (this.httpOptions.protocol === 'https:') ? 443 : 80; + } + + let pathname = options.pathname; + if (pathname?.charAt(0) !== '/') { + pathname = '/' + pathname; + } + + this.httpOptions.path = url_.format({ + pathname: pathname, + query: options.query, + }); + + if (options.auth) { + this.httpOptions.auth = options.auth; + } + + // Нужно для логов. + this.url = url_.format({ + ...this.httpOptions, + // url.format игнорит свойство path, но смотрит на pathname + query/search. + pathname: pathname, + query: options.query, + }); + + this.httpOptions.headers = {}; + if (options.headers) { + for (const name in options.headers) { + this.httpOptions.headers[ name.toLowerCase() ] = options.headers[ name ]; + } + } + // Add gzip headers. + if (this.httpOptions.headers[ 'accept-encoding' ]) { + this.httpOptions.headers[ 'accept-encoding' ] = 'gzip,deflate,' + this.httpOptions.headers[ 'accept-encoding' ]; + + } else { + this.httpOptions.headers[ 'accept-encoding' ] = 'gzip,deflate'; + } + + const method = this.httpOptions.method = options.method?.toUpperCase(); + + this.body = null; + if (options.body && (method === 'POST' || method === 'PUT' || method === 'PATCH' || method === 'DELETE')) { + if (Buffer.isBuffer(options.body)) { + this.body = options.body; + this.setContentType('application/octet-stream'); + + } else if (typeof options.body !== 'object') { + this.body = String(options.body); + this.setContentType('text/plain'); + + } else if (RX_IS_JSON.test(String(this.httpOptions.headers[ 'content-type' ] || ''))) { + this.body = JSON.stringify(options.body); + + } else { + this.body = qs_.stringify(options.body as ParsedUrlQueryInput); + this.setContentType('application/x-www-form-urlencoded'); + } + + if (this.bodyCompress) { + this.httpOptions.headers[ 'content-encoding' ] = 'gzip'; + + } else { + this.httpOptions.headers[ 'content-length' ] = Buffer.byteLength(this.body as string); + } + } + + const isHttps = (this.httpOptions.protocol === 'https:'); + this.requestModule = (isHttps) ? https_ : http_; + + if (options.agent) { + if (is_plain_object(options.agent)) { + const agentsCache = (isHttps) ? agentsCacheHttps : agentsCacheHttp; + + let agent = agentsCache.get(options.agent); + if (!agent) { + agent = new this.requestModule.Agent(options.agent); + agentsCache.set(options.agent, agent); + } + this.httpOptions.agent = agent; + + } else { + // Здесь может быть либо `false`, либо `instanceof Agent`. + // Либо еще что-нибудь, инстанс какого-то левого агента типа TunnelingAgent. + // + this.httpOptions.agent = options.agent; + } + } + + if (this.httpOptions.protocol === 'https:') { + this.httpOptions.pfx = options.pfx; + this.httpOptions.key = options.key; + this.httpOptions.passphrase = options.passphrase; + this.httpOptions.cert = options.cert; + this.httpOptions.ca = options.ca; + this.httpOptions.ciphers = options.ciphers; + this.httpOptions.rejectUnauthorized = options.rejectUnauthorized; + this.httpOptions.secureProtocol = options.secureProtocol; + this.httpOptions.servername = options.servername; + } + + this.extra = options.extra; + } + + setContentType(contentType: string) { + if (this.httpOptions.headers && !this.httpOptions?.headers?.[ 'content-type' ]) { + this.httpOptions.headers[ 'content-type' ] = contentType; + } + } + +} + +// --------------------------------------------------------------------------------------------------------------- // + +class DescriptRequest { + + options: RequestOptions; + logger: Logger; + cancel: Cancel; + timestamps: EventTimestamps; + hTimeout: number | null; + req?: http.ClientRequest; + isResolved: boolean; + + deferred: Deffered; + + constructor(options: RequestOptions, logger: Logger, cancel: Cancel) { + this.options = options; + this.logger = logger; + this.cancel = cancel; + + this.timestamps = {}; + this.hTimeout = null; + + this.isResolved = false; + } + + start(): Promise { + this.log({ + type: EVENT.REQUEST_START, + requestOptions: this.options, + }); + + this.timestamps.start = Date.now(); + + this.deferred = getDeferred(); + + this.cancel.subscribe((error) => this.doCancel(createError({ + id: ERROR_ID.HTTP_REQUEST_ABORTED, + reason: error, + }))); + this.setTimeout(); + + try { + this.req = this.options.requestModule.request(this.options.httpOptions, async(res) => { + try { + const result = await this.requestHandler(res); + this.doDone(result); + + } catch (error) { + this.doFail(error); + } + }); + + let onConnect: (() => void) | null = null; + + this.req.on('socket', (socket) => { + this.timestamps.socket = Date.now(); + + if (!socket.connecting) { + // Это сокет из пула, на нем не будет события 'connect'. + this.timestamps.tcpConnection = this.timestamps.socket; + + } else { + onConnect = () => { + this.timestamps.tcpConnection = Date.now(); + }; + + socket.once('connect', onConnect); + } + }); + + this.req.on('error', (error) => { + if (onConnect && this.req?.socket) { + this.req.socket.removeListener('connect', onConnect); + + onConnect = null; + } + + if (this.req?.aborted) { + // FIXME: правда ли нет ситуация, когда это приведет к повисанию запроса? + return; + } + if (this.isResolved) { + return; + } + + const reason = { + id: ERROR_ID.HTTP_UNKNOWN_ERROR, + message: error.message, + }; + this.destroyRequestSocket(); + + this.doFail(reason); + }); + + + if (this.options.bodyCompress && this.options.body) { + const gzipStream = createGzip(this.options.bodyCompress); + gzipStream.pipe(this.req, { end: true }); + gzipStream.end(this.options.body, () => { + this.timestamps.body = Date.now(); + this.timestamps.requestEnd = Date.now(); + }); + + } else { + if (this.options.body) { + this.req.write(this.options.body, () => { + this.timestamps.body = Date.now(); + }); + } + + this.req.end(() => { + this.timestamps.requestEnd = Date.now(); + }); + } + } catch (e) { + this.doFail(e); + } + + return this.deferred.promise; + } + + doDone(result: DescriptHttpResult) { + if (this.isResolved) { + return; + } + + this.clearTimeout(); + + this.timestamps.end = this.timestamps.end || Date.now(); + + this.log({ + type: EVENT.REQUEST_SUCCESS, + requestOptions: this.options, + result: result, + timestamps: this.timestamps, + }); + + this.isResolved = true; + + this.deferred.resolve(result); + } + + doFail(reason: Reason) { + if (this.isResolved) { + return; + } + + this.clearTimeout(); + + this.timestamps.end = this.timestamps.end || Date.now(); + + const error = createError(reason); + + this.log({ + type: EVENT.REQUEST_ERROR, + requestOptions: this.options, + error: error, + timestamps: this.timestamps, + }); + + this.isResolved = true; + + this.deferred.reject(error); + } + + doCancel(error: Reason) { + if (this.req) { + this.req.abort(); + } + + this.doFail(error); + } + + setTimeout() { + if ((this.options.timeout || 0) > 0) { + this.hTimeout = setTimeout(() => { + let error; + if (!this.timestamps.tcpConnection) { + // Не смогли к этому моменту установить tcp-соединение. + error = ERROR_ID.TCP_CONNECTION_TIMEOUT; + } else { + // Тут просто слишком долго выполняли запрос целиком. + error = ERROR_ID.REQUEST_TIMEOUT; + } + + this.doCancel(error); + + }, this.options.timeout) as unknown as number; + } + } + + clearTimeout() { + if (this.hTimeout) { + clearTimeout(this.hTimeout); + this.hTimeout = null; + } + } + + destroyRequestSocket() { + if (this.req && this.req.socket) { + this.req.socket.destroy(); + } + } + + async requestHandler(res: http.IncomingMessage): Promise { + res.once('readable', () => { + this.timestamps.firstByte = Date.now(); + }); + + const unzipped = decompressResponse(res); + + const buffers = []; + let receivedLength = 0; + + // NOTE: nodejs v15+ + // [8a6fab02ad] - (SEMVER-MAJOR) http: emit 'error' on aborted server request (Robert Nagy) #33172 + // https://github.com/nodejs/node/pull/33172 + try { + for await (const chunk of unzipped) { + this.cancel.throwIfCancelled(); + + buffers.push(chunk); + receivedLength += chunk.length; + } + } catch (error) { + const isAborted = error.code === 'ECONNRESET'; + if (!isAborted) { + throw error; + } + } + + if (!res.complete) { + const error = createError(ERROR_ID.INCOMPLETE_RESPONSE); + throw error; + } + + const statusCode = res.statusCode as number; + const body = (receivedLength) ? Buffer.concat(buffers, receivedLength) : null; + const headers = res.headers; + + const error = createError({ + id: 'HTTP_' + statusCode, + statusCode: statusCode, + headers: headers, + body: body, + error: { message: http_.STATUS_CODES[ statusCode ] }, + }); + if (this.options.isError?.(error)) { + throw error; + } + + return { + statusCode: statusCode, + requestOptions: this.options, + headers: headers, + timestamps: this.timestamps, + body: body, + }; + } + + log(event: LoggerEvent) { + if (this.logger) { + event.request = this.req; + this.logger.log(event); + } + } + +} + +// --------------------------------------------------------------------------------------------------------------- // + +async function request(options: DescriptRequestOptions, logger: Logger, cancel: Cancel): Promise { + const requestOptions = new RequestOptions(options); + + // eslint-disable-next-line no-constant-condition + while (true) { + const req = new DescriptRequest(requestOptions, logger, cancel); + + try { + const result = await req.start(); + + return result; + + } catch (error) { + if (error.error.statusCode === 429 || error.error.statusCode >= 500) { + // Удаляем сокет, чтобы не залипать на отвечающем ошибкой бекэнде. + req.destroyRequestSocket(); + } + + if (requestOptions.retries < requestOptions.maxRetries && requestOptions.isRetryAllowed?.(error, requestOptions)) { + requestOptions.retries++; + + if (requestOptions.retryTimeout > 0) { + await waitFor(requestOptions.retryTimeout); + } + + } else { + throw error; + } + } + } +} + +request.DEFAULT_OPTIONS = DEFAULT_OPTIONS; + +function waitFor(timeout: number) { + return new Promise((resolve) => { + setTimeout(resolve, timeout); + }); +} + +class ZstdDecompress extends Transform { + receivedLength: number; + receivedChunks: Array; + + constructor(options?: TransformOptions) { + super(options); + + this.receivedChunks = []; + this.receivedLength = 0; + } + + _transform(chunk: any, encoding: BufferEncoding, callback: TransformCallback) { + this.receivedChunks.push(chunk); + this.receivedLength += chunk.length; + callback(); + } + + async _flush(callback: TransformCallback) { + try { + this.push( + await decompress(Buffer.concat(this.receivedChunks, this.receivedLength)), + ); + callback(); + } catch (e) { + callback(e); + } + } +} + +function decompressResponse(res: http.IncomingMessage) { + const contentEncoding = res.headers[ 'content-encoding' ]; + + if (contentEncoding === 'zstd') { + return res.pipe(new ZstdDecompress()); + } + + if (contentEncoding === 'gzip' || contentEncoding === 'deflate') { + return res.pipe(createUnzip()); + } + + return res; +} + +// --------------------------------------------------------------------------------------------------------------- // + +export default request; diff --git a/lib/stripNullAndUndefinedValues.ts b/lib/stripNullAndUndefinedValues.ts new file mode 100644 index 0000000..fadf19f --- /dev/null +++ b/lib/stripNullAndUndefinedValues.ts @@ -0,0 +1,18 @@ +type Results = { + [K in keyof T]: Exclude +} + + +export default function stripNullAndUndefinedValue(obj: T): Results { + const r: Partial> = {}; + + for (const key in obj) { + const value = obj[ key ]; + + if (value != null) { + r[ key as keyof T] = value; + } + } + + return r as Results; +} diff --git a/lib/strip_null_and_undefined_values.js b/lib/strip_null_and_undefined_values.js deleted file mode 100644 index d77ec60..0000000 --- a/lib/strip_null_and_undefined_values.js +++ /dev/null @@ -1,14 +0,0 @@ -module.exports = function strip_null_and_undefined_values( obj ) { - const r = {}; - - for ( const key in obj ) { - const value = obj[ key ]; - - if ( value != null ) { - r[ key ] = value; - } - } - - return r; -}; - diff --git a/lib/types.ts b/lib/types.ts new file mode 100644 index 0000000..0847b6f --- /dev/null +++ b/lib/types.ts @@ -0,0 +1,187 @@ +import type Cancel from './cancel'; +import type BaseBlock from './block'; +import type { DescriptBlockDeps, DescriptBlockId } from './depsDomain'; +import type { DescriptError } from './error'; +import type Cache from './cache'; +import type DescriptLogger from './logger'; +import type { IncomingHttpHeaders } from 'http'; +import type { EventTimestamps } from './logger'; +import type { RequestOptions } from './request'; + +export interface DescriptHttpBlockResult { + statusCode: number; + headers: DescriptHttpBlockHeaders; + requestOptions: RequestOptions; + + result: Result; +} + +export type DescriptHttpBlockHeaders = IncomingHttpHeaders; +export interface DescriptHttpResult { + statusCode: number; + headers: DescriptHttpBlockHeaders; + requestOptions: RequestOptions; + body: Buffer | null; + + timestamps: EventTimestamps; +} + +export type UnionToIntersection< U > = ( + U extends any ? + (k: U) => void : + never +) extends ( + (k: infer I) => void +) ? I : never; + + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface DescriptContext {} + +export type NonNullableObject> = { + [P in keyof T]: Exclude; +} + +export type DescriptJSON = + boolean | + number | + string | + undefined | + null | + { [ property: string ]: DescriptJSON } | + object | + Array< DescriptJSON >; + +export type BlockResultOut< + BlockResult, + BeforeResultOut, + AfterResultOut, + ErrorResultOut +> = + (never | undefined)extends ErrorResultOut ? + AfterResultOut extends undefined ? + never | undefined extends BeforeResultOut ? + BlockResult : + BeforeResultOut | BlockResult : + AfterResultOut : + AfterResultOut extends undefined ? + never | undefined extends BeforeResultOut ? + BlockResult | ErrorResultOut : + BeforeResultOut | BlockResult | ErrorResultOut : + AfterResultOut | ErrorResultOut; + +export type InferResultOrResult = Result extends BaseBlock< +// eslint-disable-next-line @typescript-eslint/no-unused-vars +infer Context, infer CustomBlock, infer ParamsOut, infer ResultOut, infer IntermediateResult, +// eslint-disable-next-line @typescript-eslint/no-unused-vars +infer BlockResult, infer BeforeResultOut, infer AfterResultOut, infer ErrorResultOut, infer Params +> ? InferResultOrResult : Result; + +export type InferResultFromBlock = Type extends BaseBlock< +// eslint-disable-next-line @typescript-eslint/no-unused-vars +infer Context, infer CustomBlock, infer ParamsOut, infer ResultOut, infer IntermediateResult, +// eslint-disable-next-line @typescript-eslint/no-unused-vars +infer BlockResult, infer BeforeResultOut, infer AfterResultOut, infer ErrorResultOut, infer Params +> ? InferResultOrResult : never; + +export type InferParamsInFromBlock = Type extends BaseBlock< +// eslint-disable-next-line @typescript-eslint/no-unused-vars +infer Context, infer CustomBlock, infer ParamsOut, infer ResultOut, infer IntermediateResult, +// eslint-disable-next-line @typescript-eslint/no-unused-vars +infer BlockResult, infer BeforeResultOut, infer AfterResultOut, infer ErrorResultOut, infer Params + +> ? Params : never; + + +export type InferParamsInFromBlockOrParams = Type extends BaseBlock< +// eslint-disable-next-line @typescript-eslint/no-unused-vars +infer Context, infer CustomBlock, infer ParamsOut, infer ResultOut, infer IntermediateResult, +// eslint-disable-next-line @typescript-eslint/no-unused-vars +infer BlockResult, infer BeforeResultOut, infer AfterResultOut, infer ErrorResultOut, infer Params +> ? Params : P; + + +export type InferParamsOutFromBlock = Type extends BaseBlock< +// eslint-disable-next-line @typescript-eslint/no-unused-vars +infer Context, infer CustomBlock, infer ParamsOut, infer ResultOut, infer IntermediateResult, +// eslint-disable-next-line @typescript-eslint/no-unused-vars +infer BlockResult, infer BeforeResultOut, infer AfterResultOut, infer ErrorResultOut, infer Params +> ? ParamsOut : never; + + +export type InferContextFromBlock< T > = T extends BaseBlock< +// eslint-disable-next-line @typescript-eslint/no-unused-vars +infer Context, infer CustomBlock, infer ParamsOut, infer ResultOut, infer IntermediateResult, +// eslint-disable-next-line @typescript-eslint/no-unused-vars +infer BlockResult, infer BeforeResultOut, infer AfterResultOut, infer ErrorResultOut, infer Params +> ? Context : never; + +export type First< T > = +// eslint-disable-next-line @typescript-eslint/no-unused-vars + T extends readonly [ infer First, ...infer Rest ] | [ infer First, ...infer Rest ] ? First : never; + +export type Tail< T > = +// eslint-disable-next-line @typescript-eslint/no-unused-vars + T extends readonly [ infer First, ...infer Rest ] | [ infer First, ...infer Rest ] ? Rest : never; + +export type Equal< A, B > = A extends B ? (B extends A ? A : never) : never; + +export type DepsIds = Array; +export interface DescriptBlockOptions< + Context, + ParamsOut, + BlockResult, + BeforeResultOut, + AfterResultOut, + ErrorResultOut, + Params = ParamsOut, +> { + name?: string; + + id?: DescriptBlockId; + deps?: DescriptBlockId | DepsIds | null; + + params?: (args: { + params: Params; + context?: Context; + deps: DescriptBlockDeps; + }) => ParamsOut; + + before?: (args: { + params: ParamsOut; + context?: Context; + deps: DescriptBlockDeps; + cancel: Cancel; + }) => BeforeResultOut; + + after?: (args: { + params: ParamsOut; + context?: Context; + deps: DescriptBlockDeps; + cancel: Cancel; + result: BeforeResultOut extends (undefined | void) ? + InferResultOrResult : InferResultOrResult | InferResultOrResult; + }) => AfterResultOut; + + error?: (args: { + params: ParamsOut; + context?: Context; + deps: DescriptBlockDeps; + cancel: Cancel; + error: DescriptError; + }) => ErrorResultOut; + + timeout?: number; + + key?: string | ((args: { + params: ParamsOut; + context?: Context; + deps: DescriptBlockDeps; + }) => string); + maxage?: number; + cache?: Cache; + + required?: boolean; + + logger?: DescriptLogger; +} diff --git a/package-lock.json b/package-lock.json index e9239e3..ea418d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "descript", - "version": "3.3.4", + "version": "4.0.0", "lockfileVersion": 3, "requires": true, "packages": { @@ -12,13 +12,17 @@ "@fengkx/zstd-napi": "^0.1.0" }, "devDependencies": { + "@types/jest": "^29.5.13", "@types/node": "^20.12.7", + "@typescript-eslint/eslint-plugin": "^5.62.0", + "@typescript-eslint/parser": "^5.62.0", "eslint": "^8.57.0", - "eslint-plugin-jest": "^28.3.0", - "eslint-plugin-nop": "0.0.3", + "eslint-plugin-jest": "^27.9.0", "jest": "^29.7.0", + "ts-add-js-extension": "^1.6.4", + "ts-jest": "^29.2.5", "ts-node": "^10.9.2", - "typescript": "^5.4.5" + "typescript": "^5.6.2" }, "engines": { "node": "*" @@ -685,9 +689,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" @@ -1531,6 +1535,16 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/jest": { + "version": "29.5.13", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.13.tgz", + "integrity": "sha512-wd+MVEZCHt23V0/L642O5APvspWply/rGY5BcW4SUETo2UzPU3Z26qr8jC2qxpimI2jjx9h7+2cj2FwIr01bXg==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1573,108 +1587,168 @@ "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", "dev": true }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", - "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", + "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0" + "@eslint-community/regexpp": "^4.4.0", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/type-utils": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "node_modules/@typescript-eslint/types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", - "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, - "engines": { - "node": "^16.0.0 || >=18.0.0" + "bin": { + "semver": "bin/semver.js" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "engines": { + "node": ">=10" } }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", - "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "node_modules/@typescript-eslint/parser": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", + "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/visitor-keys": "6.21.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "9.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "debug": "^4.3.4" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, "peerDependenciesMeta": { "typescript": { "optional": true } } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", "dev": true, "dependencies": { - "balanced-match": "^1.0.0" + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "node_modules/@typescript-eslint/type-utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", + "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", "dev": true, "dependencies": { - "yallist": "^4.0.0" + "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/utils": "5.62.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" }, "engines": { - "node": ">=10" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", "dev": true, "dependencies": { - "brace-expansion": "^2.0.1" + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -1682,57 +1756,59 @@ "node": ">=10" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/@typescript-eslint/utils": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", - "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", "dev": true, "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.21.0", - "@typescript-eslint/types": "6.21.0", - "@typescript-eslint/typescript-estree": "6.21.0", - "semver": "^7.5.4" + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/@typescript-eslint/utils/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "node_modules/@typescript-eslint/utils/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", "dev": true, "dependencies": { - "yallist": "^4.0.0" + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" }, "engines": { - "node": ">=10" + "node": ">=8.0.0" + } + }, + "node_modules/@typescript-eslint/utils/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" } }, "node_modules/@typescript-eslint/utils/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -1740,23 +1816,17 @@ "node": ">=10" } }, - "node_modules/@typescript-eslint/utils/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", - "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.21.0", - "eslint-visitor-keys": "^3.4.1" + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { "type": "opencollective", @@ -1770,9 +1840,9 @@ "dev": true }, "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz", + "integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -1897,6 +1967,12 @@ "node": ">=8" } }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -2064,6 +2140,18 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/bser": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", @@ -2391,6 +2479,21 @@ "node": ">=6.0.0" } }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/electron-to-chromium": { "version": "1.4.693", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.693.tgz", @@ -2501,19 +2604,19 @@ } }, "node_modules/eslint-plugin-jest": { - "version": "28.3.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.3.0.tgz", - "integrity": "sha512-5LjCSSno8E+IUCOX4hJiIb/upPIgpkaDEcaN/40gOcw26t/5UTLHFc4JdxKjOOvGTh0XdCu+fNr0fpOVNvcxMA==", + "version": "27.9.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.9.0.tgz", + "integrity": "sha512-QIT7FH7fNmd9n4se7FFKHbsLKGQiw885Ds6Y/sxKgCZ6natwCsXdgPOADnYVxN2QrRweF0FZWbJ6S7Rsn7llug==", "dev": true, "dependencies": { - "@typescript-eslint/utils": "^6.0.0" + "@typescript-eslint/utils": "^5.10.0" }, "engines": { - "node": "^16.10.0 || ^18.12.0 || >=20.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, "peerDependencies": { - "@typescript-eslint/eslint-plugin": "^6.0.0 || ^7.0.0", - "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0", + "@typescript-eslint/eslint-plugin": "^5.0.0 || ^6.0.0 || ^7.0.0", + "eslint": "^7.0.0 || ^8.0.0", "jest": "*" }, "peerDependenciesMeta": { @@ -2525,15 +2628,6 @@ } } }, - "node_modules/eslint-plugin-nop": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-nop/-/eslint-plugin-nop-0.0.3.tgz", - "integrity": "sha512-0gB8BiDePR7LutqhQSyV8NJxwKv1qi92cQPnsn2AZyxZVG6uIhcQ/jbSRwQk4nCoKELorA7OLE6WYY631kNn2g==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", @@ -2804,6 +2898,36 @@ "node": "^10.12.0 || >=12.0.0" } }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -3203,9 +3327,9 @@ } }, "node_modules/istanbul-lib-instrument": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.2.tgz", - "integrity": "sha512-1WUsZ9R1lA0HtBSohTkm39WTPlNKSJ5iFk7UwqXkBLoHQT+hfqPsfsTDVuZdKGaBwn7din9bS7SsnoAr943hvw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", "dev": true, "dependencies": { "@babel/core": "^7.23.9", @@ -3218,26 +3342,11 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-instrument/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -3245,12 +3354,6 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-instrument/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/istanbul-lib-report": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", @@ -3292,6 +3395,24 @@ "node": ">=8" } }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", @@ -3752,26 +3873,11 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-snapshot/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -3779,12 +3885,6 @@ "node": ">=10" } }, - "node_modules/jest-snapshot/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/jest-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", @@ -3989,6 +4089,12 @@ "node": ">=8" } }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4019,26 +4125,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/make-dir/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/make-dir/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -4046,12 +4137,6 @@ "node": ">=10" } }, - "node_modules/make-dir/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -4128,6 +4213,12 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -4801,16 +4892,76 @@ "node": ">=8.0" } }, - "node_modules/ts-api-utils": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", - "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "node_modules/ts-add-js-extension": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ts-add-js-extension/-/ts-add-js-extension-1.6.4.tgz", + "integrity": "sha512-iY0rS5vLbleT4KPEq6ICdHJew+2wyXSK1wsB7HAChiMSQONRuVP7BY1q5k6gVzPXrrtWzV5qEgVujwj1nnFN7Q==", + "dev": true, + "dependencies": { + "typescript": "^5.4.3" + }, + "bin": { + "ts-add-js-extension": "build/cjs/bin.js" + } + }, + "node_modules/ts-jest": { + "version": "29.2.5", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.5.tgz", + "integrity": "sha512-KD8zB2aAZrcKIdGk4OwpJggeLcH1FgrICqDSROWqlnJXGCXK4Mn6FcdK2B6670Xr73lHMG1kHw8R87A0ecZ+vA==", "dev": true, + "dependencies": { + "bs-logger": "^0.2.6", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "^2.1.0", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.6.3", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, "engines": { - "node": ">=16" + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" }, "peerDependencies": { - "typescript": ">=4.2.0" + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/ts-node": { @@ -4856,6 +5007,27 @@ } } }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -4890,9 +5062,9 @@ } }, "node_modules/typescript": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", - "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz", + "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==", "dev": true, "bin": { "tsc": "bin/tsc", diff --git a/package.json b/package.json index eedb266..4113e76 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ }, "name": "descript", "description": "descript", - "version": "3.3.4", + "version": "4.0.0", "homepage": "https://github.com/pasaran/descript3", "repository": { "type": "git", @@ -15,21 +15,26 @@ "scripts": { "eslint": "npx eslint .", "test": "cd tests && ./gen-certs.sh && npx jest", - "ts-compile": "npx tsc --noEmit" + "ts-compile": "npx tsc --noEmit", + "prepack": "rm -rf build && npx tsc -p ./tsconfig-build.json && npx ts-add-js-extension --dir=build" }, "devDependencies": { + "@types/jest": "^29.5.13", "@types/node": "^20.12.7", + "@typescript-eslint/eslint-plugin": "^5.62.0", + "@typescript-eslint/parser": "^5.62.0", "eslint": "^8.57.0", - "eslint-plugin-jest": "^28.3.0", - "eslint-plugin-nop": "0.0.3", + "eslint-plugin-jest": "^27.9.0", "jest": "^29.7.0", + "ts-add-js-extension": "^1.6.4", + "ts-jest": "^29.2.5", "ts-node": "^10.9.2", - "typescript": "^5.4.5" + "typescript": "^5.6.2" }, "files": [ "lib" ], - "main": "lib/index.js", + "main": "./build/index.js", "engines": { "node": "*" }, diff --git a/tests/arrayBlock.test.ts b/tests/arrayBlock.test.ts new file mode 100644 index 0000000..3751fa0 --- /dev/null +++ b/tests/arrayBlock.test.ts @@ -0,0 +1,420 @@ +import * as de from '../lib' ; + +import { + // wait_for_value, + // wait_for_error, + getResultBlock, + getErrorBlock, + getTimeout, +} from './helpers' ; + +import { describe, it, expect, jest } from '@jest/globals'; + +// --------------------------------------------------------------------------------------------------------------- // + +describe('de.array', () => { + + it('block is undefined #1', () => { + expect.assertions(2); + + let error; + + try { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + de.array({ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + block: undefined, + }); + + } catch (e) { + error = e; + } + + expect(de.isError(error)).toBe(true); + expect(error.error.id).toBe(de.ERROR_ID.INVALID_BLOCK); + }); + + it('block is undefined #2', () => { + expect.assertions(2); + let error; + + try { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + de.array({}); + + } catch (e) { + error = e; + } + + expect(de.isError(error)).toBe(true); + expect(error.error.id).toBe(de.ERROR_ID.INVALID_BLOCK); + }); + + it('empty array', async() => { + const block = de.array({ + block: [], + }); + + const result = await de.run(block); + + expect(result).toEqual([]); + }); + + + it('two subblocks', async() => { + const dataFoo = { + foo: 42, + }; + const blockFoo = getResultBlock(dataFoo, getTimeout(50, 100)); + + const dataBar = { + bar: 24, + }; + const blockBar = getResultBlock(dataBar, getTimeout(50, 100)); + + const block = de.array({ + block: [ + blockFoo, + blockBar, + ], + }); + + const result = await de.run(block); + + expect(result).toEqual([ + dataFoo, + dataBar, + ]); + const r1 = result[ 0 ]; + expect(r1).toBe(dataFoo); + expect(result[ 1 ]).toBe(dataBar); + }); + + it('two subblocks, one required', async() => { + const dataFoo = { + foo: 42, + }; + const blockFoo = getResultBlock(dataFoo, getTimeout(50, 100)); + + const dataBar = { + bar: 24, + }; + const blockBar = getResultBlock(dataBar, getTimeout(50, 100)); + + const block = de.array({ + block: [ + blockFoo.extend({ + options: { + required: true, + }, + }), + blockBar, + ], + }); + + const result = await de.run(block); + //const result2 = block.run({}); + + expect(result).toEqual([ + dataFoo, + dataBar, + ]); + expect(result[ 0 ]).toBe(dataFoo); + expect(result[ 1 ]).toBe(dataBar); + }); + + it('two subblocks, one failed', async() => { + const errorFoo = de.error('SOME_ERROR'); + const blockFoo = getErrorBlock(errorFoo, getTimeout(50, 100)); + + const dataBar = { + bar: 24, + }; + const blockBar = getResultBlock(dataBar, getTimeout(50, 100)); + + const block = de.array({ + block: [ + blockFoo, + blockBar, + ], + }); + + const result = await de.run(block); + + expect(result[ 0 ]).toBe(errorFoo); + expect(result[ 1 ]).toBe(dataBar); + }); + + it('two subblocks, both failed', async() => { + const errorFoo = de.error('SOME_ERROR_1'); + const blockFoo = getErrorBlock(errorFoo, getTimeout(50, 100)); + + const errorBar = de.error('SOME_ERROR_2'); + const blockBar = getErrorBlock(errorBar, getTimeout(50, 100)); + + const block = de.array({ + block: [ + blockFoo, + blockBar, + ], + }); + + const result = await de.run(block); + + expect(result[ 0 ]).toBe(errorFoo); + expect(result[ 1 ]).toBe(errorBar); + }); + + it('two subblocks, one required failed #1', async() => { + const errorFoo = de.error('SOME_ERROR'); + const blockFoo = getErrorBlock(errorFoo, getTimeout(50, 100)); + + const dataBar = { + bar: 24, + }; + const blockBar = getResultBlock(dataBar, getTimeout(50, 100)); + + const block = de.array({ + block: [ + blockFoo.extend({ + options: { + required: true, + }, + }), + blockBar, + ], + }); + + expect.assertions(4); + + let error; + + try { + await de.run(block); + + } catch (e) { + error = e; + } + + expect(de.isError(error)).toBe(true); + expect(error.error.id).toBe(de.ERROR_ID.REQUIRED_BLOCK_FAILED); + expect(error.error.reason).toBe(errorFoo); + expect(error.error.path).toBe('[ 0 ]'); + }); + + it('two subblocks, one required failed #2', async() => { + const errorFoo = de.error('SOME_ERROR'); + const blockFoo = getErrorBlock(errorFoo, getTimeout(50, 100)); + const blockBar = getResultBlock(null, getTimeout(50, 100)); + const blockQuu = getResultBlock(null, getTimeout(50, 100)); + + const blockComplex = de.object({ + block: { + foo: blockFoo.extend({ + options: { + params: ({ params }: { params: { x: number }}) => { + return { + ...params, + x: 1, + }; + }, + required: true, + }, + }), + bar: blockBar.extend({ + options: { + params: ({ params }: { params: { z: number }}) => { + return { + ...params, + x: 1, + }; + }, + }, + }), + }, + + options: { + required: true, + }, + }); + + const block = de.array({ + block: [ + blockComplex, + blockQuu, + ], + }); + + expect.assertions(3); + let error; + + try { + const params = { + z: 1, + x: 1, + }; + + await de.run(block, { + params, + }); + + } catch (e) { + error = e; + } + + expect(de.isError(error)).toBe(true); + expect(error.error.id).toBe(de.ERROR_ID.REQUIRED_BLOCK_FAILED); + expect(error.error.path).toBe('[ 0 ].foo'); + }); + + describe('cancel', () => { + + it('cancel object, subblocks cancelled too #1', async() => { + const actionFooSpy = jest.fn(); + const blockFoo = getResultBlock(null, 150, { + onCancel: actionFooSpy, + }); + + const actionBarSpy = jest.fn(); + const blockBar = getResultBlock(null, 150, { + onCancel: actionBarSpy, + }); + + const block = de.array({ + block: [ + blockFoo, + blockBar, + ], + }); + + const abortError = de.error('SOME_ERROR'); + let error; + + try { + const cancel = new de.Cancel(); + setTimeout(() => { + cancel.cancel(abortError); + }, 100); + await de.run(block, { cancel }); + + } catch (e) { + error = e; + } + + expect(error).toBe(abortError); + expect(actionFooSpy.mock.calls[ 0 ][ 0 ]).toBe(abortError); + expect(actionBarSpy.mock.calls[ 0 ][ 0 ]).toBe(abortError); + }); + + it('cancel object, subblocks cancelled too #2', async() => { + const actionFooSpy = jest.fn(); + const blockFoo = getResultBlock(null, 50, { + onCancel: actionFooSpy, + }); + + const actionBarSpy = jest.fn(); + const blockBar = getResultBlock(null, 150, { + onCancel: actionBarSpy, + }); + + const block = de.array({ + block: [ + blockFoo, + blockBar, + ], + }); + + const abortError = de.error('SOME_ERROR'); + + let error; + + try { + const cancel = new de.Cancel(); + setTimeout(() => { + cancel.cancel(abortError); + }, 100); + await de.run(block, { cancel }); + + } catch (e) { + error = e; + } + + expect(error).toBe(abortError); + expect(actionFooSpy.mock.calls).toHaveLength(0); + expect(actionBarSpy.mock.calls[ 0 ][ 0 ]).toBe(abortError); + }); + + it('required block failed, other subblocks cancelled', async() => { + const errorFoo = de.error('SOME_ERROR'); + const blockFoo = getErrorBlock(errorFoo, 50); + + const actionBarSpy = jest.fn(); + const blockBar = getResultBlock(null, 150, { + onCancel: actionBarSpy, + }); + + const block = de.array({ + block: [ + blockFoo.extend({ + options: { + required: true, + }, + }), + blockBar, + ], + }); + + try { + await de.run(block); + + } catch (e) {} + + const call00 = actionBarSpy.mock.calls[ 0 ][ 0 ]; + expect(de.isError(call00)).toBe(true); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(call00.error.id).toBe(de.ERROR_ID.REQUIRED_BLOCK_FAILED); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(call00.error.reason).toBe(errorFoo); + }); + + }); + + describe('inheritance', () => { + + it('two subblocks', async() => { + const dataFoo = { + foo: 42, + }; + const blockFoo = getResultBlock(dataFoo, getTimeout(50, 100)); + + const dataBar = { + bar: 24, + }; + const blockBar = getResultBlock(dataBar, getTimeout(50, 100)); + + const parent = de.array({ + block: [ + blockFoo, + blockBar, + ], + }); + const child = parent.extend({ options: {} }); + + const result = await de.run(child); + + expect(result).toEqual([ + dataFoo, + dataBar, + ]); + expect(result[ 0 ]).toBe(dataFoo); + expect(result[ 1 ]).toBe(dataBar); + }); + + }); + +}); diff --git a/tests/array_block.test.js b/tests/array_block.test.js deleted file mode 100644 index 0309534..0000000 --- a/tests/array_block.test.js +++ /dev/null @@ -1,372 +0,0 @@ -const de = require( '../lib' ); - -const { - // wait_for_value, - // wait_for_error, - get_result_block, - get_error_block, - get_timeout, -} = require( './helpers' ); - -// --------------------------------------------------------------------------------------------------------------- // - -describe( 'de.array', () => { - - it( 'block is undefined #1', () => { - expect.assertions( 2 ); - try { - de.array( { - block: undefined, - } ); - - } catch ( e ) { - expect( de.is_error( e ) ).toBe( true ); - expect( e.error.id ).toBe( de.ERROR_ID.INVALID_BLOCK ); - } - } ); - - it( 'block is undefined #2', () => { - expect.assertions( 2 ); - try { - de.array(); - - } catch ( e ) { - expect( de.is_error( e ) ).toBe( true ); - expect( e.error.id ).toBe( de.ERROR_ID.INVALID_BLOCK ); - } - } ); - - it( 'empty array', async () => { - const block = de.array( { - block: [], - } ); - - const result = await de.run( block ); - - expect( result ).toEqual( [] ); - } ); - - - it( 'two subblocks', async () => { - const data_foo = { - foo: 42, - }; - const block_foo = get_result_block( data_foo, get_timeout( 50, 100 ) ); - - const data_bar = { - bar: 24, - }; - const block_bar = get_result_block( data_bar, get_timeout( 50, 100 ) ); - - const block = de.array( { - block: [ - block_foo, - block_bar, - ], - } ); - - const result = await de.run( block ); - - expect( result ).toEqual( [ - data_foo, - data_bar, - ] ); - expect( result[ 0 ] ).toBe( data_foo ); - expect( result[ 1 ] ).toBe( data_bar ); - } ); - - it( 'two subblocks, one required', async () => { - const data_foo = { - foo: 42, - }; - const block_foo = get_result_block( data_foo, get_timeout( 50, 100 ) ); - - const data_bar = { - bar: 24, - }; - const block_bar = get_result_block( data_bar, get_timeout( 50, 100 ) ); - - const block = de.array( { - block: [ - block_foo( { - options: { - required: true, - }, - } ), - block_bar, - ], - } ); - - const result = await de.run( block ); - - expect( result ).toEqual( [ - data_foo, - data_bar, - ] ); - expect( result[ 0 ] ).toBe( data_foo ); - expect( result[ 1 ] ).toBe( data_bar ); - } ); - - it( 'two subblocks, one failed', async () => { - const error_foo = de.error( { - id: 'SOME_ERROR', - } ); - const block_foo = get_error_block( error_foo, get_timeout( 50, 100 ) ); - - const data_bar = { - bar: 24, - }; - const block_bar = get_result_block( data_bar, get_timeout( 50, 100 ) ); - - const block = de.array( { - block: [ - block_foo, - block_bar, - ], - } ); - - const result = await de.run( block ); - - expect( result[ 0 ] ).toBe( error_foo ); - expect( result[ 1 ] ).toBe( data_bar ); - } ); - - it( 'two subblocks, both failed', async () => { - const error_foo = de.error( { - id: 'SOME_ERROR_1', - } ); - const block_foo = get_error_block( error_foo, get_timeout( 50, 100 ) ); - - const error_bar = de.error( { - id: 'SOME_ERROR_2', - } ); - const block_bar = get_error_block( error_bar, get_timeout( 50, 100 ) ); - - const block = de.array( { - block: [ - block_foo, - block_bar, - ], - } ); - - const result = await de.run( block ); - - expect( result[ 0 ] ).toBe( error_foo ); - expect( result[ 1 ] ).toBe( error_bar ); - } ); - - it( 'two subblocks, one required failed #1', async () => { - const error_foo = de.error( { - id: 'SOME_ERROR', - } ); - const block_foo = get_error_block( error_foo, get_timeout( 50, 100 ) ); - - const data_bar = { - bar: 24, - }; - const block_bar = get_result_block( data_bar, get_timeout( 50, 100 ) ); - - const block = de.array( { - block: [ - block_foo( { - options: { - required: true, - }, - } ), - block_bar, - ], - } ); - - expect.assertions( 4 ); - try { - await de.run( block ); - - } catch ( e ) { - expect( de.is_error( e ) ).toBe( true ); - expect( e.error.id ).toBe( de.ERROR_ID.REQUIRED_BLOCK_FAILED ); - expect( e.error.reason ).toBe( error_foo ); - expect( e.error.path ).toBe( '[ 0 ]' ); - } - } ); - - it( 'two subblocks, one required failed #2', async () => { - const error_foo = de.error( { - id: 'SOME_ERROR', - } ); - const block_foo = get_error_block( error_foo, get_timeout( 50, 100 ) ); - const block_bar = get_result_block( null, get_timeout( 50, 100 ) ); - const block_quu = get_result_block( null, get_timeout( 50, 100 ) ); - - const block = de.array( { - block: [ - de.object( { - block: { - foo: block_foo( { - options: { - required: true, - }, - } ), - bar: block_bar, - }, - - options: { - required: true, - }, - } ), - block_quu, - ], - } ); - - expect.assertions( 3 ); - try { - await de.run( block ); - - } catch ( e ) { - expect( de.is_error( e ) ).toBe( true ); - expect( e.error.id ).toBe( de.ERROR_ID.REQUIRED_BLOCK_FAILED ); - expect( e.error.path ).toBe( '[ 0 ].foo' ); - } - } ); - - describe( 'cancel', () => { - - it( 'cancel object, subblocks cancelled too #1', async () => { - const action_foo_spy = jest.fn(); - const block_foo = get_result_block( null, 150, { - on_cancel: action_foo_spy, - } ); - - const action_bar_spy = jest.fn(); - const block_bar = get_result_block( null, 150, { - on_cancel: action_bar_spy, - } ); - - const block = de.array( { - block: [ - block_foo, - block_bar, - ], - } ); - - const abort_error = de.error( { - id: 'SOME_ERROR', - } ); - try { - const cancel = new de.Cancel(); - setTimeout( () => { - cancel.cancel( abort_error ); - }, 100 ); - await de.run( block, { cancel } ); - - } catch ( e ) { - expect( e ).toBe( abort_error ); - expect( action_foo_spy.mock.calls[ 0 ][ 0 ] ).toBe( abort_error ); - expect( action_bar_spy.mock.calls[ 0 ][ 0 ] ).toBe( abort_error ); - } - } ); - - it( 'cancel object, subblocks cancelled too #2', async () => { - const action_foo_spy = jest.fn(); - const block_foo = get_result_block( null, 50, { - on_cancel: action_foo_spy, - } ); - - const action_bar_spy = jest.fn(); - const block_bar = get_result_block( null, 150, { - on_cancel: action_bar_spy, - } ); - - const block = de.array( { - block: [ - block_foo, - block_bar, - ], - } ); - - const abort_error = de.error( { - id: 'SOME_ERROR', - } ); - try { - const cancel = new de.Cancel(); - setTimeout( () => { - cancel.cancel( abort_error ); - }, 100 ); - await de.run( block, { cancel } ); - - } catch ( e ) { - expect( e ).toBe( abort_error ); - expect( action_foo_spy.mock.calls ).toHaveLength( 0 ); - expect( action_bar_spy.mock.calls[ 0 ][ 0 ] ).toBe( abort_error ); - } - } ); - - it( 'required block failed, other subblocks cancelled', async () => { - const error_foo = de.error( { - id: 'SOME_ERROR', - } ); - const block_foo = get_error_block( error_foo, 50 ); - - const action_bar_spy = jest.fn(); - const block_bar = get_result_block( null, 150, { - on_cancel: action_bar_spy, - } ); - - const block = de.array( { - block: [ - block_foo( { - options: { - required: true, - }, - } ), - block_bar, - ], - } ); - - try { - await de.run( block ); - - } catch ( e ) { - const call_0_0 = action_bar_spy.mock.calls[ 0 ][ 0 ]; - expect( de.is_error( call_0_0 ) ).toBe( true ); - expect( call_0_0.error.id ).toBe( de.ERROR_ID.REQUIRED_BLOCK_FAILED ); - expect( call_0_0.error.reason ).toBe( error_foo ); - } - } ); - - } ); - - describe( 'inheritance', () => { - - it( 'two subblocks', async () => { - const data_foo = { - foo: 42, - }; - const block_foo = get_result_block( data_foo, get_timeout( 50, 100 ) ); - - const data_bar = { - bar: 24, - }; - const block_bar = get_result_block( data_bar, get_timeout( 50, 100 ) ); - - const parent = de.array( { - block: [ - block_foo, - block_bar, - ], - } ); - const child = parent(); - - const result = await de.run( child ); - - expect( result ).toEqual( [ - data_foo, - data_bar, - ] ); - expect( result[ 0 ] ).toBe( data_foo ); - expect( result[ 1 ] ).toBe( data_bar ); - } ); - - } ); - -} ); - diff --git a/tests/cache.test.js b/tests/cache.test.js deleted file mode 100644 index 50355d6..0000000 --- a/tests/cache.test.js +++ /dev/null @@ -1,78 +0,0 @@ -const de = require( '../lib' ); - -const { wait_for_value } = require( './helpers' ); - -describe( 'cache', () => { - - it( 'get', () => { - const cache = new de.Cache(); - - const key = 'KEY'; - - const result = cache.get( { key } ); - expect( result ).toBe( undefined ); - } ); - - it( 'set then get', () => { - const cache = new de.Cache(); - - const key = 'KEY'; - const value = { - foo: 42, - }; - cache.set( { key, value } ); - - const result = cache.get( { key } ); - expect( result ).toBe( value ); - } ); - - it( 'set with max_age #1', async () => { - const cache = new de.Cache(); - - const key = 'KEY'; - const value = { - foo: 42, - }; - const maxage = 100; - cache.set( { key, value, maxage } ); - - await wait_for_value( null, 50 ); - - const result = cache.get( { key } ); - expect( result ).toBe( value ); - } ); - - it( 'set with max_age #2', async () => { - const cache = new de.Cache(); - - const key = 'KEY'; - const value = { - foo: 42, - }; - const maxage = 50; - cache.set( { key, value, maxage } ); - - await wait_for_value( null, 100 ); - - const result = cache.get( { key } ); - expect( result ).toBe( undefined ); - } ); - - it( 'set with max_age #3', async () => { - const cache = new de.Cache(); - - const key = 'KEY'; - const value = { - foo: 42, - }; - const maxage = 0; - cache.set( { key, value, maxage } ); - - await wait_for_value( null, 100 ); - - const result = cache.get( { key } ); - expect( result ).toBe( value ); - } ); - -} ); - diff --git a/tests/cache.test.ts b/tests/cache.test.ts new file mode 100644 index 0000000..e3330c0 --- /dev/null +++ b/tests/cache.test.ts @@ -0,0 +1,78 @@ +import * as de from '../lib'; + +import { waitForValue } from './helpers'; + + +describe('cache', () => { + + it('get', async() => { + const cache = new de.Cache(); + + const key = 'KEY'; + + const result = await cache.get({ key }); + expect(result).toBeUndefined(); + }); + + it('set then get', async() => { + const cache = new de.Cache(); + + const key = 'KEY'; + const value = { + foo: 42, + }; + await cache.set({ key, value }); + + const result = await cache.get({ key }); + expect(result).toBe(value); + }); + + it('set with max_age #1', async() => { + const cache = new de.Cache(); + + const key = 'KEY'; + const value = { + foo: 42, + }; + const maxage = 100; + await cache.set({ key, value, maxage }); + + await waitForValue(null, 50); + + const result = await cache.get({ key }); + expect(result).toBe(value); + }); + + it('set with max_age #2', async() => { + const cache = new de.Cache(); + + const key = 'KEY'; + const value = { + foo: 42, + }; + const maxage = 50; + await cache.set({ key, value, maxage }); + + await waitForValue(null, 100); + + const result = await cache.get({ key }); + expect(result).toBeUndefined(); + }); + + it('set with max_age #3', async() => { + const cache = new de.Cache(); + + const key = 'KEY'; + const value = { + foo: 42, + }; + const maxage = 0; + await cache.set({ key, value, maxage }); + + await waitForValue(null, 100); + + const result = await cache.get({ key }); + expect(result).toBe(value); + }); + +}); diff --git a/tests/cancel.test.js b/tests/cancel.test.js deleted file mode 100644 index e9a133e..0000000 --- a/tests/cancel.test.js +++ /dev/null @@ -1,221 +0,0 @@ -const de = require( '../lib' ); - -// --------------------------------------------------------------------------------------------------------------- // - -describe( 'de.Cancel', () => { - - it( 'cancel', () => { - const cancel = new de.Cancel(); - - const spy = jest.fn(); - cancel.subscribe( spy ); - - const error = de.error( { - id: 'SOME_ERROR', - } ); - cancel.cancel( error ); - - expect( spy.mock.calls[ 0 ][ 0 ] ).toBe( error ); - } ); - - it( 'cancel with plain object', () => { - const cancel = new de.Cancel(); - - const spy = jest.fn(); - cancel.subscribe( spy ); - - const error = { - id: 'SOME_ERROR', - }; - cancel.cancel( error ); - - const reason = spy.mock.calls[ 0 ][ 0 ]; - expect( de.is_error( reason ) ).toBe( true ); - expect( reason.error.id ).toBe( error.id ); - } ); - - it( 'cancel after cancel', () => { - const cancel = new de.Cancel(); - - const spy = jest.fn(); - cancel.subscribe( spy ); - - const error1 = de.error( { - id: 'SOME_ERROR_1', - } ); - cancel.cancel( error1 ); - const error2 = de.error( { - id: 'SOME_ERROR_2', - } ); - cancel.cancel( error2 ); - - expect( spy.mock.calls ).toHaveLength( 1 ); - } ); - - it( 'cancel after close', () => { - const cancel = new de.Cancel(); - - const spy = jest.fn(); - cancel.subscribe( spy ); - - cancel.close(); - - const error = de.error( { - id: 'SOME_ERROR', - } ); - cancel.cancel( error ); - - expect( spy.mock.calls ).toHaveLength( 0 ); - } ); - - it( 'throw_if_cancelled #1', () => { - const cancel = new de.Cancel(); - - const error = de.error( { - id: 'SOME_ERROR', - } ); - cancel.cancel( error ); - - expect.assertions( 1 ); - try { - cancel.throw_if_cancelled(); - - } catch ( e ) { - expect( e ).toBe( error ); - } - } ); - - it( 'throw_if_cancelled #2', () => { - expect( () => { - const cancel = new de.Cancel(); - cancel.throw_if_cancelled(); - } ).not.toThrow(); - } ); - - it( 'subscribe after close', () => { - const cancel = new de.Cancel(); - - cancel.close(); - - const spy = jest.fn(); - cancel.subscribe( spy ); - - expect( spy.mock.calls ).toHaveLength( 0 ); - } ); - - it( 'subscribe after cancel', () => { - const cancel = new de.Cancel(); - - const error = de.error( { - id: 'SOME_ERROR', - } ); - cancel.cancel( error ); - - const spy = jest.fn(); - cancel.subscribe( spy ); - - expect( spy.mock.calls[ 0 ][ 0 ] ).toBe( error ); - } ); - - it( 'get_promise', async () => { - const cancel = new de.Cancel(); - - const promise = cancel.get_promise(); - - const error = de.error( { - id: 'SOME_ERROR', - } ); - setTimeout( () => { - cancel.cancel( error ); - }, 50 ); - - expect.assertions( 1 ); - try { - await promise; - - } catch ( e ) { - expect( e ).toBe( error ); - } - } ); - - it( 'get_promise after cancel', async () => { - const cancel = new de.Cancel(); - - const error = de.error( { - id: 'SOME_ERROR', - } ); - cancel.cancel( error ); - - expect.assertions( 1 ); - try { - await cancel.get_promise(); - - } catch ( e ) { - expect( e ).toBe( error ); - } - } ); - - it( 'child canceled after parent cancel', () => { - const parent_cancel = new de.Cancel(); - const child_cancel = parent_cancel.create(); - - const spy = jest.fn(); - child_cancel.subscribe( spy ); - - const error = de.error( { - id: 'SOME_ERROR', - } ); - parent_cancel.cancel( error ); - - expect( spy.mock.calls[ 0 ][ 0 ] ).toBe( error ); - } ); - - it( 'child closed if parent was closed #1', () => { - const parent_cancel = new de.Cancel(); - parent_cancel.close(); - const child_cancel = parent_cancel.create(); - - const spy = jest.fn(); - child_cancel.subscribe( spy ); - - const error = de.error( { - id: 'SOME_ERROR', - } ); - parent_cancel.cancel( error ); - - expect( spy.mock.calls ).toHaveLength( 0 ); - } ); - - it( 'child closed if parent was closed #2', () => { - const parent_cancel = new de.Cancel(); - parent_cancel.close(); - const child_cancel = parent_cancel.create(); - - const spy = jest.fn(); - child_cancel.subscribe( spy ); - - const error = de.error( { - id: 'SOME_ERROR', - } ); - child_cancel.cancel( error ); - - expect( spy.mock.calls ).toHaveLength( 0 ); - } ); - - it( 'child cancelled if parent was cancelled #1', () => { - const parent_cancel = new de.Cancel(); - const error = de.error( { - id: 'SOME_ERROR', - } ); - parent_cancel.cancel( error ); - - const child_cancel = parent_cancel.create(); - - const spy = jest.fn(); - child_cancel.subscribe( spy ); - - expect( spy.mock.calls[ 0 ][ 0 ] ).toBe( error ); - } ); - -} ); - diff --git a/tests/cancel.test.ts b/tests/cancel.test.ts new file mode 100644 index 0000000..5beb3a6 --- /dev/null +++ b/tests/cancel.test.ts @@ -0,0 +1,202 @@ +import * as de from '../lib'; +// --------------------------------------------------------------------------------------------------------------- // + +describe('de.Cancel', () => { + + it('cancel', () => { + const cancel = new de.Cancel(); + + const spy = jest.fn(); + cancel.subscribe(spy); + + const error = de.error('SOME_ERROR'); + cancel.cancel(error); + + expect(spy.mock.calls[ 0 ][ 0 ]).toBe(error); + }); + + it('cancel with plain object', () => { + const cancel = new de.Cancel(); + + const spy = jest.fn(); + cancel.subscribe(spy); + + const error = 'SOME_ERROR'; + cancel.cancel(error); + + const reason = spy.mock.calls[ 0 ][ 0 ]; + expect(de.isError(reason)).toBe(true); + expect(reason.error.id).toBe(error); + }); + + it('cancel after cancel', () => { + const cancel = new de.Cancel(); + + const spy = jest.fn(); + cancel.subscribe(spy); + + const error1 = de.error('SOME_ERROR_1'); + cancel.cancel(error1); + const error2 = de.error('SOME_ERROR_2'); + cancel.cancel(error2); + + expect(spy.mock.calls).toHaveLength(1); + }); + + it('cancel after close', () => { + const cancel = new de.Cancel(); + + const spy = jest.fn(); + cancel.subscribe(spy); + + cancel.close(); + + const error = de.error('SOME_ERROR'); + cancel.cancel(error); + + expect(spy.mock.calls).toHaveLength(0); + }); + + it('throw_if_cancelled #1', () => { + const cancel = new de.Cancel(); + + const error = de.error('SOME_ERROR'); + cancel.cancel(error); + + expect.assertions(1); + let err; + try { + cancel.throwIfCancelled(); + + } catch (e) { + err = e; + } + + expect(error).toBe(err); + }); + + it('throw_if_cancelled #2', () => { + expect(() => { + const cancel = new de.Cancel(); + cancel.throwIfCancelled(); + }).not.toThrow(); + }); + + it('subscribe after close', () => { + const cancel = new de.Cancel(); + + cancel.close(); + + const spy = jest.fn(); + cancel.subscribe(spy); + + expect(spy.mock.calls).toHaveLength(0); + }); + + it('subscribe after cancel', () => { + const cancel = new de.Cancel(); + + const error = de.error('SOME_ERROR'); + cancel.cancel(error); + + const spy = jest.fn(); + cancel.subscribe(spy); + + expect(spy.mock.calls[ 0 ][ 0 ]).toBe(error); + }); + + it('get_promise', async() => { + const cancel = new de.Cancel(); + + const promise = cancel.getPromise(); + + const error = de.error('SOME_ERROR'); + setTimeout(() => { + cancel.cancel(error); + }, 50); + + expect.assertions(1); + let err; + try { + await promise; + + } catch (e) { + err = e; + } + + expect(err).toBe(error); + }); + + it('get_promise after cancel', async() => { + const cancel = new de.Cancel(); + + const error = de.error('SOME_ERROR'); + cancel.cancel(error); + + expect.assertions(1); + let err; + try { + await cancel.getPromise(); + + } catch (e) { + err = e; + } + + expect(err).toBe(error); + }); + + it('child canceled after parent cancel', () => { + const parentCancel = new de.Cancel(); + const childCancel = parentCancel.create(); + + const spy = jest.fn(); + childCancel.subscribe(spy); + + const error = de.error('SOME_ERROR'); + parentCancel.cancel(error); + + expect(spy.mock.calls[ 0 ][ 0 ]).toBe(error); + }); + + it('child closed if parent was closed #1', () => { + const parentCancel = new de.Cancel(); + parentCancel.close(); + const childCancel = parentCancel.create(); + + const spy = jest.fn(); + childCancel.subscribe(spy); + + const error = de.error('SOME_ERROR'); + parentCancel.cancel(error); + + expect(spy.mock.calls).toHaveLength(0); + }); + + it('child closed if parent was closed #2', () => { + const parentCancel = new de.Cancel(); + parentCancel.close(); + const childCancel = parentCancel.create(); + + const spy = jest.fn(); + childCancel.subscribe(spy); + + const error = de.error('SOME_ERROR'); + childCancel.cancel(error); + + expect(spy.mock.calls).toHaveLength(0); + }); + + it('child cancelled if parent was cancelled #1', () => { + const parentCancel = new de.Cancel(); + const error = de.error('SOME_ERROR'); + parentCancel.cancel(error); + + const childCancel = parentCancel.create(); + + const spy = jest.fn(); + childCancel.subscribe(spy); + + expect(spy.mock.calls[ 0 ][ 0 ]).toBe(error); + }); + +}); diff --git a/tests/descript.test.js b/tests/descript.test.js deleted file mode 100644 index da2a2d0..0000000 --- a/tests/descript.test.js +++ /dev/null @@ -1,36 +0,0 @@ -const de = require( '../lib' ); - -describe( 'descript', () => { - - it( 'de.run( value )', async () => { - const block = { - foo: 42, - }; - - const result = await de.run( block ); - expect( result ).toBe( block ); - } ); - - it.each( [ - [ 'de.func', de.func ], - [ 'de.array', de.array ], - [ 'de.object', de.object ], - [ 'de.pipe', de.pipe ], - [ 'de.first', de.first ], - ] )( '%s without arguments', ( _, factory ) => { - expect.assertions( 2 ); - try { - factory(); - - } catch ( e ) { - expect( de.is_error( e ) ).toBe( true ); - expect( e.error.id ).toBe( de.ERROR_ID.INVALID_BLOCK ); - } - } ); - - it( 'de.http without arguments', () => { - expect( () => de.http() ).not.toThrow(); - } ); - -} ); - diff --git a/tests/descript.test.ts b/tests/descript.test.ts new file mode 100644 index 0000000..c303c24 --- /dev/null +++ b/tests/descript.test.ts @@ -0,0 +1,48 @@ +import * as de from '../lib'; + +describe('descript', () => { + + /*it('de.run( value )', async() => { + const block = { + foo: 42, + }; + + const result = await de.run(block); + expect(result).toBe(block); + });*/ + + const cases: Array<[string, (...args: Array) => any]> = [ + [ 'de.func', de.func ], + [ 'de.array', de.array ], + [ 'de.object', de.object ], + //[ 'de.pipe', de.pipe ], + //[ 'de.first', de.first ], + ]; + + cases.forEach((data) => { + it(`${ data[0] } without arguments`, () => { + const factory = data[1]; + expect.assertions(2); + let err; + try { + factory({ + block: 1, + }); + + } catch (e) { + err = e; + } + + expect(de.isError(err)).toBe(true); + expect(err.error.id).toBe(de.ERROR_ID.INVALID_BLOCK); + }); + }); + + it('de.http without arguments', () => { + expect(() => de.http({ + block: {}, + options: {}, + })).not.toThrow(); + }); + +}); diff --git a/tests/error.test.js b/tests/error.test.js deleted file mode 100644 index 511ded9..0000000 --- a/tests/error.test.js +++ /dev/null @@ -1,78 +0,0 @@ -const fs_ = require( 'fs' ); - -const de = require( '../lib' ); - -describe( 'de.error', () => { - - it( 'from string', () => { - const error_id = 'SOME_ERROR'; - const error = de.error( error_id ); - - expect( error.error.id ).toBe( error_id ); - } ); - - it( 'from ReferenceError', () => { - try { - // eslint-disable-next-line - const a = b; - - } catch ( e ) { - const error = de.error( e ); - - expect( error.error.id ).toBe( 'ReferenceError' ); - } - - } ); - - it( 'from TypeError', () => { - try { - const b = null; - // eslint-disable-next-line no-unused-vars - const a = b.foo; - - } catch ( e ) { - const error = de.error( e ); - - expect( error.error.id ).toBe( 'TypeError' ); - } - - } ); - - // https://github.com/facebook/jest/issues/2549 - // - it( 'from nodejs exception', () => { - const filename = 'some_nonexistance_filename'; - - expect.assertions( 4 ); - try { - fs_.readFileSync( filename, 'utf-8' ); - - } catch ( e ) { - const error = de.error( e ); - - expect( error.error.id ).toBe( 'UNKNOWN_ERROR' ); - expect( error.error.code ).toBe( 'ENOENT' ); - expect( error.error.syscall ).toBe( 'open' ); - // FIXME: Может лучше .toBeDefined() использовать? - expect( error.error.errno ).toBe( -2 ); - } - } ); - - it( 'de.is_error #1', () => { - const error = de.error(); - - expect( de.is_error( error ) ).toBe( true ); - } ); - - it( 'de.is_error #2', () => { - const id = 'SOME_ERROR'; - const error = de.error( { - id: id, - } ); - - expect( de.is_error( error, id ) ).toBe( true ); - expect( de.is_error( error, 'SOME_OTHER_ERROR' ) ).toBe( false ); - } ); - -} ); - diff --git a/tests/error.test.ts b/tests/error.test.ts new file mode 100644 index 0000000..02b9bf3 --- /dev/null +++ b/tests/error.test.ts @@ -0,0 +1,92 @@ +/* eslint-disable no-console */ +/* eslint-disable jest/no-conditional-expect */ + +import fs_ from 'fs'; + +import * as de from '../lib'; + +describe('de.error', () => { + + it('from string', () => { + const errorId = 'SOME_ERROR'; + const error = de.error(errorId); + + expect(error.error.id).toBe(errorId); + }); + + it('from ReferenceError', () => { + expect.assertions(1); + try { + // eslint-disable-next-line + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const a = b; + + } catch (e) { + const error = de.error(e); + console.log(e); + expect(error.error.id).toBe('ReferenceError'); + } + + }); + + it('from TypeError', () => { + expect.assertions(1); + try { + const b = null; + // eslint-disable-next-line no-unused-vars,@typescript-eslint/ban-ts-comment + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const a = b.foo; + + } catch (e) { + const error = de.error(e); + + expect(error.error.id).toBe('TypeError'); + } + + }); + + // https://github.com/facebook/jest/issues/2549 + // + it('from nodejs exception', () => { + const filename = 'some_nonexistance_filename'; + + expect.assertions(4); + try { + fs_.readFileSync(filename, 'utf-8'); + + } catch (e) { + // some bug in jest + // eslint-disable-next-line no-ex-assign + const err = new Error(e); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + Object.keys(e).forEach(key => err[key] = e[key]); + const error = de.error(err); + + expect(error.error.id).toBe('UNKNOWN_ERROR'); + expect(error.error.code).toBe('ENOENT'); + expect(error.error.syscall).toBe('open'); + // FIXME: Может лучше .toBeDefined() использовать? + expect(error.error.errno).toBe(-2); + } + }); + + it('de.is_error #1', () => { + const error = de.error('id'); + + expect(de.isError(error)).toBe(true); + }); + + it('de.is_error #2', () => { + const id = de.ERROR_ID.PARSE_BODY_ERROR; + const error = de.error({ + id: id, + }); + + expect(de.isError(error, id)).toBe(true); + expect(de.isError(error, de.ERROR_ID.INVALID_DEPS_ID)).toBe(false); + }); + +}); diff --git a/tests/expect.js b/tests/expect.ts similarity index 58% rename from tests/expect.js rename to tests/expect.ts index 18c7aab..3274b71 100644 --- a/tests/expect.js +++ b/tests/expect.ts @@ -1,19 +1,20 @@ -const { gunzipSync } = require( 'node:zlib' ); +import { gunzipSync } from 'node:zlib' ; +import { expect } from '@jest/globals'; -expect.extend( { - toBeValidGzip( received ) { +expect.extend({ + toBeValidGzip(received) { // http://www.zlib.org/rfc-gzip.html#header-trailer // 2.3.1. Member header and trailer // These have the fixed values ID1 = 31 (0x1f), ID2 = 139 (0x8b), to identify the file as being in gzip format. return { message: () => `incorrect header check for ${ received }`, - pass: received.slice( 0, 2 ).equals( Buffer.from( '1f8b', 'hex' ) ), + pass: received.slice(0, 2).equals(Buffer.from('1f8b', 'hex')), }; }, - toHaveUngzipValue( received, value ) { + toHaveUngzipValue(received, value) { return { message: () => `expected ${ received } to be ${ value }`, - pass: gunzipSync( received ).toString( 'utf-8' ) === value, + pass: gunzipSync(received).toString('utf-8') === value, }; }, -} ); +}); diff --git a/tests/firstBlock.test.ts b/tests/firstBlock.test.ts new file mode 100644 index 0000000..32d571c --- /dev/null +++ b/tests/firstBlock.test.ts @@ -0,0 +1,108 @@ +/* eslint-disable jest/no-conditional-expect */ + +import * as de from '../lib'; + +describe('de.first', () => { + + it('first block is successful', async() => { + let result1; + const spy1 = jest.fn(() => { + result1 = { + a: 1, + }; + return result1; + }); + const block1 = de.func({ + block: spy1, + }); + + const spy2 = jest.fn(); + const block2 = de.func({ + block: spy2, + }); + + const block = de.first({ + block: [ block1, block2 ], + }); + + const result = await de.run(block); + + expect(result).toBe(result1); + expect(spy1.mock.calls[ 0 ][ 0 ].deps.prev).toEqual([]); + expect(spy2.mock.calls).toHaveLength(0); + }); + + it('first block throws', async() => { + let error1; + const spy1 = jest.fn(() => { + error1 = de.error({ + id: 'ERROR', + }); + throw error1; + }); + const block1 = de.func({ + block: spy1, + }); + + let result2; + const spy2 = jest.fn(() => { + result2 = { + a: 1, + }; + return result2; + }); + const block2 = de.func({ + block: spy2, + }); + + const block = de.first({ + block: [ block1, block2 ], + }); + + const result = await de.run(block); + + expect(result).toBe(result2); + expect(spy2.mock.calls[ 0 ][ 0 ].deps.prev).toHaveLength(1); + expect(spy2.mock.calls[ 0 ][ 0 ].deps.prev[ 0 ]).toBe(error1); + }); + + it('second block throws', async() => { + let error1; + const spy1 = jest.fn(() => { + error1 = de.error({ + id: 'ERROR', + }); + throw error1; + }); + const block1 = de.func({ + block: spy1, + }); + + let error2; + const block2 = de.func({ + block: () => { + error2 = de.error({ + id: 'ANOTHER_ERROR', + }); + throw error2; + }, + }); + + const block = de.first({ + block: [ block1, block2 ], + }); + + expect.assertions(5); + try { + await de.run(block); + + } catch (e) { + expect(de.isError(e)).toBe(true); + expect(e.error.id).toBe(de.ERROR_ID.ALL_BLOCKS_FAILED); + expect(e.error.reason).toHaveLength(2); + expect(e.error.reason[ 0 ]).toBe(error1); + expect(e.error.reason[ 1 ]).toBe(error2); + } + }); + +}); diff --git a/tests/first_block.test.js b/tests/first_block.test.js deleted file mode 100644 index 1d27f61..0000000 --- a/tests/first_block.test.js +++ /dev/null @@ -1,107 +0,0 @@ -const de = require( '../lib' ); - -describe( 'de.first', () => { - - it( 'first block is successful', async () => { - let result_1; - const spy_1 = jest.fn( () => { - result_1 = { - a: 1, - }; - return result_1; - } ); - const block_1 = de.func( { - block: spy_1, - } ); - - const spy_2 = jest.fn(); - const block_2 = de.func( { - block: spy_2, - } ); - - const block = de.first( { - block: [ block_1, block_2 ], - } ); - - const result = await de.run( block ); - - expect( result ).toBe( result_1 ); - expect( spy_1.mock.calls[ 0 ][ 0 ].deps.prev ).toEqual( [] ); - expect( spy_2.mock.calls ).toHaveLength( 0 ); - } ); - - it( 'first block throws', async () => { - let error_1; - const spy_1 = jest.fn( () => { - error_1 = de.error( { - id: 'ERROR', - } ); - throw error_1; - } ); - const block_1 = de.func( { - block: spy_1, - } ); - - let result_2; - const spy_2 = jest.fn( () => { - result_2 = { - a: 1, - }; - return result_2; - } ); - const block_2 = de.func( { - block: spy_2, - } ); - - const block = de.first( { - block: [ block_1, block_2 ], - } ); - - const result = await de.run( block ); - - expect( result ).toBe( result_2 ); - expect( spy_2.mock.calls[ 0 ][ 0 ].deps.prev ).toHaveLength( 1 ); - expect( spy_2.mock.calls[ 0 ][ 0 ].deps.prev[ 0 ] ).toBe( error_1 ); - } ); - - it( 'second block throws', async () => { - let error_1; - const spy_1 = jest.fn( () => { - error_1 = de.error( { - id: 'ERROR', - } ); - throw error_1; - } ); - const block_1 = de.func( { - block: spy_1, - } ); - - let error_2; - const block_2 = de.func( { - block: () => { - error_2 = de.error( { - id: 'ANOTHER_ERROR', - } ); - throw error_2; - }, - } ); - - const block = de.first( { - block: [ block_1, block_2 ], - } ); - - expect.assertions( 5 ); - try { - await de.run( block ); - - } catch ( e ) { - expect( de.is_error( e ) ).toBe( true ); - expect( e.error.id ).toBe( de.ERROR_ID.ALL_BLOCKS_FAILED ); - expect( e.error.reason ).toHaveLength( 2 ); - expect( e.error.reason[ 0 ] ).toBe( error_1 ); - expect( e.error.reason[ 1 ] ).toBe( error_2 ); - } - } ); - -} ); - diff --git a/tests/functionBlock.test.ts b/tests/functionBlock.test.ts new file mode 100644 index 0000000..6a16baa --- /dev/null +++ b/tests/functionBlock.test.ts @@ -0,0 +1,130 @@ +import { getErrorBlock, getResultBlock, waitForValue } from './helpers'; + +import * as de from '../lib'; +// --------------------------------------------------------------------------------------------------------------- // + +describe('de.func', () => { + + it('resolves with value', async() => { + const data = { + foo: 42, + }; + + const block = getResultBlock(data, 50); + + const result = await de.run(block); + + expect(result).toBe(data); + }); + + it('resolves with promise', async() => { + const data = { + foo: 42, + }; + + const block = de.func({ + block: function() { + return waitForValue(data, 50); + }, + }); + + const result = await de.run(block); + + expect(result).toBe(data); + }); + + it('resolves with block', async() => { + const data = { + foo: 42, + }; + + const block = getResultBlock(data, 50); + + const result = await de.run(block); + + expect(result).toBe(data); + }); + + // Самый простой способ вычислить факториал! + it('recursion', async() => { + type Params = { + n: number; + } + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const block = de.func({ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + block: ({ params }: { params: Params }) => { + if (params.n === 1) { + return 1; + + } else { + return block.extend({ + options: { + params: ({ params }: { params: Params }) => { + return { + n: params.n - 1, + }; + }, + after: ({ result, params }: { result: number; params: Params }) => { + return (params.n + 1) * result; + }, + }, + }); + } + }, + }); + + const params = { + n: 5, + }; + + const result = await de.run(block, { params }); + expect(result).toBe(120); + }); + + it('rejects with de.error', async() => { + const error = de.error({ + id: 'SOME_ERROR', + }); + + const block = getErrorBlock(error, 50); + + expect.assertions(1); + let err; + try { + await de.run(block); + + } catch (e) { + err = e; + } + + expect(err).toBe(error); + + }); + + it('cancellable', async() => { + const block = getResultBlock(null, 100); + const cancel = new de.Cancel(); + + const cancelReason = de.error({ + id: 'SOME_REASON', + }); + setTimeout(() => { + cancel.cancel(cancelReason); + }, 50); + + expect.assertions(1); + let e; + try { + await de.run(block, { cancel }); + + } catch (error) { + e = error; + } + + expect(e).toBe(cancelReason); + }); + +}); diff --git a/tests/function_block.test.js b/tests/function_block.test.js deleted file mode 100644 index 3eef9c1..0000000 --- a/tests/function_block.test.js +++ /dev/null @@ -1,124 +0,0 @@ -const de = require( '../lib' ); - -const { - wait_for_value, - // wait_for_error, - get_result_block, - get_error_block, -} = require( './helpers' ); - -// --------------------------------------------------------------------------------------------------------------- // - -describe( 'de.func', () => { - - it( 'resolves with value', async () => { - const data = { - foo: 42, - }; - - const block = get_result_block( data, 50 ); - - const result = await de.run( block ); - - expect( result ).toBe( data ); - } ); - - it( 'resolves with promise', async () => { - const data = { - foo: 42, - }; - - const block = de.func( { - block: function() { - return wait_for_value( data, 50 ); - }, - } ); - - const result = await de.run( block ); - - expect( result ).toBe( data ); - } ); - - it( 'resolves with block', async () => { - const data = { - foo: 42, - }; - - const block1 = get_result_block( data, 50 ); - const block2 = get_result_block( block1, 50 ); - - const result = await de.run( block2 ); - - expect( result ).toBe( data ); - } ); - - // Самый простой способ вычислить факториал! - it( 'recursion', async () => { - const block = de.func( { - block: ( { params } ) => { - if ( params.n === 1 ) { - return 1; - - } else { - return block( { - options: { - params: ( { params } ) => { - return { - n: params.n - 1, - }; - }, - after: ( { result, params } ) => { - return ( params.n + 1 ) * result; - }, - }, - } ); - } - }, - } ); - - const params = { - n: 5, - }; - const result = await de.run( block, { params } ); - expect( result ).toBe( 120 ); - } ); - - it( 'rejects with de.error', async () => { - const error = de.error( { - id: 'SOME_ERROR', - } ); - - const block = get_error_block( error, 50 ); - - expect.assertions( 1 ); - try { - await de.run( block ); - - } catch ( e ) { - expect( e ).toBe( error ); - } - - } ); - - it( 'cancellable', async () => { - const block = get_result_block( null, 100 ); - const cancel = new de.Cancel(); - - const cancel_reason = de.error( { - id: 'SOME_REASON', - } ); - setTimeout( () => { - cancel.cancel( cancel_reason ); - }, 50 ); - - expect.assertions( 1 ); - try { - await de.run( block, { cancel } ); - - } catch ( error ) { - expect( error ).toBe( cancel_reason ); - } - } ); - -} ); - diff --git a/tests/helpers.js b/tests/helpers.js deleted file mode 100644 index a9238ba..0000000 --- a/tests/helpers.js +++ /dev/null @@ -1,99 +0,0 @@ -const de = require( '../lib' ); - -// --------------------------------------------------------------------------------------------------------------- // - -let PATH_INDEX = 1; -function get_path() { - return `/test/${ PATH_INDEX++ }/`; -} - -// --------------------------------------------------------------------------------------------------------------- // - -function wait_for_value( value, timeout ) { - if ( timeout > 0 ) { - return new Promise( ( resolve ) => { - setTimeout( () => { - resolve( value ); - }, timeout ); - } ); - } - - return Promise.resolve( value ); -} - -function wait_for_error( error, timeout ) { - if ( timeout > 0 ) { - return new Promise( ( resolve, reject ) => { - setTimeout( () => { - reject( error ); - }, timeout ); - } ); - } - - return Promise.reject( error ); -} - -function get_result_block( value, timeout = 0, options = {} ) { - return de.func( { - block: async function( args ) { - const { block_cancel } = args; - if ( options.on_cancel ) { - block_cancel.subscribe( options.on_cancel ); - } - - await Promise.race( [ - wait_for_value( null, timeout ), - block_cancel.get_promise(), - ] ); - - if ( !de.is_block( value ) && ( typeof value === 'function' ) ) { - return value( args ); - } - - return value; - }, - } ); -} - -function get_error_block( error, timeout ) { - if ( !de.is_block( error ) && ( typeof error === 'function' ) ) { - return de.func( { - block: async function() { - await wait_for_value( null, timeout ); - - throw error(); - }, - } ); - } - - return de.func( { - block: function() { - return wait_for_error( error, timeout ); - }, - } ); -} - -function get_timeout( from, to ) { - return Math.round( from + ( Math.random() * ( to - from ) ) ); -} - -function set_timeout( callback, timeout ) { - return new Promise( ( resolve ) => { - setTimeout( () => { - resolve( callback() ); - }, timeout ); - } ); -} - -// --------------------------------------------------------------------------------------------------------------- // - -module.exports = { - get_path, - get_timeout, - wait_for_value, - wait_for_error, - get_result_block, - get_error_block, - set_timeout, -}; - diff --git a/tests/helpers.ts b/tests/helpers.ts new file mode 100644 index 0000000..8f44ed3 --- /dev/null +++ b/tests/helpers.ts @@ -0,0 +1,105 @@ +import * as de from '../lib'; +import type { SubscribeCallback } from '../lib/cancel'; + +// --------------------------------------------------------------------------------------------------------------- // + +let PATH_INDEX = 1; +function getPath() { + return `/test/${ PATH_INDEX++ }/`; +} + +// --------------------------------------------------------------------------------------------------------------- // + +function waitForValue(value: V, timeout: number) { + if (timeout > 0) { + return new Promise((resolve) => { + global.setTimeout(() => { + resolve(value); + }, timeout); + }); + } + + return Promise.resolve(value); +} + +function waitForError(error: ErrorOrBlock, timeout: number) { + if (timeout > 0) { + return new Promise((resolve, reject) => { + global.setTimeout(() => { + reject(error); + }, timeout); + }); + } + + return Promise.reject(error); +} + +type Options = { + onCancel?: SubscribeCallback; +} + +function getResultBlock< + Value extends(unknown | (() => unknown)), +>(value?: Value, timeout = 0, options: Options = {}) { + return de.func({ + block: async function(args): Promise { + const { blockCancel } = args; + if (options.onCancel) { + blockCancel.subscribe(options.onCancel); + } + + await Promise.race([ + waitForValue(null, timeout), + blockCancel.getPromise(), + ]); + + if (!de.isBlock(value) && (typeof value === 'function')) { + return value(args); + } + + return value as Value; + }, + }); +} + +function getErrorBlock(error: ErrorOrBlock, timeout = 0) { + if (!de.isBlock(error) && (typeof error === 'function')) { + return de.func({ + block: async function(): Promise { + await waitForValue(null, timeout); + + throw error(); + }, + }); + } + + return de.func({ + block: function() { + return waitForError(error, timeout); + }, + }); +} + +function getTimeout(from: number, to: number) { + return Math.round(from + (Math.random() * (to - from))); +} + +function setTimeout(callback: () => unknown, timeout: number) { + return new Promise((resolve) => { + global.setTimeout(() => { + resolve(callback()); + }, timeout); + }); +} + +// --------------------------------------------------------------------------------------------------------------- // + +export { + getPath, + getTimeout, + waitForValue, + waitForError, + getResultBlock, + getErrorBlock, + setTimeout, +}; diff --git a/tests/httpBlock.test.ts b/tests/httpBlock.test.ts new file mode 100644 index 0000000..4a9c58b --- /dev/null +++ b/tests/httpBlock.test.ts @@ -0,0 +1,1465 @@ +import http_ from 'http'; + +import url_ from 'url'; + +import qs_ from 'querystring'; + +import * as de from '../lib'; + +import Server from './server'; + +import { getPath, getResultBlock } from './helpers'; +import type { ServerResponse, ClientRequest } from 'node:http'; + +import strip_null_and_undefined_values from '../lib/stripNullAndUndefinedValues'; +import type { DescriptBlockOptions, DescriptHttpBlockResult } from '../lib/types'; +import type { DescriptHttpBlockDescription } from '../lib/httpBlock'; +import type { DescriptBlockId } from '../lib/depsDomain'; +// --------------------------------------------------------------------------------------------------------------- // + +describe('http', < + Context, + ParamsOut, + HTTPResult, +>() => { + + const PORT = 10000; + + const baseBlock = < + Context, + ParamsOut, + BlockResult extends DescriptHttpBlockResult, + HTTPResult, + + BeforeResultOut = undefined, + AfterResultOut = undefined, + ErrorResultOut = undefined, + Params = ParamsOut + >({ block, options }: { + block?: DescriptHttpBlockDescription; + options?: DescriptBlockOptions; + }) => de.http({ + block: { + protocol: 'http:', + hostname: '127.0.0.1', + port: PORT, + ...block, + }, + options: { + logger: new de.Logger({}), + ...options, + }, + }); + + const fake = new Server({ + module: http_, + listen_options: { + port: PORT, + }, + }); + + beforeAll(() => fake.start()); + afterAll(() => fake.stop()); + + describe('basic block properties', () => { + const path = getPath(); + + type P = 'method' | 'protocol' | 'port' | 'hostname' | 'pathname' | 'maxRetries' | 'timeout' | 'headers' | 'query' | 'body' + + const PROPS: Array<[ P, DescriptHttpBlockDescription[P]]> = [ + [ 'method', 'POST' ], + [ 'protocol', 'http:' ], + [ 'port', PORT ], + [ 'hostname', '127.0.0.1' ], + [ 'pathname', path ], + [ 'maxRetries', 0 ], + [ 'timeout', 100 ], + [ 'headers', {} ], + [ 'query', {} ], + [ 'body', {} ], + ]; + + fake.add(path); + + it.each(PROPS)('%j is a function and it gets { params, context }', async(name, value) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const spy = jest.fn((...args: any) => value); + + const block = baseBlock({ + block: { + pathname: path, + // Чтобы body отработал. + method: 'POST', + [ name ]: spy, + }, + }); + + const params = { + foo: 42, + }; + const context = { + req: true, + res: true, + }; + await de.run(block, { params, context }); + + const call = spy.mock.calls[ 0 ][ 0 ]; + expect(call.params).toBe(params); + expect(call.context).toBe(context); + }); + + it.each(PROPS)('%j is a function and it gets { deps }', async(name, value) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const spy = jest.fn((...args: any) => value); + + let fooResult; + let id: DescriptBlockId; + const block = de.func({ + block: ({ generateId }) => { + id = generateId(); + + return de.object({ + block: { + foo: getResultBlock(() => { + fooResult = { + foo: 42, + }; + return fooResult; + }).extend({ + options: { + id: id, + }, + }), + + bar: baseBlock({ + block: { + pathname: path, + method: 'POST', + [ name ]: spy, + }, + + options: { + deps: id, + }, + }), + + debor: de.func({ + block: () => ({ fooResult: 1 }), + }), + }, + }); + }, + }); + + const result = await de.run(block); + + result.bar; + + const call = spy.mock.calls[ 0 ][ 0 ]; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(call.deps[ id ]).toBe(fooResult); + }); + + }); + + /* + it( 'path is a string', async () => { + const path = getPath(); + + const CONTENT = 'Привет!'; + + fake.add( path, { + statusCode: 200, + content: CONTENT, + } ); + + const block = baseBlock( { + block: { + pathname: path, + }, + } ); + + const result = await de.run( block ); + + expect( result.statusCode ).toBe( 200 ); + expect( result.result ).toBe( CONTENT ); + } ); + + it( 'path is a function', async () => { + const path = getPath(); + + const CONTENT = 'Привет!'; + + fake.add( path, { + statusCode: 200, + content: CONTENT, + } ); + + const block = baseBlock( { + block: { + pathname: () => path, + }, + } ); + + const result = await de.run( block ); + + expect( result.statusCode ).toBe( 200 ); + expect( result.result ).toBe( CONTENT ); + } ); + */ + + describe('headers', () => { + + it('is a function', async() => { + const path = getPath(); + + const spy = jest.fn((req, res) => res.end()); + + fake.add(path, spy); + + let blockHeaders; + const block = baseBlock({ + block: { + pathname: path, + headers: () => { + blockHeaders = { + 'x-a': 'a', + 'X-B': 'B', + }; + return blockHeaders; + }, + }, + }); + + await de.run(block); + + const headers = spy.mock.calls[ 0 ][ 0 ].headers; + expect(headers[ 'x-a' ]).toBe('a'); + expect(headers[ 'x-b' ]).toBe('B'); + }); + + it('is an object', async() => { + const path = getPath(); + + const spy = jest.fn((req, res) => res.end()); + + fake.add(path, spy); + + const headerSpy = jest.fn(() => 'c'); + const block = baseBlock({ + block: { + pathname: path, + headers: { + 'x-a': 'a', + 'X-B': 'B', + 'x-c': headerSpy, + }, + }, + }); + + await de.run(block); + + const headers = spy.mock.calls[ 0 ][ 0 ].headers; + expect(headers[ 'x-a' ]).toBe('a'); + expect(headers[ 'x-b' ]).toBe('B'); + expect(headers[ 'x-c' ]).toBe('c'); + }); + + it('is an object and value function gets { params, context }', async() => { + const path = getPath(); + + fake.add(path); + + const spy = jest.fn(() => 'a'); + const block = baseBlock({ + block: { + pathname: path, + headers: { + 'x-a': spy, + }, + }, + }); + + const params = { + foo: 42, + }; + const context = { + context: true, + }; + await de.run(block, { params, context }); + + const call = spy.mock.calls[ 0 ][ 0 ]; + expect(call.params).toBe(params); + expect(call.context).toBe(context); + }); + + describe('inheritance', () => { + + it('child block is undefined', async() => { + const path = getPath(); + + const RESPONSE = 'Привет!'; + fake.add(path, (req: ClientRequest, res: ServerResponse) => res.end(RESPONSE)); + + const parent = baseBlock({ + block: { + pathname: path, + }, + }); + const child = parent.extend({ options: { + after: ({ result }: { result: DescriptHttpBlockResult }) => result, + } }); + + const result = await de.run(child); + + expect(result.result).toBe(RESPONSE); + }); + + it('child is a function and it gets { headers }', async() => { + const path = getPath(); + + fake.add(path); + + const spy = jest.fn(); + + let parentHeaders; + const parent = baseBlock({ + block: { + pathname: path, + headers: () => { + parentHeaders = { + 'x-a': 'a', + }; + return parentHeaders; + }, + }, + }); + const child = parent.extend({ + block: { + headers: spy, + }, + }); + + await de.run(child); + + const call = spy.mock.calls[ 0 ][ 0 ]; + expect(call.headers).toBe(parentHeaders); + }); + + it('child is an object', async() => { + const path = getPath(); + + const spy = jest.fn((req, res) => res.end()); + fake.add(path, spy); + + const headerSpy = jest.fn(() => 'b'); + + let parentHeaders; + const parent = baseBlock({ + block: { + pathname: path, + headers: () => { + parentHeaders = { + 'x-a': 'a', + }; + return parentHeaders; + }, + }, + }); + const child = parent.extend({ + block: { + headers: { + 'x-b': headerSpy, + 'X-C': 'C', + }, + }, + }); + + await de.run(child); + + const headerCall = headerSpy.mock.calls[ 0 ][ 0 ]; + expect(headerCall.headers).toBe(parentHeaders); + + const call = spy.mock.calls[ 0 ][ 0 ]; + expect(call.headers[ 'x-a' ]).toBeUndefined(); + expect(call.headers[ 'x-b' ]).toBe('b'); + expect(call.headers[ 'x-c' ]).toBe('C'); + }); + + }); + + }); + + describe('query', () => { + + it('is a function', async() => { + const path = getPath(); + const spy = jest.fn((req, res) => res.end()); + fake.add(path, spy); + + const block = baseBlock({ + block: { + pathname: path, + query: () => { + return { + b: 'b', + a: 'a', + c: undefined, + d: '', + e: null, + f: 0, + g: false, + }; + }, + }, + }); + + await de.run(block); + + const req = spy.mock.calls[ 0 ][ 0 ]; + expect(url_.parse(req.url, true).query).toEqual({ + b: 'b', + a: 'a', + d: '', + f: '0', + g: 'false', + }); + }); + + it('is an object and value is null', async() => { + const path = getPath(); + const spy = jest.fn((req, res) => res.end()); + fake.add(path, spy); + + const block = baseBlock({ + block: { + pathname: path, + query: { + a: null, + b: null, + c: null, + d: null, + e: null, + f: null, + }, + }, + }); + + const params = { + a: null, + b: undefined, + c: 0, + d: '', + e: false, + f: 'foo', + g: 'bar', + }; + await de.run(block, { params }); + + const req = spy.mock.calls[ 0 ][ 0 ]; + const query = url_.parse(req.url, true).query; + // NOTE: В ноде query сделано через Object.create( null ), + // так что toStrictEqual работает неправильно с ним. + expect({ ...query }).toStrictEqual({ + a: '', + c: '0', + d: '', + e: 'false', + f: 'foo', + }); + }); + + it('is an object and value is undefined', async() => { + const path = getPath(); + const spy = jest.fn((req, res) => res.end()); + fake.add(path, spy); + + const block = baseBlock({ + block: { + pathname: path, + query: { + a: undefined, + b: undefined, + c: undefined, + d: undefined, + e: undefined, + f: undefined, + }, + }, + }); + + const params = { + a: null, + b: undefined, + c: 0, + d: '', + e: false, + f: 'foo', + g: 'bar', + }; + await de.run(block, { params }); + + const req = spy.mock.calls[ 0 ][ 0 ]; + const query = url_.parse(req.url, true).query; + expect({ ...query }).toStrictEqual({}); + }); + + it('is an object and value is not null or undefined #1', async() => { + const path = getPath(); + const spy = jest.fn((req, res) => res.end()); + fake.add(path, spy); + + const block = baseBlock({ + block: { + pathname: path, + query: { + a: 0, + b: '', + c: false, + d: 42, + e: 'foo', + }, + }, + }); + + const params = {}; + await de.run(block, { params }); + + const req = spy.mock.calls[ 0 ][ 0 ]; + const query = url_.parse(req.url, true).query; + expect({ ...query }).toStrictEqual({ + a: '0', + b: '', + c: 'false', + d: '42', + e: 'foo', + }); + }); + + it('is an object and value is not null or undefined #2', async() => { + const path = getPath(); + const spy = jest.fn((req, res) => res.end()); + fake.add(path, spy); + + const block = baseBlock({ + block: { + pathname: path, + query: { + a: 'foo', + b: 'foo', + c: 'foo', + d: 'foo', + e: 'foo', + }, + }, + }); + + const params = { + a: 0, + b: '', + c: false, + d: null, + e: undefined, + }; + await de.run(block, { params }); + + const req = spy.mock.calls[ 0 ][ 0 ]; + const query = url_.parse(req.url, true).query; + expect({ ...query }).toStrictEqual({ + a: '0', + b: '', + c: 'false', + d: '', + e: 'foo', + }); + }); + + it('is an object and value function gets { params, context }', async() => { + const path = getPath(); + fake.add(path); + + const spy = jest.fn(); + const block = baseBlock({ + block: { + pathname: path, + query: spy, + }, + }); + + const params = { + foo: 42, + }; + const context = { + req: true, + res: true, + }; + await de.run(block, { params, context }); + + const call = spy.mock.calls[ 0 ][ 0 ]; + expect(call.params).toBe(params); + expect(call.context).toBe(context); + }); + + it('is an array of object and function', async() => { + const path = getPath(); + const spy = jest.fn((req, res) => res.end()); + fake.add(path, spy); + + const params = { + foo: 'foo', + bar: 'bar', + quu: 'quu', + }; + + const block = baseBlock({ + block: { + pathname: path, + query: [ + { + foo: null, + }, + ({ query, params }) => { + return { + ...query, + bar: params.bar, + }; + }, + ], + }, + options: { + params: ({ params: p }: { params: typeof params }) => p, + }, + }); + + + await de.run(block, { params }); + + const req = spy.mock.calls[ 0 ][ 0 ]; + const query = url_.parse(req.url, true).query; + expect({ ...query }).toStrictEqual({ + foo: 'foo', + bar: 'bar', + }); + }); + + describe('inheritance', () => { + + it('child is a function and it gets { query }', async() => { + const path = getPath(); + fake.add(path); + + let parentQuery; + const parent = baseBlock({ + block: { + pathname: path, + query: () => { + parentQuery = { + a: 'a', + b: undefined, + c: null, + d: 0, + e: '', + f: false, + }; + return parentQuery; + }, + }, + }); + + const spy = jest.fn(); + const child = parent.extend({ + block: { + query: spy, + }, + }); + + await de.run(child); + + const call = spy.mock.calls[ 0 ][ 0 ]; + expect(call.query).toStrictEqual(strip_null_and_undefined_values(parentQuery)); + }); + + it('child is an object and value function gets { query }', async() => { + const path = getPath(); + fake.add(path); + + let parentQuery; + const parent = baseBlock({ + block: { + pathname: path, + query: () => { + parentQuery = { + a: 'a', + b: undefined, + c: null, + d: 0, + e: '', + f: false, + }; + return parentQuery; + }, + }, + }); + + const spy = jest.fn(); + const child = parent.extend({ + block: { + query: { + bar: spy, + }, + }, + }); + + await de.run(child); + + const call = spy.mock.calls[ 0 ][ 0 ]; + expect(call.query).toStrictEqual(strip_null_and_undefined_values(parentQuery)); + }); + + }); + + }); + + describe('body', () => { + + it('no body', async() => { + const path = getPath(); + + const spy = jest.fn((req, res) => res.end()); + + fake.add(path, spy); + + const block = baseBlock({ + block: { + pathname: path, + method: 'POST', + }, + }); + + await de.run(block); + + const body = spy.mock.calls[ 0 ][ 2 ]; + expect(body).toBeNull(); + }); + + it('is a string', async() => { + const path = getPath(); + + const spy = jest.fn((req, res) => res.end()); + + fake.add(path, spy); + + const BODY = 'Привет!'; + const block = baseBlock({ + block: { + pathname: path, + method: 'POST', + body: BODY, + }, + }); + + await de.run(block); + + const body = spy.mock.calls[ 0 ][ 2 ]; + expect(body.toString()).toBe(BODY); + }); + + it('is a number', async() => { + const path = getPath(); + + const spy = jest.fn((req, res) => res.end()); + + fake.add(path, spy); + + const BODY = 42; + const block = baseBlock({ + block: { + pathname: path, + method: 'POST', + body: BODY, + }, + }); + + await de.run(block); + + const body = spy.mock.calls[ 0 ][ 2 ]; + expect(body.toString()).toBe(String(BODY)); + }); + + it('is a Buffer', async() => { + const path = getPath(); + + const spy = jest.fn((req, res) => res.end()); + + fake.add(path, spy); + + const BODY = Buffer.from('Привет!'); + const block = baseBlock({ + block: { + pathname: path, + method: 'POST', + body: BODY, + }, + }); + + await de.run(block); + + const body = spy.mock.calls[ 0 ][ 2 ]; + expect(body.toString()).toBe(BODY.toString()); + }); + + it('is a function returning string', async() => { + const path = getPath(); + + const spy = jest.fn((req, res) => res.end()); + + fake.add(path, spy); + + const BODY = 'Привет!'; + const block = baseBlock({ + block: { + pathname: path, + method: 'POST', + body: () => BODY, + }, + }); + + await de.run(block); + + const body = spy.mock.calls[ 0 ][ 2 ]; + expect(body.toString()).toBe(BODY); + }); + + it('is a function returning Buffer', async() => { + const path = getPath(); + + const spy = jest.fn((req, res) => res.end()); + + fake.add(path, spy); + + const BODY = Buffer.from('Привет!'); + const block = baseBlock({ + block: { + pathname: path, + method: 'POST', + body: () => BODY, + }, + }); + + await de.run(block); + + const body = spy.mock.calls[ 0 ][ 2 ]; + expect(body.toString()).toBe(BODY.toString()); + }); + + it('is an function returning object', async() => { + const path = getPath(); + + const spy = jest.fn((req, res) => res.end()); + + fake.add(path, spy); + + const BODY = { + a: 'Привет!', + }; + const block = baseBlock({ + block: { + pathname: path, + method: 'POST', + body: () => BODY, + }, + }); + + await de.run(block); + + const body = spy.mock.calls[ 0 ][ 2 ]; + expect(body.toString()).toBe(qs_.stringify(BODY)); + }); + + it('with body_compress', async() => { + const path = getPath(); + + const spy = jest.fn((req, res) => res.end()); + + fake.add(path, spy); + + const block = baseBlock({ + block: { + pathname: path, + method: 'POST', + body: 'Привет!', + bodyCompress: {}, + }, + }); + + await de.run(block); + + expect(spy).toHaveBeenCalledTimes(1); + const body = spy.mock.calls[ 0 ][ 2 ]; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(body).toBeValidGzip(); + expect(body).toHaveLength(34); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(body).toHaveUngzipValue('Привет!'); + }); + }); + + describe('parse response', () => { + + it('text/plain', async() => { + const path = getPath(); + + const RESPONSE = Buffer.from('Привет!'); + fake.add(path, (req: ClientRequest, res: ServerResponse) => { + res.setHeader('content-length', Buffer.byteLength(RESPONSE)); + res.setHeader('content-type', 'text/plain'); + res.end(RESPONSE); + }); + + const block = baseBlock({ + block: { + pathname: path, + }, + options: { + after: ({ result }: { result: DescriptHttpBlockResult }) => result, + }, + }); + + const result = await de.run(block); + + expect(result.result.toString()).toBe(RESPONSE.toString()); + }); + + it('application/json #1', async() => { + const path = getPath(); + + const RESPONSE = { + text: 'Привет!', + }; + fake.add(path, (req: ClientRequest, res: ServerResponse) => { + const buffer = Buffer.from(JSON.stringify(RESPONSE)); + res.setHeader('content-length', Buffer.byteLength(buffer)); + res.setHeader('content-type', 'application/json'); + res.end(buffer); + }); + + const block = baseBlock({ + block: { + pathname: path, + }, + options: { + after: ({ result }: { result: DescriptHttpBlockResult }) => result, + }, + }); + + const result = await de.run(block); + + expect(result.result).toEqual(RESPONSE); + }); + + it('application/json #2', async() => { + const path = getPath(); + + const RESPONSE = { + text: 'Привет!', + }; + fake.add(path, (req: ClientRequest, res: ServerResponse) => { + const buffer = Buffer.from(JSON.stringify(RESPONSE)); + res.setHeader('content-length', Buffer.byteLength(buffer)); + res.setHeader('content-type', 'application/json; charset=utf-8'); + res.end(buffer); + }); + + const block = baseBlock({ + block: { + pathname: path, + }, + options: { + after: ({ result }: { result: DescriptHttpBlockResult }) => result, + }, + }); + + const result = await de.run(block); + + expect(result.result).toEqual(RESPONSE); + }); + + it('text/plain, is_json: true', async() => { + const path = getPath(); + + const RESPONSE = { + text: 'Привет!', + }; + fake.add(path, (req: ClientRequest, res: ServerResponse) => { + const buffer = Buffer.from(JSON.stringify(RESPONSE)); + res.setHeader('content-length', Buffer.byteLength(buffer)); + res.setHeader('content-type', 'text/plain'); + res.end(buffer); + }); + + const block = baseBlock({ + block: { + pathname: path, + isJson: true, + }, + //TODO без этого бы прокидывать тип + options: { + after: ({ result }: { result: DescriptHttpBlockResult }) => result, + }, + }); + + const result = await de.run(block); + + expect(result.result).toEqual(RESPONSE); + }); + + it('invalid json in response, application/json', async() => { + const path = getPath(); + + const RESPONSE = Buffer.from('Привет!'); + fake.add(path, (req: ClientRequest, res: ServerResponse) => { + res.setHeader('content-length', Buffer.byteLength(RESPONSE)); + res.setHeader('content-type', 'application/json'); + res.end(RESPONSE); + }); + + const block = baseBlock({ + block: { + pathname: path, + }, + }); + + expect.assertions(2); + let e; + try { + await de.run(block); + + } catch (error) { + e = error; + } + + expect(de.isError(e)).toBe(true); + expect(e.error.id).toEqual(de.ERROR_ID.INVALID_JSON); + }); + + it('invalid json in response, is_json: true', async() => { + const path = getPath(); + + const RESPONSE = Buffer.from('Привет!'); + fake.add(path, (req: ClientRequest, res: ServerResponse) => { + res.setHeader('content-length', Buffer.byteLength(RESPONSE)); + res.setHeader('content-type', 'text/plain'); + res.end(RESPONSE); + }); + + const block = baseBlock({ + block: { + pathname: path, + isJson: true, + }, + }); + + expect.assertions(2); + let e; + try { + await de.run(block); + + } catch (error) { + e = error; + } + + expect(de.isError(e)).toBe(true); + expect(e.error.id).toEqual(de.ERROR_ID.INVALID_JSON); + }); + + describe('empty body', () => { + it('text/plain', async() => { + const path = getPath(); + + const RESPONSE = Buffer.from([]); + fake.add(path, (req: ClientRequest, res: ServerResponse) => { + res.setHeader('content-length', Buffer.byteLength(RESPONSE)); + res.setHeader('content-type', 'text/plain'); + res.end(RESPONSE); + }); + + const block = baseBlock({ + block: { + pathname: path, + }, + + options: { + after: ({ result }: { result: DescriptHttpBlockResult }) => result, + }, + }); + + const result = await de.run(block); + + expect(result.result).toBeNull(); + }); + + it('application/json', async() => { + const path = getPath(); + + const RESPONSE = Buffer.from([]); + fake.add(path, (req: ClientRequest, res: ServerResponse) => { + res.setHeader('content-length', Buffer.byteLength(RESPONSE)); + res.setHeader('content-type', 'application/json'); + res.end(RESPONSE); + }); + + const block = baseBlock({ + block: { + pathname: path, + }, + + options: { + after: ({ result }: { result: DescriptHttpBlockResult }) => result, + }, + }); + + const result = await de.run(block); + + expect(result.result).toBeNull(); + }); + }); + + }); + + describe('parse error', () => { + + it('no body', async() => { + const path = getPath(); + + fake.add(path, (req: ClientRequest, res: ServerResponse) => { + res.statusCode = 503; + res.end(); + }); + + const block = baseBlock({ + block: { + pathname: path, + }, + }); + + expect.assertions(1); + let e; + try { + await de.run(block); + + } catch (error) { + e = error; + } + + expect(e.error.body).toBeNull(); + }); + + it('text/plain', async() => { + const path = getPath(); + + const RESPONSE = 'Привет!'; + fake.add(path, (req: ClientRequest, res: ServerResponse) => { + res.statusCode = 503; + const buffer = Buffer.from(RESPONSE); + res.setHeader('content-length', Buffer.byteLength(buffer)); + res.setHeader('content-type', 'text/plain'); + res.end(buffer); + }); + + const block = baseBlock({ + block: { + pathname: path, + }, + }); + + expect.assertions(1); + let e; + try { + await de.run(block); + + } catch (error) { + e = error; + } + + expect(e.error.body).toBe(RESPONSE); + }); + + it('application/json', async() => { + const path = getPath(); + + const RESPONSE = { + error: 'Ошибка!', + }; + fake.add(path, (req: ClientRequest, res: ServerResponse) => { + res.statusCode = 503; + const buffer = Buffer.from(JSON.stringify(RESPONSE)); + res.setHeader('content-length', Buffer.byteLength(buffer)); + res.setHeader('content-type', 'application/json'); + res.end(buffer); + }); + + const block = baseBlock({ + block: { + pathname: path, + }, + + }); + + expect.assertions(1); + let e; + try { + await de.run(block); + + } catch (error) { + e = error; + } + expect(e.error.body).toEqual(RESPONSE); + }); + + it('text/plain, is_json: true, invalid json in repsonse', async() => { + const path = getPath(); + + const RESPONSE = 'Привет!'; + fake.add(path, (req: ClientRequest, res: ServerResponse) => { + res.statusCode = 503; + const buffer = Buffer.from(RESPONSE); + res.setHeader('content-length', Buffer.byteLength(buffer)); + res.setHeader('content-type', 'text/plain'); + res.end(buffer); + }); + + const block = baseBlock({ + block: { + pathname: path, + isJson: true, + }, + }); + + expect.assertions(1); + let e; + try { + await de.run(block); + + } catch (error) { + e = error; + } + expect(e.error.body).toBe(RESPONSE); + }); + + it('application/json, invalid json in repsonse', async() => { + const path = getPath(); + + const RESPONSE = 'Привет!'; + fake.add(path, (req: ClientRequest, res: ServerResponse) => { + res.statusCode = 503; + const buffer = Buffer.from(RESPONSE); + res.setHeader('content-length', Buffer.byteLength(buffer)); + res.setHeader('content-type', 'application/json'); + res.end(buffer); + }); + + const block = baseBlock({ + block: { + pathname: path, + }, + }); + + expect.assertions(1); + let e; + try { + await de.run(block); + + } catch (error) { + e = error; + } + expect(e.error.body).toBe(RESPONSE); + }); + + }); + + describe('misc', () => { + + it('extra', async() => { + const path = getPath(); + + fake.add(path, { + statusCode: 200, + }); + + const NAME = 'resource_name'; + const block = baseBlock({ + block: { + pathname: path, + }, + + options: { + name: NAME, + after: ({ result }: { result: DescriptHttpBlockResult }) => result, + }, + }); + + const result = await de.run(block); + + expect(result.requestOptions.extra).toEqual({ + name: NAME, + }); + }); + + it('prepare_request_options', async() => { + const path1 = getPath(); + const path2 = getPath(); + + const spy1 = jest.fn((req, res) => res.end()); + const spy2 = jest.fn((req, res) => res.end()); + fake.add(path1, spy1); + fake.add(path2, spy2); + + const block = baseBlock({ + block: { + pathname: path1, + prepareRequestOptions: (requestOptions) => { + return { + ...requestOptions, + pathname: path2, + }; + }, + }, + }); + + await de.run(block); + + expect(spy1.mock.calls).toHaveLength(0); + expect(spy2.mock.calls).toHaveLength(1); + }); + + it('default parse_body', async() => { + const path = getPath(); + + const CONTENT = 'Привет!'; + + fake.add(path, { + statusCode: 200, + content: CONTENT, + }); + + const block = baseBlock({ + block: { + pathname: path, + }, + options: { + after: ({ result }: { result: DescriptHttpBlockResult }) => result, + }, + }); + + const result = await de.run(block); + + expect(result.statusCode).toBe(200); + expect(result.result).toBe(CONTENT); + }); + + it('custom parse_body', async() => { + const path = getPath(); + + const CONTENT = 'Привет!'; + + fake.add(path, { + statusCode: 200, + content: CONTENT, + }); + + const BODY = 'Пока!'; + const spy = jest.fn(() => { + return BODY; + }); + + const block = baseBlock({ + block: { + pathname: path, + parseBody: spy, + }, + }); + + const context = {}; + const result = await de.run(block, { context }); + + const [ aResult, aContext ] = spy.mock.calls[ 0 ]; + expect(result.result).toBe(BODY); + expect(Buffer.compare(aResult.body, Buffer.from(CONTENT))).toBe(0); + expect(aContext).toBe(context); + }); + + it('custom parse_body should process empty body', async() => { + const path = getPath(); + + const RESPONSE = Buffer.from([]); + fake.add(path, (req: ClientRequest, res: ServerResponse) => { + res.setHeader('content-length', Buffer.byteLength(RESPONSE)); + res.setHeader('content-type', 'application/protobuf'); + res.end(RESPONSE); + }); + + const block = baseBlock({ + block: { + pathname: path, + parseBody: ({ body }) => { + if (!body || Buffer.byteLength(body) === 0) { + return {}; + + } else { + return null; + } + }, + + }, + }); + + const context = {}; + const result = await de.run(block, { context }); + + expect(result?.result).toEqual({}); + }); + + it('custom parse_body for error', async() => { + const path = getPath(); + + const CONTENT = 'Привет!'; + + fake.add(path, { + statusCode: 500, + content: CONTENT, + }); + + const BODY = 'Пока!'; + const spy = jest.fn(() => { + return BODY; + }); + + const block = baseBlock({ + block: { + pathname: path, + parseBody: spy, + }, + }); + + expect.assertions(3); + + const context = {}; + let e; + try { + await de.run(block, { context }); + } catch (error) { + e = error; + } + + const [ aResult, aContext ] = spy.mock.calls[ 0 ]; + expect(e.error.body).toBe(BODY); + expect(Buffer.compare(aResult.body, Buffer.from(CONTENT))).toBe(0); + expect(aContext).toBe(context); + }); + + }); + +}); diff --git a/tests/http_block.test.js b/tests/http_block.test.js deleted file mode 100644 index e76e64c..0000000 --- a/tests/http_block.test.js +++ /dev/null @@ -1,1372 +0,0 @@ -const http_ = require( 'http' ); -const url_ = require( 'url' ); -const qs_ = require( 'querystring' ); - -const de = require( '../lib' ); - -const Server = require( './server' ); - -const { get_path, get_result_block } = require( './helpers' ); - -const strip_null_and_undefined_values = require( '../lib/strip_null_and_undefined_values' ); - -// --------------------------------------------------------------------------------------------------------------- // - -describe( 'http', () => { - - const PORT = 10000; - - const base_block = ( args = {} ) => de.http( { - block: { - protocol: 'http:', - hostname: '127.0.0.1', - port: PORT, - ...args.block, - }, - options: { - logger: new de.Logger(), - ...args.options, - }, - } ); - - const fake = new Server( { - module: http_, - listen_options: { - port: PORT, - }, - } ); - - beforeAll( () => fake.start() ); - afterAll( () => fake.stop() ); - - describe( 'basic block properties', () => { - const path = get_path(); - - const PROPS = [ - [ 'method', 'POST' ], - [ 'protocol', 'http:' ], - [ 'port', PORT ], - [ 'hostname', '127.0.0.1' ], - [ 'pathname', path ], - [ 'max_retries', 0 ], - [ 'timeout', 100 ], - [ 'headers', {} ], - [ 'query', {} ], - [ 'body', {} ], - ]; - - fake.add( path ); - - it.each( PROPS )( '%j is a function and it gets { params, context }', async ( name, value ) => { - const spy = jest.fn( () => value ); - - const block = base_block( { - block: { - pathname: path, - // Чтобы body отработал. - method: 'POST', - [ name ]: spy, - }, - } ); - - const params = { - foo: 42, - }; - const context = { - req: true, - res: true, - }; - await de.run( block, { params, context } ); - - const call = spy.mock.calls[ 0 ][ 0 ]; - expect( call.params ).toBe( params ); - expect( call.context ).toBe( context ); - } ); - - it.each( PROPS )( '%j is a function and it gets { deps }', async ( name, value ) => { - const spy = jest.fn( () => value ); - - let foo_result; - let id; - const block = de.func( { - block: ( { generate_id } ) => { - id = generate_id(); - - return de.object( { - block: { - foo: get_result_block( () => { - foo_result = { - foo: 42, - }; - return foo_result; - } )( { - options: { - id: id, - }, - } ), - - bar: base_block( { - block: { - pathname: path, - method: 'POST', - [ name ]: spy, - }, - - options: { - deps: id, - }, - } ), - }, - } ); - }, - } ); - - await de.run( block ); - - const call = spy.mock.calls[ 0 ][ 0 ]; - expect( call.deps[ id ] ).toBe( foo_result ); - } ); - - } ); - - /* - it( 'path is a string', async () => { - const path = get_path(); - - const CONTENT = 'Привет!'; - - fake.add( path, { - status_code: 200, - content: CONTENT, - } ); - - const block = base_block( { - block: { - pathname: path, - }, - } ); - - const result = await de.run( block ); - - expect( result.status_code ).toBe( 200 ); - expect( result.result ).toBe( CONTENT ); - } ); - - it( 'path is a function', async () => { - const path = get_path(); - - const CONTENT = 'Привет!'; - - fake.add( path, { - status_code: 200, - content: CONTENT, - } ); - - const block = base_block( { - block: { - pathname: () => path, - }, - } ); - - const result = await de.run( block ); - - expect( result.status_code ).toBe( 200 ); - expect( result.result ).toBe( CONTENT ); - } ); - */ - - describe( 'headers', () => { - - it( 'is a function', async () => { - const path = get_path(); - - const spy = jest.fn( ( req, res ) => res.end() ); - - fake.add( path, spy ); - - let block_headers; - const block = base_block( { - block: { - pathname: path, - headers: () => { - block_headers = { - 'x-a': 'a', - 'X-B': 'B', - }; - return block_headers; - }, - }, - } ); - - await de.run( block ); - - const headers = spy.mock.calls[ 0 ][ 0 ].headers; - expect( headers[ 'x-a' ] ).toBe( 'a' ); - expect( headers[ 'x-b' ] ).toBe( 'B' ); - } ); - - it( 'is an object', async () => { - const path = get_path(); - - const spy = jest.fn( ( req, res ) => res.end() ); - - fake.add( path, spy ); - - const header_spy = jest.fn( () => 'c' ); - const block = base_block( { - block: { - pathname: path, - headers: { - 'x-a': 'a', - 'X-B': 'B', - 'x-c': header_spy, - }, - }, - } ); - - await de.run( block ); - - const headers = spy.mock.calls[ 0 ][ 0 ].headers; - expect( headers[ 'x-a' ] ).toBe( 'a' ); - expect( headers[ 'x-b' ] ).toBe( 'B' ); - expect( headers[ 'x-c' ] ).toBe( 'c' ); - } ); - - it( 'is an object and value function gets { params, context }', async () => { - const path = get_path(); - - fake.add( path ); - - const spy = jest.fn( () => 'a' ); - const block = base_block( { - block: { - pathname: path, - headers: { - 'x-a': spy, - }, - }, - } ); - - const params = { - foo: 42, - }; - const context = { - context: true, - }; - await de.run( block, { params, context } ); - - const call = spy.mock.calls[ 0 ][ 0 ]; - expect( call.params ).toBe( params ); - expect( call.context ).toBe( context ); - } ); - - describe( 'inheritance', () => { - - it( 'child block is undefined', async () => { - const path = get_path(); - - const RESPONSE = 'Привет!'; - fake.add( path, ( req, res ) => res.end( RESPONSE ) ); - - const parent = base_block( { - block: { - pathname: path, - }, - } ); - const child = parent(); - - const result = await de.run( child ); - - expect( result.result ).toBe( RESPONSE ); - } ); - - it( 'child is a function and it gets { headers }', async () => { - const path = get_path(); - - fake.add( path ); - - const spy = jest.fn(); - - let parent_headers; - const parent = base_block( { - block: { - pathname: path, - headers: () => { - parent_headers = { - 'x-a': 'a', - }; - return parent_headers; - }, - }, - } ); - const child = parent( { - block: { - headers: spy, - }, - } ); - - await de.run( child ); - - const call = spy.mock.calls[ 0 ][ 0 ]; - expect( call.headers ).toBe( parent_headers ); - } ); - - it( 'child is an object', async () => { - const path = get_path(); - - const spy = jest.fn( ( req, res ) => res.end() ); - fake.add( path, spy ); - - const header_spy = jest.fn( () => 'b' ); - - let parent_headers; - const parent = base_block( { - block: { - pathname: path, - headers: () => { - parent_headers = { - 'x-a': 'a', - }; - return parent_headers; - }, - }, - } ); - const child = parent( { - block: { - headers: { - 'x-b': header_spy, - 'X-C': 'C', - }, - }, - } ); - - await de.run( child ); - - const header_call = header_spy.mock.calls[ 0 ][ 0 ]; - expect( header_call.headers ).toBe( parent_headers ); - - const call = spy.mock.calls[ 0 ][ 0 ]; - expect( call.headers[ 'x-a' ] ).toBe( undefined ); - expect( call.headers[ 'x-b' ] ).toBe( 'b' ); - expect( call.headers[ 'x-c' ] ).toBe( 'C' ); - } ); - - } ); - - } ); - - describe( 'query', () => { - - it( 'is a function', async () => { - const path = get_path(); - const spy = jest.fn( ( req, res ) => res.end() ); - fake.add( path, spy ); - - const block = base_block( { - block: { - pathname: path, - query: () => { - return { - b: 'b', - a: 'a', - c: undefined, - d: '', - e: null, - f: 0, - g: false, - }; - }, - }, - } ); - - await de.run( block ); - - const req = spy.mock.calls[ 0 ][ 0 ]; - expect( url_.parse( req.url, true ).query ).toEqual( { - b: 'b', - a: 'a', - d: '', - f: '0', - g: 'false', - } ); - } ); - - it( 'is an object and value is null', async () => { - const path = get_path(); - const spy = jest.fn( ( req, res ) => res.end() ); - fake.add( path, spy ); - - const block = base_block( { - block: { - pathname: path, - query: { - a: null, - b: null, - c: null, - d: null, - e: null, - f: null, - }, - }, - } ); - - const params = { - a: null, - b: undefined, - c: 0, - d: '', - e: false, - f: 'foo', - g: 'bar', - }; - await de.run( block, { params } ); - - const req = spy.mock.calls[ 0 ][ 0 ]; - const query = url_.parse( req.url, true ).query; - // NOTE: В ноде query сделано через Object.create( null ), - // так что toStrictEqual работает неправильно с ним. - expect( { ...query } ).toStrictEqual( { - a: '', - c: '0', - d: '', - e: 'false', - f: 'foo', - } ); - } ); - - it( 'is an object and value is undefined', async () => { - const path = get_path(); - const spy = jest.fn( ( req, res ) => res.end() ); - fake.add( path, spy ); - - const block = base_block( { - block: { - pathname: path, - query: { - a: undefined, - b: undefined, - c: undefined, - d: undefined, - e: undefined, - f: undefined, - }, - }, - } ); - - const params = { - a: null, - b: undefined, - c: 0, - d: '', - e: false, - f: 'foo', - g: 'bar', - }; - await de.run( block, { params } ); - - const req = spy.mock.calls[ 0 ][ 0 ]; - const query = url_.parse( req.url, true ).query; - expect( { ...query } ).toStrictEqual( {} ); - } ); - - it( 'is an object and value is not null or undefined #1', async () => { - const path = get_path(); - const spy = jest.fn( ( req, res ) => res.end() ); - fake.add( path, spy ); - - const block = base_block( { - block: { - pathname: path, - query: { - a: 0, - b: '', - c: false, - d: 42, - e: 'foo', - }, - }, - } ); - - const params = {}; - await de.run( block, { params } ); - - const req = spy.mock.calls[ 0 ][ 0 ]; - const query = url_.parse( req.url, true ).query; - expect( { ...query } ).toStrictEqual( { - a: '0', - b: '', - c: 'false', - d: '42', - e: 'foo', - } ); - } ); - - it( 'is an object and value is not null or undefined #2', async () => { - const path = get_path(); - const spy = jest.fn( ( req, res ) => res.end() ); - fake.add( path, spy ); - - const block = base_block( { - block: { - pathname: path, - query: { - a: 'foo', - b: 'foo', - c: 'foo', - d: 'foo', - e: 'foo', - }, - }, - } ); - - const params = { - a: 0, - b: '', - c: false, - d: null, - e: undefined, - }; - await de.run( block, { params } ); - - const req = spy.mock.calls[ 0 ][ 0 ]; - const query = url_.parse( req.url, true ).query; - expect( { ...query } ).toStrictEqual( { - a: '0', - b: '', - c: 'false', - d: '', - e: 'foo', - } ); - } ); - - it( 'is an object and value function gets { params, context }', async () => { - const path = get_path(); - fake.add( path ); - - const spy = jest.fn(); - const block = base_block( { - block: { - pathname: path, - query: spy, - }, - } ); - - const params = { - foo: 42, - }; - const context = { - req: true, - res: true, - }; - await de.run( block, { params, context } ); - - const call = spy.mock.calls[ 0 ][ 0 ]; - expect( call.params ).toBe( params ); - expect( call.context ).toBe( context ); - } ); - - it( 'is an array of object and function', async () => { - const path = get_path(); - const spy = jest.fn( ( req, res ) => res.end() ); - fake.add( path, spy ); - - const block = base_block( { - block: { - pathname: path, - query: [ - { - foo: null, - }, - ( { query, params } ) => { - return { - ...query, - bar: params.bar, - }; - }, - ], - }, - } ); - - const params = { - foo: 'foo', - bar: 'bar', - quu: 'quu', - }; - await de.run( block, { params } ); - - const req = spy.mock.calls[ 0 ][ 0 ]; - const query = url_.parse( req.url, true ).query; - expect( { ...query } ).toStrictEqual( { - foo: 'foo', - bar: 'bar', - } ); - } ); - - describe( 'inheritance', () => { - - it( 'child is a function and it gets { query }', async () => { - const path = get_path(); - fake.add( path ); - - let parent_query; - const parent = base_block( { - block: { - pathname: path, - query: () => { - parent_query = { - a: 'a', - b: undefined, - c: null, - d: 0, - e: '', - f: false, - }; - return parent_query; - }, - }, - } ); - - const spy = jest.fn(); - const child = parent( { - block: { - query: spy, - }, - } ); - - await de.run( child ); - - const call = spy.mock.calls[ 0 ][ 0 ]; - expect( call.query ).toStrictEqual( strip_null_and_undefined_values( parent_query ) ); - } ); - - it( 'child is an object and value function gets { query }', async () => { - const path = get_path(); - fake.add( path ); - - let parent_query; - const parent = base_block( { - block: { - pathname: path, - query: () => { - parent_query = { - a: 'a', - b: undefined, - c: null, - d: 0, - e: '', - f: false, - }; - return parent_query; - }, - }, - } ); - - const spy = jest.fn(); - const child = parent( { - block: { - query: { - bar: spy, - }, - }, - } ); - - await de.run( child ); - - const call = spy.mock.calls[ 0 ][ 0 ]; - expect( call.query ).toStrictEqual( strip_null_and_undefined_values( parent_query ) ); - } ); - - } ); - - } ); - - describe( 'body', () => { - - it( 'no body', async () => { - const path = get_path(); - - const spy = jest.fn( ( req, res ) => res.end() ); - - fake.add( path, spy ); - - const block = base_block( { - block: { - pathname: path, - method: 'POST', - }, - } ); - - await de.run( block ); - - const body = spy.mock.calls[ 0 ][ 2 ]; - expect( body ).toBe( null ); - } ); - - it( 'is a string', async () => { - const path = get_path(); - - const spy = jest.fn( ( req, res ) => res.end() ); - - fake.add( path, spy ); - - const BODY = 'Привет!'; - const block = base_block( { - block: { - pathname: path, - method: 'POST', - body: BODY, - }, - } ); - - await de.run( block ); - - const body = spy.mock.calls[ 0 ][ 2 ]; - expect( body.toString() ).toBe( BODY ); - } ); - - it( 'is a number', async () => { - const path = get_path(); - - const spy = jest.fn( ( req, res ) => res.end() ); - - fake.add( path, spy ); - - const BODY = 42; - const block = base_block( { - block: { - pathname: path, - method: 'POST', - body: BODY, - }, - } ); - - await de.run( block ); - - const body = spy.mock.calls[ 0 ][ 2 ]; - expect( body.toString() ).toBe( String( BODY ) ); - } ); - - it( 'is a Buffer', async () => { - const path = get_path(); - - const spy = jest.fn( ( req, res ) => res.end() ); - - fake.add( path, spy ); - - const BODY = Buffer.from( 'Привет!' ); - const block = base_block( { - block: { - pathname: path, - method: 'POST', - body: BODY, - }, - } ); - - await de.run( block ); - - const body = spy.mock.calls[ 0 ][ 2 ]; - expect( body.toString() ).toBe( BODY.toString() ); - } ); - - it( 'is a function returning string', async () => { - const path = get_path(); - - const spy = jest.fn( ( req, res ) => res.end() ); - - fake.add( path, spy ); - - const BODY = 'Привет!'; - const block = base_block( { - block: { - pathname: path, - method: 'POST', - body: () => BODY, - }, - } ); - - await de.run( block ); - - const body = spy.mock.calls[ 0 ][ 2 ]; - expect( body.toString() ).toBe( BODY ); - } ); - - it( 'is a function returning Buffer', async () => { - const path = get_path(); - - const spy = jest.fn( ( req, res ) => res.end() ); - - fake.add( path, spy ); - - const BODY = Buffer.from( 'Привет!' ); - const block = base_block( { - block: { - pathname: path, - method: 'POST', - body: () => BODY, - }, - } ); - - await de.run( block ); - - const body = spy.mock.calls[ 0 ][ 2 ]; - expect( body.toString() ).toBe( BODY.toString() ); - } ); - - it( 'is an function returning object', async () => { - const path = get_path(); - - const spy = jest.fn( ( req, res ) => res.end() ); - - fake.add( path, spy ); - - const BODY = { - a: 'Привет!', - }; - const block = base_block( { - block: { - pathname: path, - method: 'POST', - body: () => BODY, - }, - } ); - - await de.run( block ); - - const body = spy.mock.calls[ 0 ][ 2 ]; - expect( body.toString() ).toBe( qs_.stringify( BODY ) ); - } ); - - it( 'with body_compress', async () => { - const path = get_path(); - - const spy = jest.fn( ( req, res ) => res.end() ); - - fake.add( path, spy ); - - const block = base_block( { - block: { - pathname: path, - method: 'POST', - body: 'Привет!', - body_compress: true, - }, - } ); - - await de.run( block ); - - expect( spy ).toHaveBeenCalledTimes( 1 ); - const body = spy.mock.calls[ 0 ][ 2 ]; - expect( body ).toBeValidGzip(); - expect( body ).toHaveLength( 34 ); - expect( body ).toHaveUngzipValue( 'Привет!' ); - } ); - } ); - - describe( 'parse response', () => { - - it( 'text/plain', async () => { - const path = get_path(); - - const RESPONSE = Buffer.from( 'Привет!' ); - fake.add( path, ( req, res ) => { - res.setHeader( 'content-length', Buffer.byteLength( RESPONSE ) ); - res.setHeader( 'content-type', 'text/plain' ); - res.end( RESPONSE ); - } ); - - const block = base_block( { - block: { - pathname: path, - }, - } ); - - const result = await de.run( block ); - - expect( result.result.toString() ).toBe( RESPONSE.toString() ); - } ); - - it( 'application/json #1', async () => { - const path = get_path(); - - const RESPONSE = { - text: 'Привет!', - }; - fake.add( path, ( req, res ) => { - const buffer = Buffer.from( JSON.stringify( RESPONSE ) ); - res.setHeader( 'content-length', Buffer.byteLength( buffer ) ); - res.setHeader( 'content-type', 'application/json' ); - res.end( buffer ); - } ); - - const block = base_block( { - block: { - pathname: path, - }, - } ); - - const result = await de.run( block ); - - expect( result.result ).toEqual( RESPONSE ); - } ); - - it( 'application/json #2', async () => { - const path = get_path(); - - const RESPONSE = { - text: 'Привет!', - }; - fake.add( path, ( req, res ) => { - const buffer = Buffer.from( JSON.stringify( RESPONSE ) ); - res.setHeader( 'content-length', Buffer.byteLength( buffer ) ); - res.setHeader( 'content-type', 'application/json; charset=utf-8' ); - res.end( buffer ); - } ); - - const block = base_block( { - block: { - pathname: path, - }, - } ); - - const result = await de.run( block ); - - expect( result.result ).toEqual( RESPONSE ); - } ); - - it( 'text/plain, is_json: true', async () => { - const path = get_path(); - - const RESPONSE = { - text: 'Привет!', - }; - fake.add( path, ( req, res ) => { - const buffer = Buffer.from( JSON.stringify( RESPONSE ) ); - res.setHeader( 'content-length', Buffer.byteLength( buffer ) ); - res.setHeader( 'content-type', 'text/plain' ); - res.end( buffer ); - } ); - - const block = base_block( { - block: { - pathname: path, - is_json: true, - }, - } ); - - const result = await de.run( block ); - - expect( result.result ).toEqual( RESPONSE ); - } ); - - it( 'invalid json in response, application/json', async () => { - const path = get_path(); - - const RESPONSE = Buffer.from( 'Привет!' ); - fake.add( path, ( req, res ) => { - res.setHeader( 'content-length', Buffer.byteLength( RESPONSE ) ); - res.setHeader( 'content-type', 'application/json' ); - res.end( RESPONSE ); - } ); - - const block = base_block( { - block: { - pathname: path, - }, - } ); - - expect.assertions( 2 ); - try { - await de.run( block ); - - } catch ( error ) { - expect( de.is_error( error ) ).toBe( true ); - expect( error.error.id ).toEqual( de.ERROR_ID.INVALID_JSON ); - } - } ); - - it( 'invalid json in response, is_json: true', async () => { - const path = get_path(); - - const RESPONSE = Buffer.from( 'Привет!' ); - fake.add( path, ( req, res ) => { - res.setHeader( 'content-length', Buffer.byteLength( RESPONSE ) ); - res.setHeader( 'content-type', 'text/plain' ); - res.end( RESPONSE ); - } ); - - const block = base_block( { - block: { - pathname: path, - is_json: true, - }, - } ); - - expect.assertions( 2 ); - try { - await de.run( block ); - - } catch ( error ) { - expect( de.is_error( error ) ).toBe( true ); - expect( error.error.id ).toEqual( de.ERROR_ID.INVALID_JSON ); - } - } ); - - describe( 'empty body', () => { - it( 'text/plain', async () => { - const path = get_path(); - - const RESPONSE = Buffer.from( [] ); - fake.add( path, ( req, res ) => { - res.setHeader( 'content-length', Buffer.byteLength( RESPONSE ) ); - res.setHeader( 'content-type', 'text/plain' ); - res.end( RESPONSE ); - } ); - - const block = base_block( { - block: { - pathname: path, - }, - } ); - - const result = await de.run( block ); - - expect( result.result ).toBeNull(); - } ); - - it( 'application/json', async () => { - const path = get_path(); - - const RESPONSE = Buffer.from( [] ); - fake.add( path, ( req, res ) => { - res.setHeader( 'content-length', Buffer.byteLength( RESPONSE ) ); - res.setHeader( 'content-type', 'application/json' ); - res.end( RESPONSE ); - } ); - - const block = base_block( { - block: { - pathname: path, - }, - } ); - - const result = await de.run( block ); - - expect( result.result ).toBeNull(); - } ); - } ); - - } ); - - describe( 'parse error', () => { - - it( 'no body', async () => { - const path = get_path(); - - fake.add( path, ( req, res ) => { - res.statusCode = 503; - res.end(); - } ); - - const block = base_block( { - block: { - pathname: path, - }, - } ); - - expect.assertions( 1 ); - try { - await de.run( block ); - - } catch ( error ) { - expect( error.error.body ).toBe( null ); - } - } ); - - it( 'text/plain', async () => { - const path = get_path(); - - const RESPONSE = 'Привет!'; - fake.add( path, ( req, res ) => { - res.statusCode = 503; - const buffer = Buffer.from( RESPONSE ); - res.setHeader( 'content-length', Buffer.byteLength( buffer ) ); - res.setHeader( 'content-type', 'text/plain' ); - res.end( buffer ); - } ); - - const block = base_block( { - block: { - pathname: path, - }, - } ); - - expect.assertions( 1 ); - try { - await de.run( block ); - - } catch ( error ) { - expect( error.error.body ).toBe( RESPONSE ); - } - } ); - - it( 'application/json', async () => { - const path = get_path(); - - const RESPONSE = { - error: 'Ошибка!', - }; - fake.add( path, ( req, res ) => { - res.statusCode = 503; - const buffer = Buffer.from( JSON.stringify( RESPONSE ) ); - res.setHeader( 'content-length', Buffer.byteLength( buffer ) ); - res.setHeader( 'content-type', 'application/json' ); - res.end( buffer ); - } ); - - const block = base_block( { - block: { - pathname: path, - }, - } ); - - expect.assertions( 1 ); - try { - await de.run( block ); - - } catch ( error ) { - expect( error.error.body ).toEqual( RESPONSE ); - } - } ); - - it( 'text/plain, is_json: true, invalid json in repsonse', async () => { - const path = get_path(); - - const RESPONSE = 'Привет!'; - fake.add( path, ( req, res ) => { - res.statusCode = 503; - const buffer = Buffer.from( RESPONSE ); - res.setHeader( 'content-length', Buffer.byteLength( buffer ) ); - res.setHeader( 'content-type', 'text/plain' ); - res.end( buffer ); - } ); - - const block = base_block( { - block: { - pathname: path, - is_json: true, - }, - } ); - - expect.assertions( 1 ); - try { - await de.run( block ); - - } catch ( error ) { - expect( error.error.body ).toBe( RESPONSE ); - } - } ); - - it( 'application/json, invalid json in repsonse', async () => { - const path = get_path(); - - const RESPONSE = 'Привет!'; - fake.add( path, ( req, res ) => { - res.statusCode = 503; - const buffer = Buffer.from( RESPONSE ); - res.setHeader( 'content-length', Buffer.byteLength( buffer ) ); - res.setHeader( 'content-type', 'application/json' ); - res.end( buffer ); - } ); - - const block = base_block( { - block: { - pathname: path, - }, - } ); - - expect.assertions( 1 ); - try { - await de.run( block ); - - } catch ( error ) { - expect( error.error.body ).toBe( RESPONSE ); - } - } ); - - } ); - - describe( 'misc', () => { - - it( 'extra', async () => { - const path = get_path(); - - fake.add( path, { - status_code: 200, - } ); - - const NAME = 'resource_name'; - const block = base_block( { - block: { - pathname: path, - }, - - options: { - name: NAME, - }, - } ); - - const result = await de.run( block ); - - expect( result.request_options.extra ).toEqual( { - name: NAME, - } ); - } ); - - it( 'prepare_request_options', async () => { - const path_1 = get_path(); - const path_2 = get_path(); - - const spy_1 = jest.fn( ( req, res ) => res.end() ); - const spy_2 = jest.fn( ( req, res ) => res.end() ); - fake.add( path_1, spy_1 ); - fake.add( path_2, spy_2 ); - - const block = base_block( { - block: { - pathname: path_1, - prepare_request_options: ( request_options ) => { - return { - ...request_options, - pathname: path_2, - }; - }, - }, - } ); - - await de.run( block ); - - expect( spy_1.mock.calls ).toHaveLength( 0 ); - expect( spy_2.mock.calls ).toHaveLength( 1 ); - } ); - - it( 'default parse_body', async () => { - const path = get_path(); - - const CONTENT = 'Привет!'; - - fake.add( path, { - status_code: 200, - content: CONTENT, - } ); - - const block = base_block( { - block: { - pathname: path, - }, - } ); - - const result = await de.run( block ); - - expect( result.status_code ).toBe( 200 ); - expect( result.result ).toBe( CONTENT ); - } ); - - it( 'custom parse_body', async () => { - const path = get_path(); - - const CONTENT = 'Привет!'; - - fake.add( path, { - status_code: 200, - content: CONTENT, - } ); - - const BODY = 'Пока!'; - const spy = jest.fn( () => { - return BODY; - } ); - - const block = base_block( { - block: { - pathname: path, - parse_body: spy, - }, - } ); - - const context = {}; - const result = await de.run( block, { context } ); - - const [ a_result, a_context ] = spy.mock.calls[ 0 ]; - expect( result.result ).toBe( BODY ); - expect( Buffer.compare( a_result.body, Buffer.from( CONTENT ) ) ).toBe( 0 ); - expect( a_context ).toBe( context ); - } ); - - it( 'custom parse_body should process empty body', async () => { - const path = get_path(); - - const RESPONSE = Buffer.from( [] ); - fake.add( path, ( req, res ) => { - res.setHeader( 'content-length', Buffer.byteLength( RESPONSE ) ); - res.setHeader( 'content-type', 'application/protobuf' ); - res.end( RESPONSE ); - } ); - - const block = base_block( { - block: { - pathname: path, - parse_body: ( { body } ) => { - if ( !body || Buffer.byteLength( body ) === 0 ) { - return {}; - - } else { - return null; - } - }, - }, - } ); - - const context = {}; - const result = await de.run( block, { context } ); - - expect( result.result ).toEqual( {} ); - } ); - - it( 'custom parse_body for error', async () => { - const path = get_path(); - - const CONTENT = 'Привет!'; - - fake.add( path, { - status_code: 500, - content: CONTENT, - } ); - - const BODY = 'Пока!'; - const spy = jest.fn( () => { - return BODY; - } ); - - const block = base_block( { - block: { - pathname: path, - parse_body: spy, - }, - } ); - - expect.assertions( 3 ); - - const context = {}; - try { - await de.run( block, { context } ); - } catch ( e ) { - const [ a_result, a_context ] = spy.mock.calls[ 0 ]; - expect( e.error.body ).toBe( BODY ); - expect( Buffer.compare( a_result.body, Buffer.from( CONTENT ) ) ).toBe( 0 ); - expect( a_context ).toBe( context ); - } - } ); - - } ); - -} ); diff --git a/tests/isPlainObject.test.ts b/tests/isPlainObject.test.ts new file mode 100644 index 0000000..58c5a2e --- /dev/null +++ b/tests/isPlainObject.test.ts @@ -0,0 +1,34 @@ +import isPlainObject from '../lib/isPlainObject'; + +describe('isPlainObject', () => { + + it.each([ null, undefined, 'Hello' ])('%j', (obj) => { + expect(isPlainObject(obj)).toBe(false); + }); + + it('{}', () => { + const obj = { + foo: 42, + }; + + expect(isPlainObject(obj)).toBe(true); + }); + + it('Object.create(null)', () => { + const obj = Object.create(null); + + expect(isPlainObject(obj)).toBe(true); + }); + + it('instanceof Foo', () => { + const Foo = function() { + // Do nothing. + }; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const obj = new Foo(); + + expect(isPlainObject(obj)).toBe(false); + }); + +}); diff --git a/tests/is_plain_object.test.js b/tests/is_plain_object.test.js deleted file mode 100644 index 577a611..0000000 --- a/tests/is_plain_object.test.js +++ /dev/null @@ -1,33 +0,0 @@ -const is_plain_object = require( '../lib/is_plain_object' ); - -describe( 'is_plain_object', () => { - - it.each( [ null, undefined, 'Hello' ] )( '%j', ( obj ) => { - expect( is_plain_object( obj ) ).toBe( false ); - } ); - - it( '{}', () => { - const obj = { - foo: 42, - }; - - expect( is_plain_object( obj ) ).toBe( true ); - } ); - - it( 'Object.create(null)', () => { - const obj = Object.create( null ); - - expect( is_plain_object( obj ) ).toBe( true ); - } ); - - it( 'instanceof Foo', () => { - const Foo = function() { - // Do nothing. - }; - const obj = new Foo(); - - expect( is_plain_object( obj ) ).toBe( false ); - } ); - -} ); - diff --git a/tests/lifecycle.test.js b/tests/lifecycle.test.js deleted file mode 100644 index 9ea7e0e..0000000 --- a/tests/lifecycle.test.js +++ /dev/null @@ -1,85 +0,0 @@ -const de = require( '../lib' ); - -describe( 'lifecycle', () => { - - it( 'inheritance', async () => { - let action_result; - const action_spy = jest.fn( () => { - action_result = { - a: 1, - }; - return action_result; - } ); - - let parent_params_result; - const parent_params_spy = jest.fn( () => { - parent_params_result = { - b: 2, - }; - return parent_params_result; - } ); - - const parent_before_spy = jest.fn(); - - let parent_after_result; - const parent_after_spy = jest.fn( () => { - parent_after_result = { - c: 3, - }; - return parent_after_result; - } ); - - const parent = de.func( { - block: action_spy, - options: { - params: parent_params_spy, - before: parent_before_spy, - after: parent_after_spy, - }, - } ); - - let child_params_result; - const child_params_spy = jest.fn( () => { - child_params_result = { - d: 4, - }; - return child_params_result; - } ); - - const child_before_spy = jest.fn(); - - let child_after_result; - const child_after_spy = jest.fn( () => { - child_after_result = { - e: 5, - }; - return child_after_result; - } ); - - const child = parent( { - options: { - params: child_params_spy, - before: child_before_spy, - after: child_after_spy, - }, - } ); - - const params = { - foo: 42, - }; - const result = await de.run( child, { params } ); - - expect( child_params_spy.mock.calls[ 0 ][ 0 ].params ).toBe( params ); - expect( child_before_spy.mock.calls[ 0 ][ 0 ].params ).toBe( child_params_result ); - expect( parent_params_spy.mock.calls[ 0 ][ 0 ].params ).toBe( child_params_result ); - expect( parent_before_spy.mock.calls[ 0 ][ 0 ].params ).toBe( parent_params_result ); - expect( action_spy.mock.calls[ 0 ][ 0 ].params ).toBe( parent_params_result ); - expect( parent_after_spy.mock.calls[ 0 ][ 0 ].params ).toBe( parent_params_result ); - expect( parent_after_spy.mock.calls[ 0 ][ 0 ].result ).toBe( action_result ); - expect( child_after_spy.mock.calls[ 0 ][ 0 ].params ).toBe( child_params_result ); - expect( child_after_spy.mock.calls[ 0 ][ 0 ].result ).toBe( parent_after_result ); - expect( result ).toBe( child_after_result ); - } ); - -} ); - diff --git a/tests/lifecycle.test.ts b/tests/lifecycle.test.ts new file mode 100644 index 0000000..e08c050 --- /dev/null +++ b/tests/lifecycle.test.ts @@ -0,0 +1,84 @@ +import * as de from '../lib'; + +describe('lifecycle', () => { + + it('inheritance', async() => { + let actionResult; + const actionSpy = jest.fn(() => { + actionResult = { + a: 1, + }; + return actionResult; + }); + + let parentParamsResult; + const parentParamsSpy = jest.fn(() => { + parentParamsResult = { + b: 2, + }; + return parentParamsResult; + }); + + const parentBeforeSpy = jest.fn(); + + let parentAfterResult; + const parentAfterSpy = jest.fn(() => { + parentAfterResult = { + c: 3, + }; + return parentAfterResult; + }); + + const parent = de.func({ + block: actionSpy, + options: { + params: parentParamsSpy, + before: parentBeforeSpy, + after: parentAfterSpy, + }, + }); + + let childParamsResult; + const childParamsSpy = jest.fn(() => { + childParamsResult = { + d: 4, + }; + return childParamsResult; + }); + + const childBeforeSpy = jest.fn(); + + let childAfterResult; + const childAfterSpy = jest.fn(() => { + childAfterResult = { + e: 5, + }; + return childAfterResult; + }); + + const child = parent.extend({ + options: { + params: childParamsSpy, + before: childBeforeSpy, + after: childAfterSpy, + }, + }); + + const params = { + foo: 42, + }; + const result = await de.run(child, { params }); + + expect(childParamsSpy.mock.calls[ 0 ][ 0 ].params).toBe(params); + expect(childBeforeSpy.mock.calls[ 0 ][ 0 ].params).toBe(childParamsResult); + expect(parentParamsSpy.mock.calls[ 0 ][ 0 ].params).toBe(childParamsResult); + expect(parentBeforeSpy.mock.calls[ 0 ][ 0 ].params).toBe(parentParamsResult); + expect(actionSpy.mock.calls[ 0 ][ 0 ].params).toBe(parentParamsResult); + expect(parentAfterSpy.mock.calls[ 0 ][ 0 ].params).toBe(parentParamsResult); + expect(parentAfterSpy.mock.calls[ 0 ][ 0 ].result).toBe(actionResult); + expect(childAfterSpy.mock.calls[ 0 ][ 0 ].params).toBe(childParamsResult); + expect(childAfterSpy.mock.calls[ 0 ][ 0 ].result).toBe(parentAfterResult); + expect(result).toBe(childAfterResult); + }); + +}); diff --git a/tests/objectBlock.test.ts b/tests/objectBlock.test.ts new file mode 100644 index 0000000..c4e88ba --- /dev/null +++ b/tests/objectBlock.test.ts @@ -0,0 +1,438 @@ +import * as de from '../lib'; + +import { + getResultBlock, getErrorBlock, getTimeout, +} from './helpers'; + +// --------------------------------------------------------------------------------------------------------------- // + +describe('de.object', () => { + + it('block is undefined #1', () => { + expect.assertions(2); + let err; + try { + de.object({}); + + } catch (e) { + err = e; + } + + expect(de.isError(err)).toBe(true); + expect(err.error.id).toBe(de.ERROR_ID.INVALID_BLOCK); + }); + + it('block is undefined #2', () => { + expect.assertions(2); + let err; + try { + de.object(); + + } catch (e) { + err = e; + + } + + expect(de.isError(err)).toBe(true); + expect(err.error.id).toBe(de.ERROR_ID.INVALID_BLOCK); + }); + + it('empty object', async() => { + const block = de.object({ + block: {}, + }); + + const result = await de.run(block); + + expect(result).toEqual({}); + }); + + it('two subblocks', async() => { + const dataFoo = { + foo: 42, + }; + const blockFoo = getResultBlock(dataFoo, getTimeout(50, 100)); + + const dataBar = { + bar: 24, + }; + const blockBar = getResultBlock(dataBar, getTimeout(50, 100)); + + const block = de.object({ + block: { + foo: blockFoo, + bar: blockBar, + }, + }); + + const result = await de.run(block); + + expect(result).toEqual({ + foo: dataFoo, + bar: dataBar, + }); + expect(result.foo).toBe(dataFoo); + expect(result.bar).toBe(dataBar); + }); + + it('two subblocks, one required', async() => { + const dataFoo = { + foo: 42, + }; + const blockFoo = getResultBlock(dataFoo, getTimeout(50, 100)); + + const dataBar = { + bar: 24, + }; + const blockBar = getResultBlock(dataBar, getTimeout(50, 100)); + + const block = de.object({ + block: { + foo: blockFoo.extend({ + options: { + params: ({ params }: { params: { x: number }}) => { + return { + ...params, + x: 1, + }; + }, + required: true, + }, + }), + bar: blockBar.extend({ + options: { + params: ({ params }: { params: { z: number }}) => { + return { + ...params, + x: 1, + }; + }, + }, + }), + }, + }); + + const params = { + x: 1, + z: 2, + }; + + const result = await de.run(block, { params }); + + expect(result).toEqual({ + foo: dataFoo, + bar: dataBar, + }); + expect(result.foo).toBe(dataFoo); + expect(result.bar).toBe(dataBar); + }); + + it('two subblocks, one failed', async() => { + const errorFoo = de.error('SOME_ERROR'); + const blockFoo = getErrorBlock(errorFoo, getTimeout(50, 100)); + + const dataBar = { + bar: 24, + }; + const blockBar = getResultBlock(dataBar, getTimeout(50, 100)); + + const block = de.object({ + block: { + foo: blockFoo, + bar: blockBar, + }, + }); + + const result = await de.run(block); + + expect(result.foo).toBe(errorFoo); + expect(result.bar).toBe(dataBar); + }); + + it('two subblocks, both failed', async() => { + const errorFoo = de.error('SOME_ERROR_1'); + const blockFoo = getErrorBlock(errorFoo, getTimeout(50, 100)); + + const errorBar = de.error('SOME_ERROR_2'); + const blockBar = getErrorBlock(errorBar, getTimeout(50, 100)); + + const block = de.object({ + block: { + foo: blockFoo, + bar: blockBar, + }, + }); + + const result = await de.run(block); + + expect(result.foo).toBe(errorFoo); + expect(result.bar).toBe(errorBar); + }); + + it('two subblocks, one required failed #1', async() => { + const errorFoo = de.error('SOME_ERROR'); + const blockFoo = getErrorBlock(errorFoo, getTimeout(50, 100)); + const blockBar = getResultBlock(null, getTimeout(50, 100)); + + const block = de.object({ + block: { + foo: blockFoo.extend({ + options: { + required: true, + }, + }), + bar: blockBar, + }, + }); + + expect.assertions(4); + let e; + try { + await de.run(block); + + } catch (err) { + e = err; + } + expect(de.isError(e)).toBe(true); + expect(e.error.id).toBe(de.ERROR_ID.REQUIRED_BLOCK_FAILED); + expect(e.error.reason).toBe(errorFoo); + expect(e.error.path).toBe('.foo'); + }); + + it('two subblocks, one required failed #2', async() => { + const errorFoo = de.error('SOME_ERROR'); + const blockFoo = getErrorBlock(errorFoo, getTimeout(50, 100)); + const blockBar = getResultBlock(null, getTimeout(50, 100)); + const blockQuu = getResultBlock(null, getTimeout(50, 100)); + + const block = de.object({ + block: { + foo: de.array({ + block: [ + blockFoo.extend({ + options: { + required: true, + }, + }), + blockBar, + ], + options: { + required: true, + }, + }), + quu: blockQuu, + }, + }); + + expect.assertions(3); + let e; + try { + await de.run(block); + + } catch (err) { + e = err; + } + + expect(de.isError(e)).toBe(true); + expect(e.error.id).toBe(de.ERROR_ID.REQUIRED_BLOCK_FAILED); + expect(e.error.path).toBe('.foo[ 0 ]'); + }); + + it('order of keys', async() => { + const dataFoo = { + foo: 42, + }; + const blockFoo = getResultBlock(dataFoo, 100); + + const dataBar = { + bar: 24, + }; + const blockBar = getResultBlock(dataBar, 50); + + const block = de.object({ + block: { + foo: blockFoo, + bar: blockBar, + }, + }); + + const result = await de.run(block); + + expect(Object.keys(result)).toEqual([ 'foo', 'bar' ]); + }); + + describe('cancel', () => { + + it('cancel object, subblocks cancelled too #1', async() => { + const actionFooSpy = jest.fn(); + const blockFoo = getResultBlock(null, 150, { + onCancel: actionFooSpy, + }); + + const actionBarSpy = jest.fn(); + const blockBar = getResultBlock(null, 150, { + onCancel: actionBarSpy, + }); + + const block = de.object({ + block: { + foo: blockFoo, + bar: blockBar, + }, + }); + + const abortError = de.error('SOME_ERROR'); + let e; + try { + const cancel = new de.Cancel(); + setTimeout(() => { + cancel.cancel(abortError); + }, 100); + await de.run(block, { cancel }); + + } catch (err) { + e = err; + } + + expect(e).toBe(abortError); + expect(actionFooSpy.mock.calls[ 0 ][ 0 ]).toBe(abortError); + expect(actionBarSpy.mock.calls[ 0 ][ 0 ]).toBe(abortError); + }); + + it('cancel object, subblocks cancelled too #2', async() => { + const actionFooSpy = jest.fn(); + const blockFoo = getResultBlock(null, 50, { + onCancel: actionFooSpy, + }); + + const actionBarSpy = jest.fn(); + const blockBar = getResultBlock(null, 150, { + onCancel: actionBarSpy, + }); + + const block = de.object({ + block: { + foo: blockFoo, + bar: blockBar, + }, + }); + + const abortError = de.error('SOME_ERROR'); + let e; + try { + const cancel = new de.Cancel(); + setTimeout(() => { + cancel.cancel(abortError); + }, 100); + await de.run(block, { cancel }); + + } catch (err) { + e = err; + } + expect(e).toBe(abortError); + expect(actionFooSpy.mock.calls).toHaveLength(0); + expect(actionBarSpy.mock.calls[ 0 ][ 0 ]).toBe(abortError); + }); + + it('required block failed, other subblocks cancelled', async() => { + const errorFoo = de.error('SOME_ERROR'); + const blockFoo = getErrorBlock(errorFoo, 50); + + const actionBarSpy = jest.fn(); + const blockBar = getResultBlock(null, 150, { + onCancel: actionBarSpy, + }); + + const block = de.object({ + block: { + foo: blockFoo.extend({ + options: { + required: true, + }, + }), + bar: blockBar, + }, + }); + + try { + await de.run(block); + + } catch (err) {} + + const call00 = actionBarSpy.mock.calls[ 0 ][ 0 ]; + expect(de.isError(call00)).toBe(true); + expect(call00.error.id).toBe(de.ERROR_ID.REQUIRED_BLOCK_FAILED); + expect(call00.error.reason).toBe(errorFoo); + }); + + it('nested subblock cancels all', async() => { + + let cancelReason; + + const block = de.object({ + block: { + foo: de.object({ + block: { + bar: getResultBlock().extend({ + options: { + after: ({ cancel }) => { + cancelReason = de.error('ERROR'); + cancel.cancel(cancelReason); + }, + }, + }), + }, + }), + }, + }); + + expect.assertions(1); + let e; + try { + await de.run(block); + + } catch (error) { + e = error; + } + + expect(e).toBe(cancelReason); + }); + + }); + + describe('inheritance', () => { + + it('two subblocks', async() => { + const dataFoo = { + foo: 42, + }; + const blockFoo = getResultBlock(dataFoo, getTimeout(50, 100)); + + const dataBar = { + bar: 24, + }; + const blockBar = getResultBlock(dataBar, getTimeout(50, 100)); + + const parent = de.object({ + block: { + foo: blockFoo, + bar: blockBar, + }, + }); + const child = parent.extend({ + options: {}, + }); + + const result = await de.run(child); + + expect(result).toEqual({ + foo: dataFoo, + bar: dataBar, + }); + expect(result.foo).toBe(dataFoo); + expect(result.bar).toBe(dataBar); + }); + + }); + +}); diff --git a/tests/object_block.test.js b/tests/object_block.test.js deleted file mode 100644 index 1a1e4d4..0000000 --- a/tests/object_block.test.js +++ /dev/null @@ -1,421 +0,0 @@ -const de = require( '../lib' ); - -const { - // wait_for_value, - // wait_for_error, - get_result_block, - get_error_block, - get_timeout, -} = require( './helpers' ); - -// --------------------------------------------------------------------------------------------------------------- // - -describe( 'de.object', () => { - - it( 'block is undefined #1', () => { - expect.assertions( 2 ); - try { - de.object( { - block: undefined, - } ); - - } catch ( e ) { - expect( de.is_error( e ) ).toBe( true ); - expect( e.error.id ).toBe( de.ERROR_ID.INVALID_BLOCK ); - } - } ); - - it( 'block is undefined #2', () => { - expect.assertions( 2 ); - try { - de.object(); - - } catch ( e ) { - expect( de.is_error( e ) ).toBe( true ); - expect( e.error.id ).toBe( de.ERROR_ID.INVALID_BLOCK ); - } - } ); - - it( 'empty object', async () => { - const block = de.object( { - block: {}, - } ); - - const result = await de.run( block ); - - expect( result ).toEqual( {} ); - } ); - - it( 'two subblocks', async () => { - const data_foo = { - foo: 42, - }; - const block_foo = get_result_block( data_foo, get_timeout( 50, 100 ) ); - - const data_bar = { - bar: 24, - }; - const block_bar = get_result_block( data_bar, get_timeout( 50, 100 ) ); - - const block = de.object( { - block: { - foo: block_foo, - bar: block_bar, - }, - } ); - - const result = await de.run( block ); - - expect( result ).toEqual( { - foo: data_foo, - bar: data_bar, - } ); - expect( result.foo ).toBe( data_foo ); - expect( result.bar ).toBe( data_bar ); - } ); - - it( 'two subblocks, one required', async () => { - const data_foo = { - foo: 42, - }; - const block_foo = get_result_block( data_foo, get_timeout( 50, 100 ) ); - - const data_bar = { - bar: 24, - }; - const block_bar = get_result_block( data_bar, get_timeout( 50, 100 ) ); - - const block = de.object( { - block: { - foo: block_foo( { - options: { - required: true, - }, - } ), - bar: block_bar, - }, - } ); - - const result = await de.run( block ); - - expect( result ).toEqual( { - foo: data_foo, - bar: data_bar, - } ); - expect( result.foo ).toBe( data_foo ); - expect( result.bar ).toBe( data_bar ); - } ); - - it( 'two subblocks, one failed', async () => { - const error_foo = de.error( { - id: 'SOME_ERROR', - } ); - const block_foo = get_error_block( error_foo, get_timeout( 50, 100 ) ); - - const data_bar = { - bar: 24, - }; - const block_bar = get_result_block( data_bar, get_timeout( 50, 100 ) ); - - const block = de.object( { - block: { - foo: block_foo, - bar: block_bar, - }, - } ); - - const result = await de.run( block ); - - expect( result.foo ).toBe( error_foo ); - expect( result.bar ).toBe( data_bar ); - } ); - - it( 'two subblocks, both failed', async () => { - const error_foo = de.error( { - id: 'SOME_ERROR_1', - } ); - const block_foo = get_error_block( error_foo, get_timeout( 50, 100 ) ); - - const error_bar = de.error( { - id: 'SOME_ERROR_2', - } ); - const block_bar = get_error_block( error_bar, get_timeout( 50, 100 ) ); - - const block = de.object( { - block: { - foo: block_foo, - bar: block_bar, - }, - } ); - - const result = await de.run( block ); - - expect( result.foo ).toBe( error_foo ); - expect( result.bar ).toBe( error_bar ); - } ); - - it( 'two subblocks, one required failed #1', async () => { - const error_foo = de.error( { - id: 'SOME_ERROR', - } ); - const block_foo = get_error_block( error_foo, get_timeout( 50, 100 ) ); - const block_bar = get_result_block( null, get_timeout( 50, 100 ) ); - - const block = de.object( { - block: { - foo: block_foo( { - options: { - required: true, - }, - } ), - bar: block_bar, - }, - } ); - - expect.assertions( 4 ); - try { - await de.run( block ); - - } catch ( e ) { - expect( de.is_error( e ) ).toBe( true ); - expect( e.error.id ).toBe( de.ERROR_ID.REQUIRED_BLOCK_FAILED ); - expect( e.error.reason ).toBe( error_foo ); - expect( e.error.path ).toBe( '.foo' ); - } - } ); - - it( 'two subblocks, one required failed #2', async () => { - const error_foo = de.error( { - id: 'SOME_ERROR', - } ); - const block_foo = get_error_block( error_foo, get_timeout( 50, 100 ) ); - const block_bar = get_result_block( null, get_timeout( 50, 100 ) ); - const block_quu = get_result_block( null, get_timeout( 50, 100 ) ); - - const block = de.object( { - block: { - foo: de.array( { - block: [ - block_foo( { - options: { - required: true, - }, - } ), - block_bar, - ], - options: { - required: true, - }, - } ), - quu: block_quu, - }, - } ); - - expect.assertions( 3 ); - try { - await de.run( block ); - - } catch ( e ) { - expect( de.is_error( e ) ).toBe( true ); - expect( e.error.id ).toBe( de.ERROR_ID.REQUIRED_BLOCK_FAILED ); - expect( e.error.path ).toBe( '.foo[ 0 ]' ); - } - } ); - - it( 'order of keys', async () => { - const data_foo = { - foo: 42, - }; - const block_foo = get_result_block( data_foo, 100 ); - - const data_bar = { - bar: 24, - }; - const block_bar = get_result_block( data_bar, 50 ); - - const block = de.object( { - block: { - foo: block_foo, - bar: block_bar, - }, - } ); - - const result = await de.run( block ); - - expect( Object.keys( result ) ).toEqual( [ 'foo', 'bar' ] ); - } ); - - describe( 'cancel', () => { - - it( 'cancel object, subblocks cancelled too #1', async () => { - const action_foo_spy = jest.fn(); - const block_foo = get_result_block( null, 150, { - on_cancel: action_foo_spy, - } ); - - const action_bar_spy = jest.fn(); - const block_bar = get_result_block( null, 150, { - on_cancel: action_bar_spy, - } ); - - const block = de.object( { - block: { - foo: block_foo, - bar: block_bar, - }, - } ); - - const abort_error = de.error( { - id: 'SOME_ERROR', - } ); - try { - const cancel = new de.Cancel(); - setTimeout( () => { - cancel.cancel( abort_error ); - }, 100 ); - await de.run( block ); - - } catch ( e ) { - expect( e ).toBe( abort_error ); - expect( action_foo_spy.mock.calls[ 0 ][ 0 ] ).toBe( abort_error ); - expect( action_bar_spy.mock.calls[ 0 ][ 0 ] ).toBe( abort_error ); - } - } ); - - it( 'cancel object, subblocks cancelled too #2', async () => { - const action_foo_spy = jest.fn(); - const block_foo = get_result_block( null, 50, { - on_cancel: action_foo_spy, - } ); - - const action_bar_spy = jest.fn(); - const block_bar = get_result_block( null, 150, { - on_cancel: action_bar_spy, - } ); - - const block = de.object( { - block: { - foo: block_foo, - bar: block_bar, - }, - } ); - - const abort_error = de.error( { - id: 'SOME_ERROR', - } ); - try { - const cancel = new de.Cancel(); - setTimeout( () => { - cancel.cancel( abort_error ); - }, 100 ); - await de.run( block ); - - } catch ( e ) { - expect( e ).toBe( abort_error ); - expect( action_foo_spy.mock.calls ).toHaveLength( 0 ); - expect( action_bar_spy.mock.calls[ 0 ][ 0 ] ).toBe( abort_error ); - } - } ); - - it( 'required block failed, other subblocks cancelled', async () => { - const error_foo = de.error( { - id: 'SOME_ERROR', - } ); - const block_foo = get_error_block( error_foo, 50 ); - - const action_bar_spy = jest.fn(); - const block_bar = get_result_block( null, 150, { - on_cancel: action_bar_spy, - } ); - - const block = de.object( { - block: { - foo: block_foo( { - options: { - required: true, - }, - } ), - bar: block_bar, - }, - } ); - - try { - await de.run( block ); - - } catch ( e ) { - const call_0_0 = action_bar_spy.mock.calls[ 0 ][ 0 ]; - expect( de.is_error( call_0_0 ) ).toBe( true ); - expect( call_0_0.error.id ).toBe( de.ERROR_ID.REQUIRED_BLOCK_FAILED ); - expect( call_0_0.error.reason ).toBe( error_foo ); - } - } ); - - it( 'nested subblock cancels all', async () => { - - let cancel_reason; - - const block = de.object( { - block: { - foo: de.object( { - block: { - bar: get_result_block()( { - options: { - after: ( { cancel } ) => { - cancel_reason = de.error( { - id: 'ERROR', - } ); - cancel.cancel( cancel_reason ); - }, - }, - } ), - }, - } ), - }, - } ); - - expect.assertions( 1 ); - try { - await de.run( block ); - - } catch ( error ) { - expect( error ).toBe( cancel_reason ); - } - } ); - - } ); - - describe( 'inheritance', () => { - - it( 'two subblocks', async () => { - const data_foo = { - foo: 42, - }; - const block_foo = get_result_block( data_foo, get_timeout( 50, 100 ) ); - - const data_bar = { - bar: 24, - }; - const block_bar = get_result_block( data_bar, get_timeout( 50, 100 ) ); - - const parent = de.object( { - block: { - foo: block_foo, - bar: block_bar, - }, - } ); - const child = parent(); - - const result = await de.run( child ); - - expect( result ).toEqual( { - foo: data_foo, - bar: data_bar, - } ); - expect( result.foo ).toBe( data_foo ); - expect( result.bar ).toBe( data_bar ); - } ); - - } ); - -} ); - diff --git a/tests/options.after.test.js b/tests/options.after.test.js deleted file mode 100644 index 602430e..0000000 --- a/tests/options.after.test.js +++ /dev/null @@ -1,399 +0,0 @@ -const de = require( '../lib' ); - -const { - wait_for_value, - wait_for_error, - get_result_block, - get_error_block, -} = require( './helpers' ); - -describe( 'options.after', () => { - - it( 'after gets { params, context, result }', async () => { - const spy = jest.fn(); - const block_result = { - foo: 42, - }; - const block = get_result_block( block_result )( { - options: { - after: spy, - }, - } ); - - const params = { - bar: 24, - }; - const context = { - context: true, - }; - await de.run( block, { params, context } ); - - const calls = spy.mock.calls; - expect( calls[ 0 ][ 0 ].params ).toBe( params ); - expect( calls[ 0 ][ 0 ].context ).toBe( context ); - expect( calls[ 0 ][ 0 ].result ).toBe( block_result ); - } ); - - it( 'after never called if block errors', async () => { - const block_error = de.error( { - id: 'ERROR', - } ); - const spy = jest.fn(); - const block = get_error_block( block_error, 50 )( { - options: { - after: spy, - }, - } ); - - try { - await de.run( block ); - - } catch ( e ) { - expect( spy.mock.calls ).toHaveLength( 0 ); - } - } ); - - it.each( [ null, false, 0, '', 42, 'foo', {}, undefined ] )( 'after returns %j', async ( after_result ) => { - const block_result = { - foo: 42, - }; - const spy = jest.fn( () => after_result ); - const block = get_result_block( block_result )( { - options: { - after: spy, - }, - } ); - - const result = await de.run( block ); - - expect( result ).toBe( after_result ); - expect( spy.mock.calls ).toHaveLength( 1 ); - } ); - - it( 'after throws', async () => { - const after_error = de.error( { - id: 'SOME_ERROR', - } ); - const block = get_result_block( null )( { - options: { - after: () => { - throw after_error; - }, - }, - } ); - - expect.assertions( 1 ); - try { - await de.run( block ); - - } catch ( e ) { - expect( e ).toBe( after_error ); - } - } ); - - it( 'after throws, error returns value', async () => { - let error_result; - const spy_error = jest.fn( () => { - error_result = { - bar: 24, - }; - return error_result; - } ); - - let after_error; - const block = get_result_block( null, 50 )( { - options: { - after: () => { - after_error = de.error( { - id: 'ERROR', - } ); - throw after_error; - }, - error: spy_error, - }, - } ); - - const result = await de.run( block ); - - expect( spy_error.mock.calls[ 0 ][ 0 ].error ).toBe( after_error ); - expect( result ).toBe( error_result ); - } ); - - it( 'after returns error', async () => { - const after_error = de.error( { - id: 'AFTER_ERROR', - } ); - const block = get_result_block( null )( { - options: { - after: () => after_error, - }, - } ); - - const result = await de.run( block ); - - expect( result ).toBe( after_error ); - } ); - - it( 'after returns promise that resolves', async () => { - const after_result = { - foo: 42, - }; - const block = get_result_block( null )( { - options: { - after: () => wait_for_value( after_result, 50 ), - }, - } ); - - const result = await de.run( block ); - - expect( result ).toBe( after_result ); - } ); - - it( 'after returns promise that rejects', async () => { - const after_error = de.error( { - id: 'SOME_ERROR', - } ); - const block = get_result_block( null )( { - options: { - after: () => wait_for_error( after_error, 50 ), - }, - } ); - - expect.assertions( 1 ); - try { - await de.run( block ); - - } catch ( e ) { - expect( e ).toBe( after_error ); - } - } ); - - it( 'after returns promise that rejects, block has deps', async () => { - let bar_result; - - const block = de.func( { - block: ( { generate_id } ) => { - const id = generate_id(); - - return de.object( { - block: { - foo: get_result_block( null, 50 )( { - options: { - id: id, - }, - } ), - - bar: get_result_block( null, 50 )( { - options: { - deps: id, - after: () => { - return new Promise( ( resolve ) => { - setTimeout( () => { - bar_result = { - bar: 24, - }; - resolve( bar_result ); - }, 50 ); - } ); - }, - }, - } ), - }, - } ); - }, - } ); - - const r = await de.run( block ); - - expect( r.bar ).toBe( bar_result ); - } ); - - it( 'after returns recursive block', async () => { - const factorial = de.func( { - block: () => 1, - options: { - after: ( { params } ) => { - if ( params.n === 1 ) { - return { - r: params.r, - }; - - } else { - return factorial( { - options: { - params: ( { params } ) => { - return { - n: params.n - 1, - r: ( params.r || 1 ) * params.n, - }; - }, - }, - } ); - } - }, - }, - } ); - - const params = { - n: 5, - }; - const result = await de.run( factorial, { params } ); - expect( result.r ).toBe( 120 ); - } ); - - it( 'cancelled during after', async () => { - const error = de.error( { - id: 'ERROR', - } ); - const spy = jest.fn( () => wait_for_value( null, 100 ) ); - const block = get_result_block( null )( { - options: { - after: spy, - }, - } ); - const cancel = new de.Cancel(); - setTimeout( () => { - cancel.cancel( error ); - }, 50 ); - - expect.assertions( 2 ); - try { - await de.run( block, { cancel } ); - - } catch ( e ) { - expect( e ).toBe( error ); - expect( spy.mock.calls ).toHaveLength( 1 ); - } - - } ); - - describe( 'inheritance', () => { - - it( 'parent\'s first, child\'s second', async () => { - const spy = jest.fn(); - const parent = get_result_block( null )( { - options: { - after: () => spy( 'PARENT' ), - }, - } ); - const child = parent( { - options: { - after: () => spy( 'CHILD' ), - }, - } ); - - await de.run( child ); - - const calls = spy.mock.calls; - expect( calls ).toHaveLength( 2 ); - expect( calls[ 0 ][ 0 ] ).toBe( 'PARENT' ); - expect( calls[ 1 ][ 0 ] ).toBe( 'CHILD' ); - } ); - - it( 'parent throws, child never called', async () => { - const spy = jest.fn(); - const parent_after_error = de.error( { - id: 'SOME_ERROR', - } ); - const parent = get_result_block( null )( { - options: { - after: () => { - throw parent_after_error; - }, - }, - } ); - const child = parent( { - options: { - after: spy, - }, - } ); - - expect.assertions( 2 ); - try { - await de.run( child ); - - } catch ( e ) { - expect( e ).toBe( parent_after_error ); - expect( spy.mock.calls ).toHaveLength( 0 ); - } - } ); - - it( 'child throws', async () => { - const parent_after_result = { - foo: 42, - }; - const child_after_error = de.error( { - id: 'SOME_ERROR', - } ); - const parent = get_result_block( null )( { - options: { - after: () => parent_after_result, - }, - } ); - const child = parent( { - options: { - after: () => { - throw child_after_error; - }, - }, - } ); - - expect.assertions( 1 ); - try { - await de.run( child ); - - } catch ( e ) { - expect( e ).toBe( child_after_error ); - } - } ); - - it.each( [ null, false, 0, '', 42, 'foo', {}, undefined ] )( 'parent returns %j, child gets parent\'s result in { result }', async ( value ) => { - const spy = jest.fn( () => value ); - const parent_after_result = { - foo: 42, - }; - const parent = get_result_block( null )( { - options: { - after: () => parent_after_result, - }, - } ); - const child = parent( { - options: { - after: spy, - }, - } ); - - const result = await de.run( child ); - - expect( result ).toBe( value ); - const calls = spy.mock.calls; - expect( calls[ 0 ][ 0 ].result ).toBe( parent_after_result ); - } ); - - it.each( [ null, false, 0, '', 42, 'foo', {} ] )( 'child returns %j', async ( child_after_result ) => { - const block_result = { - foo: 42, - }; - const parent_after_result = { - bar: 24, - }; - const parent = get_result_block( block_result )( { - options: { - after: () => parent_after_result, - }, - } ); - const child = parent( { - options: { - after: () => child_after_result, - }, - } ); - - const result = await de.run( child ); - - expect( result ).toBe( child_after_result ); - } ); - - } ); - -} ); - diff --git a/tests/options.after.test.ts b/tests/options.after.test.ts new file mode 100644 index 0000000..625062a --- /dev/null +++ b/tests/options.after.test.ts @@ -0,0 +1,397 @@ +import * as de from '../lib'; + +import { getErrorBlock, getResultBlock, waitForError, waitForValue } from './helpers'; + + +describe('options.after', () => { + + it('after gets { params, context, result }', async() => { + const spy = jest.fn(); + const blockResult = { + foo: 42, + }; + const block = getResultBlock(blockResult).extend({ + options: { + after: spy, + }, + }); + + const params = { + bar: 24, + }; + const context = { + context: true, + }; + await de.run(block, { params, context }); + + const calls = spy.mock.calls; + expect(calls[ 0 ][ 0 ].params).toBe(params); + expect(calls[ 0 ][ 0 ].context).toBe(context); + expect(calls[ 0 ][ 0 ].result).toBe(blockResult); + }); + + it('after never called if block errors', async() => { + const blockError = de.error('ERROR'); + const spy = jest.fn(); + + const block = getErrorBlock(blockError, 50).extend({ + options: { + after: spy, + }, + }); + + try { + await de.run(block); + } catch (e) { + } + + expect(spy.mock.calls).toHaveLength(0); + }); + + it.each([ null, false, 0, '', 42, 'foo', {}, undefined ])('after returns %j', async(afterResult) => { + const blockResult = { + foo: 42, + }; + const spy = jest.fn(() => afterResult); + const block = getResultBlock(blockResult).extend({ + options: { + after: spy, + }, + }); + + const result = await de.run(block); + + expect(result).toBe(afterResult); + expect(spy.mock.calls).toHaveLength(1); + }); + + it('after throws', async() => { + const afterError = de.error('SOME_ERROR'); + const block = getResultBlock(null).extend({ + options: { + after: () => { + throw afterError; + }, + }, + }); + + expect.assertions(1); + let e; + try { + await de.run(block); + + } catch (err) { + e = err; + } + + expect(e).toBe(afterError); + }); + + it('after throws, error returns value', async() => { + let errorResult; + const spyError = jest.fn(() => { + errorResult = { + bar: 24, + }; + return errorResult; + }); + + let afterError; + const block = getResultBlock(null, 50).extend({ + options: { + after: () => { + afterError = de.error('ERROR'); + throw afterError; + }, + error: spyError, + }, + }); + + const result = await de.run(block); + + expect(spyError.mock.calls[ 0 ][ 0 ].error).toBe(afterError); + expect(result).toBe(errorResult); + }); + + it('after returns error', async() => { + const afterError = de.error('AFTER_ERROR'); + const block = getResultBlock(null).extend({ + options: { + after: () => afterError, + }, + }); + + const result = await de.run(block); + + expect(result).toBe(afterError); + }); + + it('after returns promise that resolves', async() => { + const afterResult = { + foo: 42, + }; + const block = getResultBlock(null).extend({ + options: { + after: () => waitForValue(afterResult, 50), + }, + }); + + const result = await de.run(block); + + expect(result).toBe(afterResult); + }); + + it('after returns promise that rejects', async() => { + const afterError = de.error('SOME_ERROR'); + const block = getResultBlock(null).extend({ + options: { + after: () => waitForError(afterError, 50), + }, + }); + + expect.assertions(1); + let e; + try { + await de.run(block); + + } catch (err) { + e = err; + } + expect(e).toBe(afterError); + }); + + it('after returns promise that rejects, block has deps', async() => { + let barResult; + + const block = de.func({ + block: ({ generateId }) => { + const id = generateId(); + + return de.object({ + block: { + foo: getResultBlock(null, 50).extend({ + options: { + id: id, + }, + }), + + bar: getResultBlock(null, 50).extend({ + options: { + deps: id, + after: () => { + return new Promise((resolve) => { + setTimeout(() => { + barResult = { + bar: 24, + }; + resolve(barResult); + }, 50); + }); + }, + }, + }), + }, + }); + }, + }); + + //TODO кривые результаты + const r = await de.run(block); + + expect(r.bar).toBe(barResult); + }); + + it('after returns recursive block', async() => { + type Params = { + n: number; + r: number; + } + const factorial: any = de.func({ + block: () => 1, + options: { + after: ({ params }: { params: Params }) => { + if (params.n === 1) { + return { + r: params.r, + }; + + } else { + return factorial.extend({ + options: { + params: ({ params }: { params: Params }) => { + return { + n: params.n - 1, + r: (params.r || 1) * params.n, + }; + }, + }, + }); + } + }, + }, + }); + + const params = { + n: 5, + }; + + const result: any = await de.run(factorial, { params }); + expect(result.r).toBe(120); + }); + + it('cancelled during after', async() => { + const error = de.error('ERROR'); + const spy = jest.fn(() => waitForValue(null, 100)); + const block = getResultBlock(null).extend({ + options: { + after: spy, + }, + }); + const cancel = new de.Cancel(); + setTimeout(() => { + cancel.cancel(error); + }, 50); + + expect.assertions(2); + let e; + try { + await de.run(block, { cancel }); + + } catch (err) { + e = err; + } + + expect(e).toBe(error); + expect(spy.mock.calls).toHaveLength(1); + + }); + + describe('inheritance', () => { + + it('parent\'s first, child\'s second', async() => { + const spy = jest.fn(); + const parent = getResultBlock(null).extend({ + options: { + after: () => spy('PARENT'), + }, + }); + const child = parent.extend({ + options: { + after: () => spy('CHILD'), + }, + }); + + await de.run(child); + + const calls = spy.mock.calls; + expect(calls).toHaveLength(2); + expect(calls[ 0 ][ 0 ]).toBe('PARENT'); + expect(calls[ 1 ][ 0 ]).toBe('CHILD'); + }); + + it('parent throws, child never called', async() => { + const spy = jest.fn(); + const parentAfterError = de.error('SOME_ERROR'); + const parent = getResultBlock(null).extend({ + options: { + after: () => { + throw parentAfterError; + }, + }, + }); + const child = parent.extend({ + options: { + after: spy, + }, + }); + + expect.assertions(2); + let e; + try { + await de.run(child); + + } catch (err) { + e = err; + } + expect(e).toBe(parentAfterError); + expect(spy.mock.calls).toHaveLength(0); + }); + + it('child throws', async() => { + const parentAfterResult = { + foo: 42, + }; + const childAfterError = de.error('SOME_ERROR'); + const parent = getResultBlock(null).extend({ + options: { + after: () => parentAfterResult, + }, + }); + const child = parent.extend({ + options: { + after: () => { + throw childAfterError; + }, + }, + }); + + expect.assertions(1); + let e; + try { + await de.run(child); + + } catch (err) { + e = err; + } + expect(e).toBe(childAfterError); + }); + + it.each([ null, false, 0, '', 42, 'foo', {}, undefined ])('parent returns %j, child gets parent\'s result in { result }', async(value) => { + const spy = jest.fn(() => value); + const parentAfterResult = { + foo: 42, + }; + const parent = getResultBlock(null).extend({ + options: { + after: () => parentAfterResult, + }, + }); + const child = parent.extend({ + options: { + after: spy, + }, + }); + + const result = await de.run(child); + + expect(result).toBe(value); + const calls = spy.mock.calls; + expect(calls[ 0 ][ 0 ].result).toBe(parentAfterResult); + }); + + it.each([ null, false, 0, '', 42, 'foo', {} ])('child returns %j', async(childAfterResult) => { + const blockResult = { + foo: 42, + }; + const parentAfterResult = { + bar: 24, + }; + const parent = getResultBlock(blockResult).extend({ + options: { + after: () => parentAfterResult, + }, + }); + const child = parent.extend({ + options: { + after: () => childAfterResult, + }, + }); + + const result = await de.run(child); + + expect(result).toBe(childAfterResult); + }); + + }); + +}); diff --git a/tests/options.before.test.js b/tests/options.before.test.js deleted file mode 100644 index 0c9b6e2..0000000 --- a/tests/options.before.test.js +++ /dev/null @@ -1,331 +0,0 @@ -const de = require( '../lib' ); - -const { - wait_for_value, - wait_for_error, - get_result_block, -} = require( './helpers' ); - -describe( 'options.before', () => { - - it( 'before gets { params, context }', async () => { - const spy = jest.fn(); - const block = get_result_block( null )( { - options: { - before: spy, - }, - } ); - - const params = { - foo: 42, - }; - const context = { - context: true, - }; - await de.run( block, { params, context } ); - - const calls = spy.mock.calls; - expect( calls[ 0 ][ 0 ].params ).toBe( params ); - expect( calls[ 0 ][ 0 ].context ).toBe( context ); - } ); - - it.each( [ null, false, 0, '', 42, 'foo', {} ] )( 'before returns %j, action never called', async ( before_result ) => { - const spy = jest.fn(); - const block = get_result_block( spy )( { - options: { - before: () => before_result, - }, - } ); - - const result = await de.run( block ); - - expect( result ).toBe( before_result ); - expect( spy.mock.calls ).toHaveLength( 0 ); - } ); - - it( 'before throws, action never called', async () => { - const spy = jest.fn(); - const before_error = de.error( { - id: 'SOME_ERROR', - } ); - const block = get_result_block( spy )( { - options: { - before: () => { - throw before_error; - }, - }, - } ); - - expect.assertions( 2 ); - try { - await de.run( block ); - - } catch ( e ) { - expect( e ).toBe( before_error ); - expect( spy.mock.calls ).toHaveLength( 0 ); - } - } ); - - it( 'before returns promise that rejects, action never called', async () => { - const spy = jest.fn(); - const before_error = de.error( { - id: 'SOME_ERROR', - } ); - const block = get_result_block( spy )( { - options: { - before: () => wait_for_error( before_error, 50 ), - }, - } ); - - expect.assertions( 2 ); - try { - await de.run( block ); - - } catch ( e ) { - expect( e ).toBe( before_error ); - expect( spy.mock.calls ).toHaveLength( 0 ); - } - } ); - - it( 'before returns promise that resolves, block has deps', async () => { - let bar_result; - - const block = de.func( { - block: ( { generate_id } ) => { - const id = generate_id(); - - return de.object( { - block: { - foo: get_result_block( null, 50 )( { - options: { - id: id, - }, - } ), - - bar: get_result_block( null, 50 )( { - options: { - deps: id, - before: () => { - return new Promise( ( resolve ) => { - setTimeout( () => { - bar_result = { - bar: 24, - }; - resolve( bar_result ); - }, 50 ); - } ); - }, - }, - } ), - }, - } ); - }, - } ); - - const r = await de.run( block ); - - expect( r.bar ).toBe( bar_result ); - } ); - - it( 'before returns promise that resolves, action never called', async () => { - const spy = jest.fn(); - const before_result = { - foo: 42, - }; - const block = get_result_block( spy )( { - options: { - before: () => wait_for_value( before_result, 50 ), - }, - } ); - - const result = await de.run( block ); - - expect( result ).toBe( before_result ); - expect( spy.mock.calls ).toHaveLength( 0 ); - } ); - - it( 'before returns undefined', async () => { - const block_result = { - foo: 42, - }; - const block = get_result_block( block_result )( { - options: { - before: () => undefined, - }, - } ); - - const result = await de.run( block ); - - expect( result ).toBe( block_result ); - } ); - - it( 'before returns recursive block', async () => { - const factorial = de.func( { - block: () => 1, - options: { - before: ( { params } ) => { - if ( params.n === 1 ) { - return { - r: params.r, - }; - - } else { - return factorial( { - options: { - params: ( { params } ) => { - return { - n: params.n - 1, - r: ( params.r || 1 ) * params.n, - }; - }, - }, - } ); - } - }, - }, - } ); - - const params = { - n: 5, - }; - const result = await de.run( factorial, { params } ); - expect( result.r ).toBe( 120 ); - } ); - - describe( 'inheritance', () => { - - it( 'child\'s first, parent\'s second', async () => { - const spy = jest.fn(); - const parent = get_result_block( null )( { - options: { - before: () => spy( 'PARENT' ), - }, - } ); - const child = parent( { - options: { - before: () => spy( 'CHILD' ), - }, - } ); - - await de.run( child ); - - const calls = spy.mock.calls; - expect( calls ).toHaveLength( 2 ); - expect( calls[ 0 ][ 0 ] ).toBe( 'CHILD' ); - expect( calls[ 1 ][ 0 ] ).toBe( 'PARENT' ); - } ); - - it.each( [ null, false, 0, '', 42, 'foo', {} ] )( 'child returns %j, parent never called', async ( child_before_result ) => { - const spy = jest.fn(); - const parent = get_result_block( null )( { - options: { - before: spy, - }, - } ); - const child = parent( { - options: { - before: () => child_before_result, - }, - } ); - - const result = await de.run( child ); - - expect( result ).toBe( child_before_result ); - expect( spy.mock.calls ).toHaveLength( 0 ); - } ); - - it.each( [ null, false, 0, '', 42, 'foo', {} ] )( 'child returns undefined, parent returns %j', async ( parent_before_result ) => { - const parent = get_result_block( null )( { - options: { - before: () => parent_before_result, - }, - } ); - const child = parent( { - options: { - before: () => undefined, - }, - } ); - - const result = await de.run( child ); - - expect( result ).toBe( parent_before_result ); - } ); - - it( 'child returns undefined, parent returns undefined', async () => { - const block_result = { - foo: 42, - }; - const parent = get_result_block( block_result )( { - options: { - before: () => undefined, - }, - } ); - const child = parent( { - options: { - before: () => undefined, - }, - } ); - - const result = await de.run( child ); - - expect( result ).toBe( block_result ); - } ); - - it( 'child returns undefined, parent throws', async () => { - const parent_before_error = de.error( { - id: 'SOME_ERROR', - } ); - const parent = get_result_block( null )( { - options: { - before: () => { - throw parent_before_error; - }, - }, - } ); - const child = parent( { - options: { - before: () => undefined, - }, - } ); - - expect.assertions( 1 ); - try { - await de.run( child ); - - } catch ( e ) { - expect( e ).toBe( parent_before_error ); - } - } ); - - it( 'child throws, parent never called', async () => { - const spy = jest.fn(); - const child_before_error = de.error( { - id: 'SOME_ERROR', - } ); - const parent = get_result_block( null )( { - options: { - before: spy, - }, - } ); - const child = parent( { - options: { - before: () => { - throw child_before_error; - }, - }, - } ); - - expect.assertions( 2 ); - try { - await de.run( child ); - - } catch ( e ) { - expect( e ).toBe( child_before_error ); - expect( spy.mock.calls ).toHaveLength( 0 ); - } - } ); - - } ); - -} ); - diff --git a/tests/options.before.test.ts b/tests/options.before.test.ts new file mode 100644 index 0000000..f1e79db --- /dev/null +++ b/tests/options.before.test.ts @@ -0,0 +1,333 @@ +import { getResultBlock, waitForError, waitForValue } from './helpers'; + +import * as de from '../lib'; + +describe('options.before', () => { + + it('before gets { params, context }', async() => { + const spy = jest.fn(); + const block = getResultBlock(null).extend({ + options: { + before: spy, + }, + }); + + const params = { + foo: 42, + }; + const context = { + context: true, + }; + await de.run(block, { params, context }); + + const calls = spy.mock.calls; + expect(calls[ 0 ][ 0 ].params).toBe(params); + expect(calls[ 0 ][ 0 ].context).toBe(context); + }); + + [ null, false, 0, '', 42, 'foo', {} ].forEach((beforeResult) => { + it(`before returns ${ beforeResult }, action never called`, async() => { + const spy = jest.fn(); + const block = getResultBlock(spy).extend({ + options: { + before: () => beforeResult, + }, + }); + + const result = await de.run(block); + + expect(result).toBe(beforeResult); + expect(spy.mock.calls).toHaveLength(0); + }); + }); + + it('before throws, action never called', async() => { + const spy = jest.fn(); + const beforeError = de.error('SOME_ERROR'); + const block = getResultBlock(spy).extend({ + options: { + before: () => { + throw beforeError; + }, + }, + }); + + expect.assertions(2); + let e; + try { + await de.run(block); + + } catch (err) { + e = err; + } + + expect(e).toBe(beforeError); + expect(spy.mock.calls).toHaveLength(0); + }); + + it('before returns promise that rejects, action never called', async() => { + const spy = jest.fn(); + const beforeError = de.error('SOME_ERROR'); + const block = getResultBlock(spy).extend({ + options: { + before: () => waitForError(beforeError, 50), + }, + }); + + expect.assertions(2); + let e; + try { + await de.run(block); + + } catch (err) { + e = err; + } + expect(e).toBe(beforeError); + expect(spy.mock.calls).toHaveLength(0); + }); + + it('before returns promise that resolves, block has deps', async() => { + let barResult; + + const block = de.func({ + block: ({ generateId }) => { + const id = generateId(); + + return de.object({ + block: { + foo: getResultBlock(null, 50).extend({ + options: { + id: id, + }, + }), + + bar: getResultBlock(null, 50).extend({ + options: { + deps: id, + before: () => { + return new Promise((resolve) => { + setTimeout(() => { + barResult = { + bar: 24, + }; + resolve(barResult); + }, 50); + }); + }, + }, + }), + }, + }); + }, + }); + + const r = await de.run(block); + + expect(r.bar).toBe(barResult); + }); + + it('before returns promise that resolves, action never called', async() => { + const spy = jest.fn(); + const beforeResult = { + foo: 42, + }; + const block = getResultBlock(spy).extend({ + options: { + before: () => waitForValue(beforeResult, 50), + }, + }); + + const result = await de.run(block); + + expect(result).toBe(beforeResult); + expect(spy.mock.calls).toHaveLength(0); + }); + + it('before returns undefined', async() => { + const blockResult = { + foo: 42, + }; + const block = getResultBlock(blockResult).extend({ + options: { + before: () => undefined, + }, + }); + + const result = await de.run(block); + + expect(result).toBe(blockResult); + }); + + it('before returns recursive block', async() => { + type Params = { + n: number; + r: number; + } + const factorial: any = de.func({ + block: () => 1, + options: { + before: ({ params }: { params: Params}) => { + if (params.n === 1) { + return { + r: params.r, + }; + + } else { + return factorial.extend({ + options: { + params: ({ params }: { params: Params }) => { + return { + n: params.n - 1, + r: (params.r || 1) * params.n, + }; + }, + }, + }); + } + }, + }, + }); + + const params = { + n: 5, + }; + const result: any = await de.run(factorial, { params }); + expect(result.r).toBe(120); + }); + + describe('inheritance', () => { + + it('child\'s first, parent\'s second', async() => { + const spy = jest.fn(); + const parent = getResultBlock(null).extend({ + options: { + before: () => spy('PARENT'), + }, + }); + const child = parent.extend({ + options: { + before: () => spy('CHILD'), + }, + }); + + await de.run(child); + + const calls = spy.mock.calls; + expect(calls).toHaveLength(2); + expect(calls[ 0 ][ 0 ]).toBe('CHILD'); + expect(calls[ 1 ][ 0 ]).toBe('PARENT'); + }); + + it.each([ null, false, 0, '', 42, 'foo', {} ])('child returns %j, parent never called', async(childBeforeResult) => { + const spy = jest.fn(); + const parent = getResultBlock(null).extend({ + options: { + before: spy, + }, + }); + const child = parent.extend({ + options: { + before: () => childBeforeResult, + }, + }); + + const result = await de.run(child); + + expect(result).toBe(childBeforeResult); + expect(spy.mock.calls).toHaveLength(0); + }); + + it.each([ null, false, 0, '', 42, 'foo', {} ])('child returns undefined, parent returns %j', async(parentBeforeResult) => { + const parent = getResultBlock(null).extend({ + options: { + before: () => parentBeforeResult, + }, + }); + const child = parent.extend({ + options: { + before: () => undefined, + }, + }); + + const result = await de.run(child); + + expect(result).toBe(parentBeforeResult); + }); + + it('child returns undefined, parent returns undefined', async() => { + const blockResult = { + foo: 42, + }; + const parent = getResultBlock(blockResult).extend({ + options: { + before: () => undefined, + }, + }); + const child = parent.extend({ + options: { + before: () => undefined, + }, + }); + + const result = await de.run(child); + + expect(result).toBe(blockResult); + }); + + it('child returns undefined, parent throws', async() => { + const parentBeforeError = de.error('SOME_ERROR'); + const parent = getResultBlock(null).extend({ + options: { + before: () => { + throw parentBeforeError; + }, + }, + }); + const child = parent.extend({ + options: { + before: () => undefined, + }, + }); + + expect.assertions(1); + let e; + try { + await de.run(child); + + } catch (err) { + e = err; + } + expect(e).toBe(parentBeforeError); + }); + + it('child throws, parent never called', async() => { + const spy = jest.fn(); + const childBeforeError = de.error('SOME_ERROR'); + const parent = getResultBlock(null).extend({ + options: { + before: spy, + }, + }); + const child = parent.extend({ + options: { + before: () => { + throw childBeforeError; + }, + }, + }); + + expect.assertions(2); + let e; + try { + await de.run(child); + + } catch (err) { + e = err; + } + expect(e).toBe(childBeforeError); + expect(spy.mock.calls).toHaveLength(0); + }); + + }); + +}); diff --git a/tests/options.cache.test.js b/tests/options.cache.test.js deleted file mode 100644 index 7b25d33..0000000 --- a/tests/options.cache.test.js +++ /dev/null @@ -1,333 +0,0 @@ -const de = require( '../lib' ); - -const { - wait_for_value, - // wait_for_error, - get_result_block, - // get_error_block, - get_timeout, -} = require( './helpers' ); - -// --------------------------------------------------------------------------------------------------------------- // - -class Cache { - - constructor() { - this.cache = {}; - } - - get( { key, context } ) { - return new Promise( ( resolve ) => { - const timeout = get_timeout( 0, 10 ); - - setTimeout( () => { - const cached = this.cache[ key ]; - let value; - if ( cached && !( ( cached.maxage > 0 ) && ( Date.now() - cached.timestamp > cached.maxage ) ) ) { - value = cached.value; - } - resolve( value ); - }, timeout ); - } ); - } - - set( { key, value, maxage, context } ) { - return new Promise( ( resolve ) => { - const timeout = get_timeout( 0, 10 ); - setTimeout( () => { - this.cache[ key ] = { - timestamp: Date.now(), - maxage: maxage, - value: value, - }; - resolve(); - }, timeout ); - } ); - } - -} - -// --------------------------------------------------------------------------------------------------------------- // - -describe( 'options.cache, options.key, options.maxage', () => { - - it( 'key is garbage #1', async () => { - const cache = new Cache(); - - const block_value = Symbol(); - const spy = jest.fn( () => block_value ); - const block = get_result_block( spy, 50 )( { - options: { - cache: cache, - // Все, что не строка и не функция, должно игнорироваться. - key: 42, - maxage: 10000, - }, - } ); - - const result_1 = await de.run( block ); - await wait_for_value( null, 100 ); - const result_2 = await de.run( block ); - - expect( result_1 ).toBe( block_value ); - expect( result_2 ).toBe( block_value ); - expect( spy.mock.calls ).toHaveLength( 2 ); - - } ); - - it( 'key is garbage #2', async () => { - const cache = new Cache(); - - const block_value = Symbol(); - const spy = jest.fn( () => block_value ); - const block = get_result_block( spy, 50 )( { - options: { - cache: cache, - // Все, что не строка и не функция, должно игнорироваться. - key: () => 42, - maxage: 10000, - }, - } ); - - const result_1 = await de.run( block ); - await wait_for_value( null, 100 ); - const result_2 = await de.run( block ); - - expect( result_1 ).toBe( block_value ); - expect( result_2 ).toBe( block_value ); - expect( spy.mock.calls ).toHaveLength( 2 ); - - } ); - - it( 'key is a function, second run from cache', async () => { - const cache = new Cache(); - - const block_value = Symbol(); - const spy = jest.fn( () => block_value ); - const key = 'KEY'; - const block = get_result_block( spy, 50 )( { - options: { - cache: cache, - key: () => key, - maxage: 10000, - }, - } ); - - const result_1 = await de.run( block ); - - await wait_for_value( null, 100 ); - - const result_2 = await de.run( block ); - - expect( result_1 ).toBe( block_value ); - expect( result_2 ).toBe( block_value ); - expect( spy.mock.calls ).toHaveLength( 1 ); - } ); - - it( 'key is a function, cache expired, real second run', async () => { - const cache = new Cache(); - - const block_value = Symbol(); - const spy = jest.fn( () => block_value ); - const key = 'KEY'; - const block = get_result_block( spy, 50 )( { - options: { - cache: cache, - key: () => key, - maxage: 50, - }, - } ); - - await de.run( block ); - - await wait_for_value( null, 100 ); - - await de.run( block ); - - expect( spy.mock.calls ).toHaveLength( 2 ); - } ); - - it( 'key is a function and returns undefined', async () => { - const spy = jest.fn(); - const cache = { - get: spy, - }; - - const data = { - foo: 42, - }; - const block = get_result_block( data, 50 )( { - options: { - cache: cache, - key: () => undefined, - maxage: 100, - }, - } ); - - const result = await de.run( block ); - - expect( spy.mock.calls ).toHaveLength( 0 ); - expect( result ).toBe( data ); - } ); - - it( 'key is a string, second run from cache', async () => { - const cache = new Cache(); - - const block_value = Symbol(); - const spy = jest.fn( () => block_value ); - const block = get_result_block( spy, 50 )( { - options: { - cache: cache, - key: 'KEY', - maxage: 10000, - }, - } ); - - const result_1 = await de.run( block ); - - await wait_for_value( null, 100 ); - - const result_2 = await de.run( block ); - - expect( result_1 ).toBe( block_value ); - expect( result_2 ).toBe( block_value ); - expect( spy.mock.calls ).toHaveLength( 1 ); - } ); - - it( 'cache.get throws', async () => { - const cache = { - get: () => { - throw de.error( { - id: 'SOME_ERROR', - } ); - }, - set: () => undefined, - }; - const spy = jest.fn( () => null ); - const block = get_result_block( spy, 50 )( { - options: { - cache: cache, - key: 'KEY', - maxage: 10000, - }, - } ); - - await de.run( block ); - - expect( spy.mock.calls ).toHaveLength( 1 ); - } ); - - it( 'cache.get returns promise that rejects, block has deps', async () => { - const cache = { - get: () => { - return new Promise( ( resolve, reject ) => { - setTimeout( () => { - reject( de.error( { - id: 'SOME_ERROR', - } ) ); - }, 50 ); - } ); - }, - set: () => undefined, - }; - const spy = jest.fn( () => null ); - const block = de.func( { - block: ( { generate_id } ) => { - const id = generate_id(); - - return de.object( { - block: { - foo: get_result_block( null, 50 )( { - options: { - id: id, - }, - } ), - - bar: get_result_block( spy, 50 )( { - options: { - deps: id, - cache: cache, - key: 'KEY', - maxage: 10000, - }, - } ), - }, - } ); - }, - } ); - - const r = await de.run( block ); - - expect( r ).toEqual( { foo: null, bar: null } ); - expect( spy.mock.calls ).toHaveLength( 1 ); - } ); - - it( 'cache.set throws', async () => { - const cache = { - get: () => undefined, - set: () => { - throw de.error( { - id: 'SOME_ERROR', - } ); - }, - }; - const block = get_result_block( null, 50 )( { - options: { - cache: cache, - key: 'KEY', - maxage: 10000, - }, - } ); - - await expect( async () => { - await de.run( block ); - } ).not.toThrow(); - } ); - - it( 'cache.get returns rejected promise', async () => { - const cache = { - get: () => { - return Promise.reject( de.error( { - id: 'SOME_ERROR', - } ) ); - }, - set: () => undefined, - }; - const spy = jest.fn( () => null ); - const block = get_result_block( spy, 50 )( { - options: { - cache: cache, - key: 'KEY', - maxage: 10000, - }, - } ); - - await de.run( block ); - - expect( spy.mock.calls ).toHaveLength( 1 ); - } ); - - it( 'cache.set returns rejected promise', async () => { - const cache = { - get: () => undefined, - set: () => { - return Promise.reject( de.error( { - id: 'SOME_ERROR', - } ) ); - }, - }; - const block = get_result_block( null, 50 )( { - options: { - cache: cache, - key: 'KEY', - maxage: 10000, - }, - } ); - - await expect( async () => { - await de.run( block ); - } ).not.toThrow(); - } ); - -} ); - diff --git a/tests/options.cache.test.ts b/tests/options.cache.test.ts new file mode 100644 index 0000000..20e89bb --- /dev/null +++ b/tests/options.cache.test.ts @@ -0,0 +1,321 @@ +import * as de from '../lib'; + +import { getResultBlock, getTimeout, waitForValue } from './helpers'; +import CacheParent from '../lib/cache'; + + +// --------------------------------------------------------------------------------------------------------------- // +type CacheItem = { + expires: number; + maxage: number; + value: Result; + + timestamp: number; +} +class Cache extends CacheParent> { + + async get({ key }: { key: string }): Promise { + return new Promise((resolve) => { + const timeout = getTimeout(0, 10); + + setTimeout(() => { + const cached = this.cache[ key ]; + let value; + if (cached && !((cached.maxage > 0) && (Date.now() - cached.timestamp > cached.maxage))) { + value = cached.value; + } + resolve(value); + }, timeout); + }); + } + + async set({ key, value, maxage = 0 }: { key: string; value: Result; maxage?: number }) { + return new Promise((resolve) => { + const timeout = getTimeout(0, 10); + setTimeout(() => { + this.cache[ key ] = { + timestamp: Date.now(), + maxage: maxage, + value: value, + expires: 0, + }; + resolve(undefined); + }, timeout); + }); + } + +} + +// --------------------------------------------------------------------------------------------------------------- // + +describe('options.cache, options.key, options.maxage', () => { + + it('key is garbage #1', async() => { + const cache = new Cache(); + + const blockValue = Symbol(); + const spy = jest.fn(() => blockValue);// as () => typeof blockValue; + const block = getResultBlock(spy, 50).extend({ + options: { + cache: cache, + maxage: 10000, + }, + }); + + const result1 = await de.run(block); + await waitForValue(null, 100); + const result2 = await de.run(block); + + expect(result1).toBe(blockValue); + expect(result2).toBe(blockValue); + expect(spy.mock.calls).toHaveLength(2); + + }); + + it('key is garbage #2', async() => { + const cache = new Cache(); + + const blockValue = Symbol(); + const spy = jest.fn(() => blockValue); + const block = getResultBlock(spy, 50).extend({ + options: { + cache: cache, + maxage: 10000, + }, + }); + + const result1 = await de.run(block); + await waitForValue(null, 100); + const result2 = await de.run(block); + + expect(result1).toBe(blockValue); + expect(result2).toBe(blockValue); + expect(spy.mock.calls).toHaveLength(2); + + }); + + it('key is a function, second run from cache', async() => { + const cache = new Cache(); + + const blockValue = Symbol(); + const spy = jest.fn(() => blockValue); + const key = 'KEY'; + const block = getResultBlock(spy, 50).extend({ + options: { + cache: cache, + key: () => key, + maxage: 10000, + }, + }); + + const result1 = await de.run(block); + + await waitForValue(null, 100); + + const result2 = await de.run(block); + + expect(result1).toBe(blockValue); + expect(result2).toBe(blockValue); + expect(spy.mock.calls).toHaveLength(1); + }); + + it('key is a function, cache expired, real second run', async() => { + const cache = new Cache(); + + const blockValue = Symbol(); + const spy = jest.fn(() => blockValue); + const key = 'KEY'; + const block = getResultBlock(spy, 50).extend({ + options: { + cache: cache, + key: () => key, + maxage: 50, + }, + }); + + await de.run(block); + + await waitForValue(null, 100); + + await de.run(block); + + expect(spy.mock.calls).toHaveLength(2); + }); + + it('key is a function and returns undefined', async() => { + const spy = jest.fn(); + + const data = { + foo: 42, + }; + + const cache = { + get: spy, + } as unknown as Cache; + + const block = getResultBlock(data, 50).extend({ + options: { + cache: cache, + maxage: 100, + }, + }); + + const result = await de.run(block); + + expect(spy.mock.calls).toHaveLength(0); + expect(result).toBe(data); + }); + + it('key is a string, second run from cache', async() => { + const cache = new Cache(); + + const blockValue = Symbol(); + const spy = jest.fn(() => blockValue); + const block = getResultBlock(spy, 50).extend({ + options: { + cache: cache, + key: 'KEY', + maxage: 10000, + }, + }); + + const result1 = await de.run(block); + + await waitForValue(null, 100); + + const result2 = await de.run(block); + + expect(result1).toBe(blockValue); + expect(result2).toBe(blockValue); + expect(spy.mock.calls).toHaveLength(1); + }); + + it('cache.get throws', async() => { + const cache = { + get: () => { + throw de.error('SOME_ERROR'); + }, + set: () => undefined, + } as unknown as Cache; + + const spy = jest.fn(() => null); + const block = getResultBlock(spy, 50).extend({ + options: { + cache: cache, + key: 'KEY', + maxage: 10000, + }, + }); + + await de.run(block); + + expect(spy.mock.calls).toHaveLength(1); + }); + + it('cache.get returns promise that rejects, block has deps', async() => { + const cache = { + get: () => { + return new Promise((resolve, reject) => { + setTimeout(() => { + reject(de.error('SOME_ERROR')); + }, 50); + }); + }, + set: () => undefined, + } as unknown as Cache; + + const spy = jest.fn(() => null); + + const block = de.func({ + block: ({ generateId }) => { + const id = generateId(); + + return de.object({ + block: { + foo: getResultBlock(null, 50).extend({ + options: { + id: id, + }, + }), + + bar: getResultBlock(spy, 50).extend({ + options: { + deps: id, + cache: cache, + key: 'KEY', + maxage: 10000, + }, + }), + }, + }); + }, + }); + + const r = await de.run(block); + + expect(r).toEqual({ foo: null, bar: null }); + expect(spy.mock.calls).toHaveLength(1); + }); + + it('cache.set throws', async() => { + const cache = { + get: () => undefined, + set: () => { + throw de.error('SOME_ERROR'); + }, + } as unknown as Cache; + const block = getResultBlock(null, 50).extend({ + options: { + cache: cache, + key: 'KEY', + maxage: 10000, + }, + }); + + await expect(async() => { + await de.run(block); + }).not.toThrow(); + }); + + it('cache.get returns rejected promise', async() => { + const cache = { + get: () => { + return Promise.reject(de.error('SOME_ERROR')); + }, + set: () => undefined, + } as unknown as Cache; + const spy = jest.fn(() => null); + const block = getResultBlock(spy, 50).extend({ + options: { + cache: cache, + key: 'KEY', + maxage: 10000, + }, + }); + + await de.run(block); + + expect(spy.mock.calls).toHaveLength(1); + }); + + it('cache.set returns rejected promise', async() => { + const cache = { + get: () => undefined, + set: () => { + return Promise.reject(de.error('SOME_ERROR')); + }, + } as unknown as Cache; + const block = getResultBlock(null, 50).extend({ + options: { + cache: cache, + key: 'KEY', + maxage: 10000, + }, + }); + + await expect(async() => { + await de.run(block); + }).not.toThrow(); + }); + +}); diff --git a/tests/options.deps.test.js b/tests/options.deps.test.js deleted file mode 100644 index 1d3e30d..0000000 --- a/tests/options.deps.test.js +++ /dev/null @@ -1,960 +0,0 @@ -const de = require( '../lib' ); - -const { - get_timeout, - wait_for_value, - // wait_for_error, - get_result_block, - get_error_block, -} = require( './helpers' ); - -// --------------------------------------------------------------------------------------------------------------- // - -describe( 'options.deps', () => { - - it( 'block with id and without deps', async () => { - const data = { - foo: 42, - }; - const id = Symbol( 'block' ); - const block = get_result_block( data, 50 )( { - options: { - id: id, - }, - } ); - - const result = await de.run( block ); - - expect( result ).toBe( data ); - } ); - - it( 'no options.deps, deps is empty object', async () => { - const spy = jest.fn(); - const block = de.func( { - block: spy, - } ); - - await de.run( block ); - expect( spy.mock.calls[ 0 ][ 0 ].deps ).toEqual( {} ); - } ); - - it( 'empty deps', async () => { - const data = { - foo: 42, - }; - const block = get_result_block( data, 50 )( { - options: { - deps: [], - }, - } ); - - const result = await de.run( block ); - - expect( result ).toBe( data ); - } ); - - it( 'failed block with id and without deps', async () => { - const error = de.error( { - id: 'ERROR', - } ); - const id = Symbol( 'block' ); - const block = get_error_block( error, 50 )( { - options: { - id: id, - }, - } ); - - expect.assertions( 1 ); - try { - await de.run( block ); - - } catch ( e ) { - expect( e ).toBe( error ); - } - } ); - - it( 'block with invalid deps id #1', async () => { - const data = { - foo: 42, - }; - const id = Symbol( 'block' ); - const block = get_result_block( data, 50 )( { - options: { - deps: id, - }, - } ); - - expect.assertions( 2 ); - try { - await de.run( block ); - - } catch ( e ) { - expect( de.is_error( e ) ).toBe( true ); - expect( e.error.id ).toBe( de.ERROR_ID.INVALID_DEPS_ID ); - } - } ); - - it( 'block with invalid deps id #2', async () => { - const data = { - foo: 42, - }; - const block = de.func( { - block: () => { - const id = Symbol( 'block' ); - return get_result_block( data, 50 )( { - options: { - deps: id, - }, - } ); - }, - } ); - - expect.assertions( 2 ); - try { - await de.run( block ); - - } catch ( e ) { - expect( de.is_error( e ) ).toBe( true ); - expect( e.error.id ).toBe( de.ERROR_ID.INVALID_DEPS_ID ); - } - } ); - - it( 'block depends on block #1 (deps is id)', async () => { - const spy = jest.fn(); - - const block_foo = get_result_block( () => spy( 'FOO' ), 50 ); - const block_bar = get_result_block( () => spy( 'BAR' ), 50 ); - - const block = de.func( { - block: ( { generate_id } ) => { - const id_foo = generate_id(); - - return de.object( { - block: { - foo: block_foo( { - options: { - id: id_foo, - }, - } ), - bar: block_bar( { - options: { - deps: id_foo, - }, - } ), - }, - } ); - }, - } ); - - await de.run( block ); - - const calls = spy.mock.calls; - - expect( calls ).toHaveLength( 2 ); - expect( calls[ 0 ][ 0 ] ).toBe( 'FOO' ); - expect( calls[ 1 ][ 0 ] ).toBe( 'BAR' ); - } ); - - it( 'block depends on block #2 (deps is array)', async () => { - const spy = jest.fn(); - - const block_foo = get_result_block( () => spy( 'FOO' ), 50 ); - const block_bar = get_result_block( () => spy( 'BAR' ), 50 ); - - const block = de.func( { - block: ( { generate_id } ) => { - const id_foo = generate_id(); - - return de.object( { - block: { - foo: block_foo( { - options: { - id: id_foo, - }, - } ), - bar: block_bar( { - options: { - deps: [ id_foo ], - }, - } ), - }, - } ); - }, - } ); - - await de.run( block ); - - const calls = spy.mock.calls; - - expect( calls ).toHaveLength( 2 ); - expect( calls[ 0 ][ 0 ] ).toBe( 'FOO' ); - expect( calls[ 1 ][ 0 ] ).toBe( 'BAR' ); - } ); - - it( 'block depends on block depends on block', async () => { - const spy = jest.fn(); - - const block_foo = get_result_block( () => spy( 'FOO' ), 50 ); - const block_bar = get_result_block( () => spy( 'BAR' ), 50 ); - const block_quu = get_result_block( () => spy( 'QUU' ), 50 ); - - const block = de.func( { - block: ( { generate_id } ) => { - const id_foo = generate_id(); - const id_bar = generate_id(); - - return de.object( { - block: { - foo: block_foo( { - options: { - id: id_foo, - }, - } ), - bar: block_bar( { - options: { - id: id_bar, - deps: id_foo, - }, - } ), - quu: block_quu( { - options: { - deps: id_bar, - }, - } ), - }, - } ); - }, - } ); - - await de.run( block ); - - const calls = spy.mock.calls; - - expect( calls ).toHaveLength( 3 ); - expect( calls[ 0 ][ 0 ] ).toBe( 'FOO' ); - expect( calls[ 1 ][ 0 ] ).toBe( 'BAR' ); - expect( calls[ 2 ][ 0 ] ).toBe( 'QUU' ); - } ); - - it( 'block depends on two blocks', async () => { - const spy = jest.fn(); - - const block_foo = get_result_block( () => spy( 'FOO' ), get_timeout( 50, 100 ) ); - const block_bar = get_result_block( () => spy( 'BAR' ), get_timeout( 50, 100 ) ); - const block_quu = get_result_block( () => spy( 'QUU' ), 50 ); - - const block = de.func( { - block: ( { generate_id } ) => { - const id_foo = generate_id(); - const id_bar = generate_id(); - - return de.object( { - block: { - foo: block_foo( { - options: { - id: id_foo, - }, - } ), - bar: block_bar( { - options: { - id: id_bar, - }, - } ), - quu: block_quu( { - options: { - deps: [ id_foo, id_bar ], - }, - } ), - }, - } ); - }, - } ); - - await de.run( block ); - - const calls = spy.mock.calls; - - expect( calls ).toHaveLength( 3 ); - expect( calls[ 2 ][ 0 ] ).toBe( 'QUU' ); - } ); - - it( 'two block depend on block', async () => { - const spy = jest.fn(); - - const block_foo = get_result_block( () => spy( 'FOO' ), get_timeout( 50, 100 ) ); - const block_bar = get_result_block( () => spy( 'BAR' ), 50 ); - const block_quu = get_result_block( () => spy( 'QUU' ), 100 ); - - const block = de.func( { - block: ( { generate_id } ) => { - const id_foo = generate_id(); - - return de.object( { - block: { - foo: block_foo( { - options: { - id: id_foo, - }, - } ), - bar: block_bar( { - options: { - deps: id_foo, - }, - } ), - quu: block_quu( { - options: { - deps: id_foo, - }, - } ), - }, - } ); - }, - } ); - - await de.run( block ); - - const calls = spy.mock.calls; - - expect( calls ).toHaveLength( 3 ); - expect( calls[ 0 ][ 0 ] ).toBe( 'FOO' ); - expect( calls[ 1 ][ 0 ] ).toBe( 'BAR' ); - expect( calls[ 2 ][ 0 ] ).toBe( 'QUU' ); - } ); - - it( 'failed deps #1', async () => { - const error_foo = de.error( { - id: 'SOME_ERROR', - } ); - const block_foo = get_error_block( error_foo, 50 ); - - const body_bar = jest.fn(); - const block_bar = get_result_block( body_bar, 50 ); - - const block = de.func( { - block: ( { generate_id } ) => { - const id_foo = generate_id(); - - return de.object( { - block: { - foo: block_foo( { - options: { - id: id_foo, - }, - } ), - - bar: block_bar( { - options: { - deps: id_foo, - }, - } ), - }, - } ); - }, - } ); - - const result = await de.run( block ); - - expect( de.is_error( result.bar ) ).toBe( true ); - expect( result.bar.error.id ).toBe( de.ERROR_ID.DEPS_ERROR ); - expect( result.bar.error.reason ).toBe( error_foo ); - expect( body_bar ).toHaveBeenCalledTimes( 0 ); - } ); - - it( 'failed deps #2', async () => { - const error_foo = de.error( { - id: 'SOME_ERROR_1', - } ); - const block_foo = get_error_block( error_foo, 100 ); - - const error_bar = de.error( { - id: 'SOME_ERROR_2', - } ); - const block_bar = get_error_block( error_bar, 50 ); - - const body_quu = jest.fn(); - const block_quu = get_result_block( body_quu, 50 ); - - const block = de.func( { - block: ( { generate_id } ) => { - const id_foo = generate_id(); - const id_bar = generate_id(); - - return de.object( { - block: { - foo: block_foo( { - options: { - id: id_foo, - }, - } ), - - bar: block_bar( { - options: { - id: id_bar, - }, - } ), - - quu: block_quu( { - options: { - deps: [ id_foo, id_bar ], - }, - } ), - }, - } ); - }, - } ); - - const result = await de.run( block ); - - expect( de.is_error( result.quu ) ).toBe( true ); - expect( result.quu.error.id ).toBe( de.ERROR_ID.DEPS_ERROR ); - // block_bar падает за 50 мс, а block_foo за 100 мс. - // Поэтому в reason будет error_bar. - expect( result.quu.error.reason ).toBe( error_bar ); - expect( body_quu ).toHaveBeenCalledTimes( 0 ); - } ); - - it( 'deps not resolved #1', async () => { - const block_foo = get_result_block( null, 50 ); - const block_bar = get_result_block( null, 100 ); - - const block = de.func( { - block: ( { generate_id } ) => { - const id_foo = generate_id(); - - return de.object( { - block: { - foo: block_foo, - - bar: block_bar( { - options: { - deps: id_foo, - }, - } ), - }, - } ); - }, - } ); - - const result = await de.run( block ); - - expect( de.is_error( result.bar ) ).toBe( true ); - expect( result.bar.error.id ).toBe( de.ERROR_ID.DEPS_NOT_RESOLVED ); - } ); - - it( 'deps not resolved #2', async () => { - const block_foo = get_result_block( null, 50 ); - const block_bar = get_result_block( null, 100 ); - - const block = de.func( { - block: ( { generate_id } ) => { - const id_foo = generate_id(); - - return de.object( { - block: { - foo: block_foo, - - bar: block_bar( { - options: { - deps: id_foo, - }, - } ), - }, - } ); - }, - } ); - - const result = await de.run( block ); - - expect( de.is_error( result.bar ) ).toBe( true ); - expect( result.bar.error.id ).toBe( de.ERROR_ID.DEPS_NOT_RESOLVED ); - } ); - - it( 'one block with deps', async () => { - const block = de.func( { - block: ( { generate_id } ) => { - const id = generate_id(); - - return get_result_block( null, 50 )( { - options: { - deps: id, - }, - } ); - }, - } ); - - expect.assertions( 2 ); - try { - await de.run( block ); - - } catch ( e ) { - expect( de.is_error( e ) ).toBe( true ); - expect( e.error.id ).toBe( de.ERROR_ID.DEPS_NOT_RESOLVED ); - } - } ); - - it( 'block returns another block that depends on parent block', async () => { - const block = de.func( { - block: ( { generate_id } ) => { - const id = generate_id(); - - const child = get_result_block( null, 50 )( { - options: { - deps: id, - }, - } ); - - return get_result_block( child, 50 )( { - options: { - id: id, - }, - } ); - }, - } ); - - expect.assertions( 2 ); - try { - await de.run( block ); - - } catch ( e ) { - expect( de.is_error( e ) ).toBe( true ); - expect( e.error.id ).toBe( de.ERROR_ID.DEPS_NOT_RESOLVED ); - } - - - } ); - - it( 'before( { deps } ) has deps results #1', async () => { - const data_foo = { - foo: 42, - }; - const block_foo = get_result_block( data_foo, 50 ); - const block_bar = get_result_block( null, 50 ); - - const before_bar = jest.fn(); - - let id_foo; - const block = de.func( { - block: ( { generate_id } ) => { - id_foo = generate_id(); - - return de.object( { - block: { - foo: block_foo( { - options: { - id: id_foo, - }, - } ), - - bar: block_bar( { - options: { - deps: id_foo, - - before: before_bar, - }, - } ), - }, - } ); - }, - } ); - - await de.run( block ); - - const deps = before_bar.mock.calls[ 0 ][ 0 ].deps; - expect( deps[ id_foo ] ).toBe( data_foo ); - } ); - - it( 'before( { deps } ) has deps results #2', async () => { - const data_foo = { - foo: 42, - }; - const block_foo = get_result_block( data_foo, 300 ); - - const data_bar = { - bar: 24, - }; - const block_bar = get_result_block( data_bar, 200 ); - - const block_quu = get_result_block( null, 100 ); - - const before_quu = jest.fn(); - - let id_foo; - let id_bar; - const block = de.func( { - block: ( { generate_id } ) => { - id_foo = generate_id(); - id_bar = generate_id(); - - return de.object( { - block: { - foo: block_foo( { - options: { - id: id_foo, - }, - } ), - - bar: block_bar( { - options: { - id: id_bar, - }, - } ), - - quu: block_quu( { - options: { - deps: [ id_foo, id_bar ], - - before: before_quu, - }, - } ), - }, - } ); - }, - } ); - - await de.run( block ); - - const deps = before_quu.mock.calls[ 0 ][ 0 ].deps; - expect( deps[ id_foo ] ).toBe( data_foo ); - expect( deps[ id_bar ] ).toBe( data_bar ); - } ); - - it( 'before( { deps } ) has not results from other blocks', async () => { - const data_foo = { - foo: 42, - }; - const block_foo = get_result_block( data_foo, 50 ); - - const data_bar = { - bar: 24, - }; - const block_bar = get_result_block( data_bar, 100 ); - - const block_quu = get_result_block( null, 50 ); - - const before_quu = jest.fn(); - - let id_foo; - let id_bar; - const block = de.func( { - block: ( { generate_id } ) => { - id_foo = generate_id(); - id_bar = generate_id(); - - return de.object( { - block: { - foo: block_foo( { - options: { - id: id_foo, - }, - } ), - - bar: block_bar( { - options: { - id: id_bar, - }, - } ), - - quu: block_quu( { - options: { - deps: id_bar, - - before: before_quu, - }, - } ), - }, - } ); - }, - } ); - - await de.run( block ); - - const deps = before_quu.mock.calls[ 0 ][ 0 ].deps; - expect( deps[ id_foo ] ).toBe( undefined ); - expect( deps[ id_bar ] ).toBe( data_bar ); - } ); - - it( 'wait for result of de.func', async () => { - const before_quu = jest.fn(); - - let data_foo; - let id_foo; - - const block = de.func( { - block: ( { generate_id } ) => { - id_foo = generate_id(); - - data_foo = { - foo: 42, - }; - - return de.object( { - block: { - bar: get_result_block( () => { - return get_result_block( data_foo, 50 )( { - options: { - id: id_foo, - }, - } ); - }, 50 ), - - quu: get_result_block( null, 50 )( { - options: { - deps: id_foo, - - before: before_quu, - }, - } ), - }, - } ); - }, - } ); - - await de.run( block ); - - const deps = before_quu.mock.calls[ 0 ][ 0 ].deps; - expect( deps[ id_foo ] ).toBe( data_foo ); - } ); - - it( 'result of de.func has deps #1', async () => { - const block = de.func( { - block: ( { generate_id } ) => { - const id_foo = generate_id(); - - return de.object( { - block: { - foo: get_result_block( null, 50 )( { - options: { - id: id_foo, - }, - } ), - - bar: de.func( { - block: () => { - return new Promise( ( resolve ) => { - setTimeout( () => { - resolve( get_result_block( null, 50 )( { - options: { - deps: id_foo, - }, - } ) ); - }, 100 ); - } ); - }, - } ), - }, - } ); - }, - } ); - - const result = await de.run( block ); - - expect( result ).toEqual( { foo: null, bar: null } ); - - } ); - - it( 'result of de.func has deps #2', async () => { - let error_foo; - - const block = de.func( { - block: ( { generate_id } ) => { - const id_foo = generate_id(); - - return de.object( { - block: { - foo: de.func( { - block: async () => { - await wait_for_value( null, 50 ); - - error_foo = de.error( { - id: 'ERROR', - } ); - throw error_foo; - }, - options: { - id: id_foo, - }, - } ), - - bar: de.func( { - block: async () => { - await wait_for_value( null, 50 ); - - return get_result_block( null, 50 )( { - options: { - deps: id_foo, - }, - } ); - }, - } ), - }, - } ); - }, - } ); - - const result = await de.run( block ); - - expect( result.foo ).toBe( error_foo ); - expect( de.is_error( result.bar ) ).toBe( true ); - expect( result.bar.error.id ).toBe( de.ERROR_ID.DEPS_ERROR ); - expect( result.bar.error.reason ).toBe( error_foo ); - } ); - - it.each( [ 'foo', Symbol( 'foo' ) ] )( 'unresolved deps #1, id is %p', async ( id ) => { - const block = get_result_block( null, 50 )( { - options: { - deps: id, - }, - } ); - - expect.assertions( 2 ); - try { - await de.run( block ); - - } catch ( error ) { - expect( de.is_error( error ) ).toBe( true ); - expect( error.error.id ).toBe( de.ERROR_ID.INVALID_DEPS_ID ); - } - } ); - - it.each( [ 'foo', Symbol( 'foo' ) ] )( 'unresolved deps #2, id is %p', async ( id ) => { - const block_foo = get_result_block( null, 50 ); - const block_bar = get_result_block( null, 50 ); - - const block = de.object( { - block: { - foo: block_foo, - - bar: block_bar( { - options: { - deps: id, - }, - } ), - }, - } ); - - const result = await de.run( block ); - - expect( de.is_error( result.bar ) ).toBe( true ); - expect( result.bar.error.id ).toBe( de.ERROR_ID.INVALID_DEPS_ID ); - } ); - - it( 'fix 3.0.19', async () => { - const block = de.func( { - block: ( { generate_id } ) => { - const id_a = generate_id(); - const id_c = generate_id(); - - return de.object( { - block: { - A: get_error_block( () => de.error( { - id: 'ERROR_A', - } ), 50 )( { - options: { - id: id_a, - }, - } ), - - // Вот этот блок падает из-за A, n_active_blocks при этом не инкрементился, - // но в конце декрементился. В итоге n_active_blocks разъезжался и уходил в минус. - // - B: get_result_block( null, 50 )( { - options: { - deps: id_a, - }, - } ), - - C: get_result_block( null, 200 )( { - options: { - id: id_c, - }, - } ), - - // Этот блок зависит от C, но когда B падает из-за блока A, - // неправильно декрементился счетчик n_active_blocks и D решал, что - // зависимости не сходятся. Короче, какая-то Санта Барбара. - // - D: get_result_block( null, 50 )( { - options: { - deps: id_c, - required: true, - }, - } ), - }, - } ); - }, - } ); - - const r = await de.run( block ); - - expect( r.D ).toBe( null ); - } ); - - describe( 'de.pipe', () => { - - it( 'second block in pipe depends of the first one', async () => { - let result_bar; - const block = de.func( { - block: ( { generate_id } ) => { - const id_foo = generate_id(); - const block_foo = get_result_block( null, 50 )( { - options: { - id: id_foo, - }, - } ); - - result_bar = { - bar: 24, - }; - const block_bar = get_result_block( result_bar, 50 )( { - options: { - deps: id_foo, - }, - } ); - - return de.pipe( { - block: [ block_foo, block_bar ], - } ); - }, - } ); - - const result = await de.run( block ); - - expect( result ).toBe( result_bar ); - } ); - - it( 'first block in pipe depends of the second one', async () => { - const block = de.func( { - block: ( { generate_id } ) => { - const id_bar = generate_id(); - - const block_foo = get_result_block( null, 50 )( { - options: { - deps: id_bar, - }, - } ); - const block_bar = get_result_block( null, 50 )( { - options: { - deps: id_bar, - }, - } ); - - return de.pipe( { - block: [ block_foo, block_bar ], - } ); - }, - } ); - - expect.assertions( 2 ); - try { - await de.run( block ); - - } catch ( e ) { - expect( de.is_error( e ) ).toBe( true ); - expect( e.error.id ).toBe( de.ERROR_ID.DEPS_NOT_RESOLVED ); - } - } ); - - } ); - -} ); - diff --git a/tests/options.deps.test.ts b/tests/options.deps.test.ts new file mode 100644 index 0000000..e9cfb9b --- /dev/null +++ b/tests/options.deps.test.ts @@ -0,0 +1,983 @@ +/* eslint-disable jest/no-conditional-expect */ +import * as de from '../lib'; + +import { getErrorBlock, getResultBlock, getTimeout, waitForValue } from './helpers'; +import type { DescriptBlockId } from '../lib/depsDomain'; + + +// --------------------------------------------------------------------------------------------------------------- // + +describe('options.deps', () => { + + it('block with id and without deps', async() => { + const data = { + foo: 42, + }; + const id = Symbol('block'); + const block = getResultBlock(data, 50).extend({ + options: { + id: id, + }, + }); + + const result = await de.run(block); + + expect(result).toBe(data); + }); + + it('no options.deps, deps is empty object', async() => { + const spy = jest.fn(); + const block = de.func({ + block: spy, + }); + + await de.run(block); + expect(spy.mock.calls[ 0 ][ 0 ].deps).toEqual({}); + }); + + it('empty deps', async() => { + const data = { + foo: 42, + }; + const block = getResultBlock(data, 50).extend({ + options: { + deps: [], + }, + }); + + const result = await de.run(block); + + expect(result).toBe(data); + }); + + it('failed block with id and without deps', async() => { + const error = de.error('ERROR'); + const id = Symbol('block'); + const block = getErrorBlock(error, 50).extend({ + options: { + id: id, + }, + }); + + expect.assertions(1); + let e; + try { + await de.run(block); + + } catch (err) { + e = err; + } + expect(e).toBe(error); + }); + + it('block with invalid deps id #1', async() => { + const data = { + foo: 42, + }; + const id = Symbol('block'); + const block = getResultBlock(data, 50).extend({ + options: { + deps: id, + }, + }); + + expect.assertions(2); + let e; + try { + await de.run(block); + + } catch (err) { + e = err; + } + + expect(de.isError(e)).toBe(true); + expect(e.error.id).toBe(de.ERROR_ID.INVALID_DEPS_ID); + }); + + it('block with invalid deps id #2', async() => { + const data = { + foo: 42, + }; + const block = de.func({ + block: () => { + const id = Symbol('block'); + return getResultBlock(data, 50).extend({ + options: { + deps: id, + }, + }); + }, + }); + + expect.assertions(2); + try { + await de.run(block); + + } catch (e) { + expect(de.isError(e)).toBe(true); + expect(e.error.id).toBe(de.ERROR_ID.INVALID_DEPS_ID); + } + }); + + it('block depends on block #1 (deps is id)', async() => { + const spy = jest.fn(); + + const blockFoo = getResultBlock(() => spy('FOO'), 50); + const blockBar = getResultBlock(() => spy('BAR'), 50); + + const block = de.func({ + block: ({ generateId }) => { + const idFoo = generateId(); + + return de.object({ + block: { + foo: blockFoo.extend({ + options: { + id: idFoo, + }, + }), + bar: blockBar.extend({ + options: { + deps: idFoo, + }, + }), + }, + }); + }, + }); + + await de.run(block); + + const calls = spy.mock.calls; + + expect(calls).toHaveLength(2); + expect(calls[ 0 ][ 0 ]).toBe('FOO'); + expect(calls[ 1 ][ 0 ]).toBe('BAR'); + }); + + it('block depends on block #2 (deps is array)', async() => { + const spy = jest.fn(); + + const blockFoo = getResultBlock(() => spy('FOO'), 50); + const blockBar = getResultBlock(() => spy('BAR'), 50); + + const block = de.func({ + block: ({ generateId }) => { + const idFoo = generateId(); + + return de.object({ + block: { + foo: blockFoo.extend({ + options: { + id: idFoo, + }, + }), + bar: blockBar.extend({ + options: { + deps: [ idFoo ], + }, + }), + }, + }); + }, + }); + + await de.run(block); + + const calls = spy.mock.calls; + + expect(calls).toHaveLength(2); + expect(calls[ 0 ][ 0 ]).toBe('FOO'); + expect(calls[ 1 ][ 0 ]).toBe('BAR'); + }); + + it('block depends on block depends on block', async() => { + const spy = jest.fn(); + + const blockFoo = getResultBlock(() => spy('FOO'), 50); + const blockBar = getResultBlock(() => spy('BAR'), 50); + const blockQuu = getResultBlock(() => spy('QUU'), 50); + + const block = de.func({ + block: ({ generateId }) => { + const idFoo = generateId(); + const idBar = generateId(); + + return de.object({ + block: { + foo: blockFoo.extend({ + options: { + id: idFoo, + }, + }), + bar: blockBar.extend({ + options: { + id: idBar, + deps: idFoo, + }, + }), + quu: blockQuu.extend({ + options: { + deps: idBar, + }, + }), + }, + }); + }, + }); + + await de.run(block); + + const calls = spy.mock.calls; + + expect(calls).toHaveLength(3); + expect(calls[ 0 ][ 0 ]).toBe('FOO'); + expect(calls[ 1 ][ 0 ]).toBe('BAR'); + expect(calls[ 2 ][ 0 ]).toBe('QUU'); + }); + + it('block depends on two blocks', async() => { + const spy = jest.fn(); + + const blockFoo = getResultBlock(() => spy('FOO'), getTimeout(50, 100)); + const blockBar = getResultBlock(() => spy('BAR'), getTimeout(50, 100)); + const blockQuu = getResultBlock(() => spy('QUU'), 50); + + const block = de.func({ + block: ({ generateId }) => { + const idFoo = generateId(); + const idBar = generateId(); + + return de.object({ + block: { + foo: blockFoo.extend({ + options: { + id: idFoo, + }, + }), + bar: blockBar.extend({ + options: { + id: idBar, + }, + }), + quu: blockQuu.extend({ + options: { + deps: [ idFoo, idBar ], + }, + }), + }, + }); + }, + }); + + await de.run(block); + + const calls = spy.mock.calls; + + expect(calls).toHaveLength(3); + expect(calls[ 2 ][ 0 ]).toBe('QUU'); + }); + + it('two block depend on block', async() => { + const spy = jest.fn(); + + const blockFoo = getResultBlock(() => spy('FOO'), getTimeout(50, 100)); + const blockBar = getResultBlock(() => spy('BAR'), 50); + const blockQuu = getResultBlock(() => spy('QUU'), 100); + + const block = de.func({ + block: ({ generateId }) => { + const idFoo = generateId(); + + return de.object({ + block: { + foo: blockFoo.extend({ + options: { + id: idFoo, + }, + }), + bar: blockBar.extend({ + options: { + deps: idFoo, + }, + }), + quu: blockQuu.extend({ + options: { + deps: idFoo, + }, + }), + }, + }); + }, + }); + + await de.run(block); + + const calls = spy.mock.calls; + + expect(calls).toHaveLength(3); + expect(calls[ 0 ][ 0 ]).toBe('FOO'); + expect(calls[ 1 ][ 0 ]).toBe('BAR'); + expect(calls[ 2 ][ 0 ]).toBe('QUU'); + }); + + it('failed deps #1', async() => { + const errorFoo = de.error('SOME_ERROR'); + const blockFoo = getErrorBlock(errorFoo, 50); + + const bodyBar = jest.fn(() => undefined); + const blockBar = getResultBlock(bodyBar, 50); + + const block = de.func({ + block: ({ generateId }) => { + const idFoo = generateId(); + + return de.object({ + block: { + foo: blockFoo.extend({ + options: { + id: idFoo, + }, + }), + + bar: blockBar.extend({ + options: { + deps: idFoo, + }, + }), + }, + }); + }, + }); + + const result = await de.run(block); + + expect.assertions(4); + expect(de.isError(result.bar)).toBe(true); + + if ('error' in result.bar) { + expect(result.bar.error.id).toBe(de.ERROR_ID.DEPS_ERROR); + expect(result.bar.error.reason).toBe(errorFoo); + } + expect(bodyBar).toHaveBeenCalledTimes(0); + }); + + it('failed deps #2', async() => { + const errorFoo = de.error('SOME_ERROR_1'); + const blockFoo = getErrorBlock(errorFoo, 100); + + const errorBar = de.error('SOME_ERROR_2'); + const blockBar = getErrorBlock(errorBar, 50); + + const bodyQuu = jest.fn(); + const blockQuu = getResultBlock(bodyQuu, 50); + + const block = de.func({ + block: ({ generateId }) => { + const idFoo = generateId(); + const idBar = generateId(); + + return de.object({ + block: { + foo: blockFoo.extend({ + options: { + id: idFoo, + }, + }), + + bar: blockBar.extend({ + options: { + id: idBar, + }, + }), + + quu: blockQuu.extend({ + options: { + deps: [ idFoo, idBar ], + }, + }), + }, + }); + }, + }); + + const result = await de.run(block); + + expect.assertions(4); + + expect(de.isError(result.quu)).toBe(true); + if ('error' in result.quu) { + expect(result.quu.error.id).toBe(de.ERROR_ID.DEPS_ERROR); + // blockBar падает за 50 мс, а blockFoo за 100 мс. + // Поэтому в reason будет errorBar. + expect(result.quu.error.reason).toBe(errorBar); + } + expect(bodyQuu).toHaveBeenCalledTimes(0); + }); + + it('deps not resolved #1', async() => { + const blockFoo = getResultBlock(null, 50); + const blockBar = getResultBlock(null, 100); + + const block = de.func({ + block: ({ generateId }) => { + const idFoo = generateId(); + + return de.object({ + block: { + foo: blockFoo, + + bar: blockBar.extend({ + options: { + deps: idFoo, + }, + }), + }, + }); + }, + }); + + const result = await de.run(block); + + expect.assertions(2); + expect(de.isError(result.bar)).toBe(true); + + if (result.bar && 'error' in result.bar) { + expect(result.bar.error.id).toBe(de.ERROR_ID.DEPS_NOT_RESOLVED); + } + }); + + it('deps not resolved #2', async() => { + const blockFoo = getResultBlock(null, 50); + const blockBar = getResultBlock(null, 100); + + const block = de.func({ + block: ({ generateId }) => { + const idFoo = generateId(); + + return de.object({ + block: { + foo: blockFoo, + + bar: blockBar.extend({ + options: { + deps: idFoo, + }, + }), + }, + }); + }, + }); + + const result = await de.run(block); + + expect.assertions(2); + + expect(de.isError(result.bar)).toBe(true); + + if (result.bar && 'error' in result.bar) { + expect(result.bar.error.id).toBe(de.ERROR_ID.DEPS_NOT_RESOLVED); + } + }); + + it('one block with deps', async() => { + const block = de.func({ + block: ({ generateId }) => { + const id = generateId(); + + return getResultBlock(null, 50).extend({ + options: { + deps: id, + }, + }); + }, + }); + + expect.assertions(2); + try { + await de.run(block); + + } catch (e) { + expect(de.isError(e)).toBe(true); + expect(e.error.id).toBe(de.ERROR_ID.DEPS_NOT_RESOLVED); + } + }); + + it('block returns another block that depends on parent block', async() => { + const block = de.func({ + block: ({ generateId }) => { + const id = generateId(); + + const child = getResultBlock(null, 50).extend({ + options: { + deps: id, + }, + }); + + return getResultBlock(child, 50).extend({ + options: { + id: id, + }, + }); + }, + }); + + expect.assertions(2); + try { + await de.run(block); + + } catch (e) { + expect(de.isError(e)).toBe(true); + expect(e.error.id).toBe(de.ERROR_ID.DEPS_NOT_RESOLVED); + } + + + }); + + it('before( { deps } ) has deps results #1', async() => { + const dataFoo = { + foo: 42, + }; + const blockFoo = getResultBlock(dataFoo, 50); + const blockBar = getResultBlock(null, 50); + + const beforeBar = jest.fn(); + + let idFoo: DescriptBlockId; + const block = de.func({ + block: ({ generateId }) => { + idFoo = generateId(); + + return de.object({ + block: { + foo: blockFoo.extend({ + options: { + id: idFoo, + }, + }), + + bar: blockBar.extend({ + options: { + deps: idFoo, + + before: beforeBar, + }, + }), + }, + }); + }, + }); + + await de.run(block); + + const deps = beforeBar.mock.calls[ 0 ][ 0 ].deps; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(deps[ idFoo ]).toBe(dataFoo); + }); + + it('before( { deps } ) has deps results #2', async() => { + const dataFoo = { + foo: 42, + }; + const blockFoo = getResultBlock(dataFoo, 300); + + const dataBar = { + bar: 24, + }; + const blockBar = getResultBlock(dataBar, 200); + + const blockQuu = getResultBlock(null, 100); + + const beforeQuu = jest.fn(); + + let idFoo: DescriptBlockId; + let idBar: DescriptBlockId; + const block = de.func({ + block: ({ generateId }) => { + idFoo = generateId(); + idBar = generateId(); + + return de.object({ + block: { + foo: blockFoo.extend({ + options: { + id: idFoo, + }, + }), + + bar: blockBar.extend({ + options: { + id: idBar, + }, + }), + + quu: blockQuu.extend({ + options: { + deps: [ idFoo, idBar ], + + before: beforeQuu, + }, + }), + }, + }); + }, + }); + + await de.run(block); + + const deps = beforeQuu.mock.calls[ 0 ][ 0 ].deps; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(deps[ idFoo ]).toBe(dataFoo); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(deps[ idBar ]).toBe(dataBar); + }); + + it('before( { deps } ) has not results from other blocks', async() => { + const dataFoo = { + foo: 42, + }; + const blockFoo = getResultBlock(dataFoo, 50); + + const dataBar = { + bar: 24, + }; + const blockBar = getResultBlock(dataBar, 100); + + const blockQuu = getResultBlock(null, 50); + + const beforeQuu = jest.fn(); + + let idFoo: DescriptBlockId; + let idBar: DescriptBlockId; + const block = de.func({ + block: ({ generateId }) => { + idFoo = generateId(); + idBar = generateId(); + + return de.object({ + block: { + foo: blockFoo.extend({ + options: { + id: idFoo, + }, + }), + + bar: blockBar.extend({ + options: { + id: idBar, + }, + }), + + quu: blockQuu.extend({ + options: { + deps: idBar, + + before: beforeQuu, + }, + }), + }, + }); + }, + }); + + await de.run(block); + + const deps = beforeQuu.mock.calls[ 0 ][ 0 ].deps; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(deps[ idFoo ]).toBeUndefined(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(deps[ idBar ]).toBe(dataBar); + }); + + it('wait for result of de.func', async() => { + const beforeQuu = jest.fn(); + + const dataFoo = { + foo: 42, + }; + let idFoo: DescriptBlockId; + + const block = de.func({ + block: ({ generateId }) => { + idFoo = generateId(); + + return de.object({ + block: { + bar: getResultBlock(() => { + return getResultBlock(dataFoo, 50).extend({ + options: { + id: idFoo, + }, + }); + }, 50), + + quu: getResultBlock(null, 50).extend({ + options: { + deps: idFoo, + + before: beforeQuu, + }, + }), + }, + }); + }, + }); + + await de.run(block); + + const deps = beforeQuu.mock.calls[ 0 ][ 0 ].deps; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(deps[ idFoo ]).toBe(dataFoo); + }); + + it('result of de.func has deps #1', async() => { + const block = de.func({ + block: ({ generateId }) => { + const idFoo = generateId(); + + return de.object({ + block: { + foo: getResultBlock(null, 50).extend({ + options: { + id: idFoo, + }, + }), + + bar: de.func({ + block: () => { + return new Promise((resolve) => { + setTimeout(() => { + resolve(getResultBlock(null, 50).extend({ + options: { + deps: idFoo, + }, + })); + }, 100); + }); + }, + }), + }, + }); + }, + }); + + const result = await de.run(block); + + expect(result).toEqual({ foo: null, bar: null }); + + }); + + it('result of de.func has deps #2', async() => { + let errorFoo; + + const block = de.func({ + block: ({ generateId }) => { + const idFoo = generateId(); + + return de.object({ + block: { + foo: de.func({ + block: async() => { + await waitForValue(null, 50); + + errorFoo = de.error('ERROR'); + throw errorFoo; + }, + options: { + id: idFoo, + }, + }), + + bar: de.func({ + block: async() => { + await waitForValue(null, 50); + + return getResultBlock(null, 50).extend({ + options: { + deps: idFoo, + }, + }); + }, + }), + }, + }); + }, + }); + + const result = await de.run(block); + + expect.assertions(4); + expect(result.foo).toBe(errorFoo); + expect(de.isError(result.bar)).toBe(true); + + if (result.bar && 'error' in result.bar) { + expect(result.bar.error.id).toBe(de.ERROR_ID.DEPS_ERROR); + expect(result.bar.error.reason).toBe(errorFoo); + } + }); + + it.each([ Symbol('foo') ])('unresolved deps #1, id is %p', async(id) => { + const block = getResultBlock(null, 50).extend({ + options: { + deps: id, + }, + }); + + expect.assertions(2); + try { + await de.run(block); + + } catch (error) { + expect(de.isError(error)).toBe(true); + expect(error.error.id).toBe(de.ERROR_ID.INVALID_DEPS_ID); + } + }); + + it.each([ Symbol('foo') ])('unresolved deps #2, id is %p', async(id) => { + const blockFoo = getResultBlock(null, 50); + const blockBar = getResultBlock(null, 50); + + const block = de.object({ + block: { + foo: blockFoo, + + bar: blockBar.extend({ + options: { + deps: id, + }, + }), + }, + }); + + const result = await de.run(block); + expect.assertions(2); + expect(de.isError(result.bar)).toBe(true); + if (result.bar && 'error' in result.bar) { + expect(result.bar.error.id).toBe(de.ERROR_ID.INVALID_DEPS_ID); + } + }); + + it('fix 3.0.19', async() => { + const block = de.func({ + block: ({ generateId }) => { + const idA = generateId(); + const idC = generateId(); + + return de.object({ + block: { + A: getErrorBlock(() => de.error('ERROR_A'), 50).extend({ + options: { + id: idA, + }, + }), + + // Вот этот блок падает из-за A, n_active_blocks при этом не инкрементился, + // но в конце декрементился. В итоге n_active_blocks разъезжался и уходил в минус. + // + B: getResultBlock(null, 50).extend({ + options: { + deps: idA, + }, + }), + + C: getResultBlock(null, 200).extend({ + options: { + id: idC, + }, + }), + + // Этот блок зависит от C, но когда B падает из-за блока A, + // неправильно декрементился счетчик n_active_blocks и D решал, что + // зависимости не сходятся. Короче, какая-то Санта Барбара. + // + D: getResultBlock(null, 50).extend({ + options: { + deps: idC, + required: true, + }, + }), + }, + }); + }, + }); + + const r = await de.run(block); + + expect(r.D).toBeNull(); + }); + + + describe('de.pipe', () => { + + it('second block in pipe depends of the first one', async() => { + let resultBar; + const block = de.func({ + block: ({ generateId }) => { + const idFoo = generateId(); + const blockFoo = getResultBlock(null, 50).extend({ + options: { + id: idFoo, + }, + }); + + resultBar = { + bar: 24, + }; + const blockBar = getResultBlock(resultBar, 50).extend({ + options: { + deps: idFoo, + }, + }); + + return de.pipe({ + block: [ blockFoo, blockBar ], + }); + }, + }); + + const result = await de.run(block); + + expect(result).toBe(resultBar); + }); + + it('first block in pipe depends of the second one', async() => { + const block = de.func({ + block: ({ generateId }) => { + const idBar = generateId(); + + const blockFoo = getResultBlock(null, 50).extend({ + options: { + deps: idBar, + }, + }); + const blockBar = getResultBlock(null, 50).extend({ + options: { + deps: idBar, + }, + }); + + return de.pipe({ + block: [ blockFoo, blockBar ], + }); + }, + }); + + expect.assertions(2); + try { + await de.run(block); + + } catch (e) { + expect(de.isError(e)).toBe(true); + expect(e.error.id).toBe(de.ERROR_ID.DEPS_NOT_RESOLVED); + } + }); + + }); + +}); diff --git a/tests/options.error.test.js b/tests/options.error.test.js deleted file mode 100644 index b07b47c..0000000 --- a/tests/options.error.test.js +++ /dev/null @@ -1,206 +0,0 @@ -const de = require( '../lib' ); - -const { - get_result_block, - get_error_block, -} = require( './helpers' ); - -// --------------------------------------------------------------------------------------------------------------- // - -describe( 'options.error', () => { - - it( 'receives { params, context, error }', async () => { - const error = de.error( { - id: 'ERROR', - } ); - const spy = jest.fn( () => null ); - const block = get_error_block( error )( { - options: { - error: spy, - }, - } ); - - const params = { - id: 42, - }; - const context = { - context: true, - }; - await de.run( block, { params, context } ); - - const arg = spy.mock.calls[ 0 ][ 0 ]; - expect( arg.params ).toBe( params ); - expect( arg.context ).toBe( context ); - expect( arg.error ).toBe( error ); - } ); - - it( 'never called if block successful', async () => { - const block_result = { - foo: 42, - }; - const spy = jest.fn(); - const block = get_result_block( block_result )( { - options: { - error: spy, - }, - } ); - - await de.run( block ); - - expect( spy.mock.calls ).toHaveLength( 0 ); - } ); - - it( 'returns another error', async () => { - // Нужно делать throw, а не кидать ошибку. - // Просто return de.error( ... ) не приводит к ошибке на самом деле. - - const error_1 = de.error( { - id: 'ERROR_1', - } ); - const error_2 = de.error( { - id: 'ERROR_2', - } ); - const block = get_error_block( error_1 )( { - options: { - error: () => error_2, - }, - } ); - - const result = await de.run( block ); - - expect( result ).toBe( error_2 ); - } ); - - it( 'throws ReferenceError', async () => { - const error_1 = de.error( { - id: 'ERROR_1', - } ); - const spy = jest.fn( () => { - // eslint-disable-next-line no-undef - return x; - } ); - const block = get_error_block( error_1 )( { - options: { - error: spy, - }, - } ); - - expect.assertions( 3 ); - try { - await de.run( block ); - - } catch ( e ) { - expect( de.is_error( e ) ).toBe( true ); - expect( e.error.id ).toBe( 'ReferenceError' ); - expect( spy.mock.calls ).toHaveLength( 1 ); - } - } ); - - it( 'throws de.error', async () => { - const error_1 = de.error( { - id: 'ERROR_1', - } ); - let error_2; - const spy = jest.fn( () => { - error_2 = de.error( { - id: 'ERROR_2', - } ); - - throw error_2; - } ); - const block = get_error_block( error_1 )( { - options: { - error: spy, - }, - } ); - - expect.assertions( 2 ); - try { - await de.run( block ); - - } catch ( e ) { - expect( e ).toBe( error_2 ); - expect( spy.mock.calls ).toHaveLength( 1 ); - } - } ); - - it.each( [ { foo: 42 }, 0, '', null, false ] )( 'returns %j', async ( value ) => { - const error = de.error( { - id: 'ERROR', - } ); - const block = get_error_block( error )( { - options: { - error: () => value, - }, - } ); - - const result = await de.run( block ); - - expect( result ).toBe( value ); - } ); - - it( 'returns undefined', async () => { - const error = de.error( { - id: 'ERROR', - } ); - const spy = jest.fn( () => undefined ); - const block = get_error_block( error )( { - options: { - error: spy, - }, - } ); - - const result = await de.run( block ); - expect( result ).toBe( undefined ); - } ); - - it.each( [ { foo: 42 }, 0, '', null, false, undefined ] )( 'first returns %j, second never called', async ( value ) => { - const error = de.error( { - id: 'ERROR', - } ); - const spy = jest.fn(); - const block_1 = get_error_block( error )( { - options: { - error: () => value, - }, - } ); - const block_2 = block_1( { - options: { - error: spy, - }, - } ); - - const result = await de.run( block_2 ); - - expect( result ).toBe( value ); - expect( spy.mock.calls ).toHaveLength( 0 ); - } ); - - it( 'first throws, second gets error from first', async () => { - const error_1 = de.error( { - id: 'ERROR', - } ); - const error_2 = de.error( { - id: 'ANOTHER_ERROR', - } ); - const block_1 = get_error_block( error_1, 50 )( { - options: { - error: () => { - throw error_2; - }, - }, - } ); - const spy = jest.fn( () => null ); - const block_2 = block_1( { - options: { - error: spy, - }, - } ); - - await de.run( block_2 ); - - expect( spy.mock.calls[ 0 ][ 0 ].error ).toBe( error_2 ); - } ); - -} ); - diff --git a/tests/options.error.test.ts b/tests/options.error.test.ts new file mode 100644 index 0000000..00f5a07 --- /dev/null +++ b/tests/options.error.test.ts @@ -0,0 +1,181 @@ +/* eslint-disable jest/no-conditional-expect */ +import { getErrorBlock, getResultBlock } from './helpers'; + +import * as de from '../lib'; +// --------------------------------------------------------------------------------------------------------------- // + +describe('options.error', () => { + + it('receives { params, context, error }', async() => { + const error = de.error('ERROR'); + const spy = jest.fn(() => null); + const block = getErrorBlock(error).extend({ + options: { + error: spy, + }, + }); + + const params = { + id: 42, + }; + const context = { + context: true, + }; + await de.run(block, { params, context }); + + const arg = spy.mock.calls[ 0 ][ 0 ]; + expect(arg.params).toBe(params); + expect(arg.context).toBe(context); + expect(arg.error).toBe(error); + }); + + it('never called if block successful', async() => { + const blockResult = { + foo: 42, + }; + const spy = jest.fn(); + const block = getResultBlock(blockResult).extend({ + options: { + error: spy, + }, + }); + + await de.run(block); + + expect(spy.mock.calls).toHaveLength(0); + }); + + it('returns another error', async() => { + // Нужно делать throw, а не кидать ошибку. + // Просто return de.error( ... ) не приводит к ошибке на самом деле. + + const error1 = de.error('ERROR_1'); + const error2 = de.error('ERROR_2'); + const block = getErrorBlock(error1).extend({ + options: { + error: () => error2, + }, + }); + + const result = await de.run(block); + + expect(result).toBe(error2); + }); + + it('throws ReferenceError', async() => { + const error1 = de.error('ERROR_1'); + const spy = jest.fn(() => { + // eslint-disable-next-line no-undef,@typescript-eslint/ban-ts-comment + // @ts-ignore + return x; + }); + const block = getErrorBlock(error1).extend({ + options: { + error: spy, + }, + }); + + expect.assertions(3); + try { + await de.run(block); + + } catch (e) { + expect(de.isError(e)).toBe(true); + expect(e.error.id).toBe('ReferenceError'); + expect(spy.mock.calls).toHaveLength(1); + } + }); + + it('throws de.error', async() => { + const error1 = de.error('ERROR_1'); + let error2; + const spy = jest.fn(() => { + error2 = de.error('ERROR_2'); + + throw error2; + }); + const block = getErrorBlock(error1).extend({ + options: { + error: spy, + }, + }); + + expect.assertions(2); + try { + await de.run(block); + + } catch (e) { + expect(e).toBe(error2); + expect(spy.mock.calls).toHaveLength(1); + } + }); + + it.each([ { foo: 42 }, 0, '', null, false ])('returns %j', async(value) => { + const error = de.error('ERROR'); + const block = getErrorBlock(error).extend({ + options: { + error: () => value, + }, + }); + + const result = await de.run(block); + + expect(result).toBe(value); + }); + + it('returns undefined', async() => { + const error = de.error('ERROR'); + const spy = jest.fn(() => undefined); + const block = getErrorBlock(error).extend({ + options: { + error: spy, + }, + }); + + const result = await de.run(block); + expect(result).toBeUndefined(); + }); + + it.each([ { foo: 42 }, 0, '', null, false, undefined ])('first returns %j, second never called', async(value) => { + const error = de.error('ERROR'); + const spy = jest.fn(); + const block1 = getErrorBlock(error).extend({ + options: { + error: () => value, + }, + }); + const block2 = block1.extend({ + options: { + error: spy, + }, + }); + + const result = await de.run(block2); + + expect(result).toBe(value); + expect(spy.mock.calls).toHaveLength(0); + }); + + it('first throws, second gets error from first', async() => { + const error1 = de.error('ERROR'); + const error2 = de.error('ANOTHER_ERROR'); + const block1 = getErrorBlock(error1, 50).extend({ + options: { + error: () => { + throw error2; + }, + }, + }); + const spy = jest.fn(() => null); + const block2 = block1.extend({ + options: { + error: spy, + }, + }); + + await de.run(block2); + + expect(spy.mock.calls[ 0 ][ 0 ].error).toBe(error2); + }); + +}); diff --git a/tests/options.params.test.js b/tests/options.params.test.js deleted file mode 100644 index 72d20b8..0000000 --- a/tests/options.params.test.js +++ /dev/null @@ -1,208 +0,0 @@ -const de = require( '../lib' ); - -const { - get_result_block, -} = require( './helpers' ); - -describe( 'options.params', () => { - - it( 'no params', async () => { - const spy = jest.fn(); - const block = get_result_block( spy ); - - await de.run( block ); - - const calls = spy.mock.calls; - expect( calls[ 0 ][ 0 ].params ).toStrictEqual( {} ); - } ); - - describe( 'params is a function', () => { - - it( 'params gets { params, context }', async () => { - const spy = jest.fn( () => ( {} ) ); - const block = get_result_block( null )( { - options: { - params: spy, - }, - } ); - - const params = { - id: 42, - }; - const context = { - context: true, - }; - await de.run( block, { params, context } ); - - const calls = spy.mock.calls; - expect( calls[ 0 ][ 0 ].params ).toBe( params ); - expect( calls[ 0 ][ 0 ].context ).toBe( context ); - } ); - - it( 'params gets { deps }', async () => { - const spy = jest.fn(); - - let data_foo; - let id_foo; - - const block = de.func( { - block: ( { generate_id } ) => { - data_foo = { - foo: 42, - }; - id_foo = generate_id( 'foo' ); - - return de.object( { - block: { - foo: get_result_block( data_foo )( { - options: { - id: id_foo, - }, - } ), - - bar: get_result_block( null )( { - options: { - deps: id_foo, - params: spy, - }, - } ), - }, - } ); - }, - } ); - - await de.run( block ); - - const calls = spy.mock.calls; - expect( calls[ 0 ][ 0 ].deps[ id_foo ] ).toBe( data_foo ); - } ); - - it.each( [ undefined, null, false, '', 0, 42 ] )( 'params returns %j', async ( params_result ) => { - const spy = jest.fn(); - const block = get_result_block( spy )( { - options: { - params: () => params_result, - }, - } ); - - expect.assertions( 3 ); - try { - await de.run( block ); - - } catch ( e ) { - expect( de.is_error( e ) ).toBe( true ); - expect( e.error.id ).toBe( de.ERROR_ID.INVALID_OPTIONS_PARAMS ); - expect( spy.mock.calls ).toHaveLength( 0 ); - } - } ); - - it( 'params returns object, action gets it as { params }', async () => { - const spy = jest.fn(); - - let params; - - const block = get_result_block( spy )( { - options: { - params: () => { - params = { - id: 42, - }; - - return params; - }, - }, - } ); - - await de.run( block ); - - expect( spy.mock.calls[ 0 ][ 0 ].params ).toBe( params ); - } ); - - it( 'params throws', async () => { - const error = de.error( { - id: 'SOME_ERROR', - } ); - const block = get_result_block( null )( { - options: { - params: () => { - throw error; - }, - }, - } ); - - expect.assertions( 1 ); - try { - await de.run( block ); - - } catch ( e ) { - expect( e ).toBe( error ); - } - } ); - - } ); - - it( 'params is an object', async () => { - const block = get_result_block( null )( { - options: { - params: { - foo: null, - }, - }, - } ); - - expect.assertions( 2 ); - try { - await de.run( block ); - - } catch ( e ) { - expect( de.is_error( e ) ).toBe( true ); - expect( e.error.id ).toBe( de.ERROR_ID.INVALID_OPTIONS_PARAMS ); - } - } ); - - describe( 'inheritance', () => { - - it( 'child first, then parent', async () => { - let parent_params; - const parent_spy = jest.fn( () => { - parent_params = { - foo: 42, - }; - return parent_params; - } ); - - let child_params; - const child_spy = jest.fn( () => { - child_params = { - bar: 24, - }; - return child_params; - } ); - - const action_spy = jest.fn(); - - const parent = get_result_block( action_spy )( { - options: { - params: parent_spy, - }, - } ); - const child = parent( { - options: { - params: child_spy, - }, - } ); - - const params = { - quu: 66, - }; - await de.run( child, { params } ); - - expect( child_spy.mock.calls[ 0 ][ 0 ].params ).toBe( params ); - expect( parent_spy.mock.calls[ 0 ][ 0 ].params ).toBe( child_params ); - expect( action_spy.mock.calls[ 0 ][ 0 ].params ).toBe( parent_params ); - } ); - - } ); - -} ); - diff --git a/tests/options.params.test.ts b/tests/options.params.test.ts new file mode 100644 index 0000000..f4cd511 --- /dev/null +++ b/tests/options.params.test.ts @@ -0,0 +1,190 @@ +/* eslint-disable jest/no-conditional-expect */ + +import * as de from '../lib'; + +import { getResultBlock } from './helpers'; +import type { DescriptBlockId } from '../lib/depsDomain'; + + +describe('options.params', () => { + + it('no params', async() => { + const spy = jest.fn(); + const block = getResultBlock(spy); + + await de.run(block); + + const calls = spy.mock.calls; + expect(calls[ 0 ][ 0 ].params).toStrictEqual({}); + }); + + describe('params is a function', () => { + + it('params gets { params, context }', async() => { + const spy = jest.fn(() => ({})); + const block = getResultBlock(null).extend({ + options: { + params: spy, + }, + }); + + const params = { + id: 42, + }; + const context = { + context: true, + }; + await de.run(block, { params, context }); + + const calls = spy.mock.calls; + expect(calls[ 0 ][ 0 ].params).toBe(params); + expect(calls[ 0 ][ 0 ].context).toBe(context); + }); + + it('params gets { deps }', async() => { + const spy = jest.fn(); + + let dataFoo; + let idFoo: DescriptBlockId; + + const block = de.func({ + block: ({ generateId }) => { + dataFoo = { + foo: 42, + }; + idFoo = generateId('foo'); + + return de.object({ + block: { + foo: getResultBlock(dataFoo).extend({ + options: { + id: idFoo, + }, + }), + + bar: getResultBlock(null).extend({ + options: { + deps: idFoo, + params: spy, + }, + }), + }, + }); + }, + }); + + await de.run(block); + + const calls = spy.mock.calls; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(calls[ 0 ][ 0 ].deps[ idFoo ]).toBe(dataFoo); + }); + + it.each([ undefined, null, false, '', 0, 42 ])('params returns %j', async(paramsResult) => { + const spy = jest.fn(); + const block = getResultBlock(spy).extend({ + options: { + params: () => paramsResult, + }, + }); + + expect.assertions(3); + try { + await de.run(block); + + } catch (e) { + expect(de.isError(e)).toBe(true); + expect(e.error.id).toBe(de.ERROR_ID.INVALID_OPTIONS_PARAMS); + expect(spy.mock.calls).toHaveLength(0); + } + }); + + it('params returns object, action gets it as { params }', async() => { + const spy = jest.fn(); + + let params; + + const block = getResultBlock(spy).extend({ + options: { + params: () => { + params = { + id: 42, + }; + + return params; + }, + }, + }); + + await de.run(block); + + expect(spy.mock.calls[ 0 ][ 0 ].params).toBe(params); + }); + + it('params throws', async() => { + const error = de.error('SOME_ERROR'); + const block = getResultBlock(null).extend({ + options: { + params: () => { + throw error; + }, + }, + }); + + expect.assertions(1); + try { + await de.run(block); + + } catch (e) { + expect(e).toBe(error); + } + }); + + }); + + describe('inheritance', () => { + + it('child first, then parent', async() => { + let parentParams; + const parentSpy = jest.fn(() => { + parentParams = { + foo: 42, + }; + return parentParams; + }); + + let childParams; + const childSpy = jest.fn(() => { + childParams = { + bar: 24, + }; + return childParams; + }); + + const actionSpy = jest.fn(); + + const parent = getResultBlock(actionSpy).extend({ + options: { + params: parentSpy, + }, + }); + const child = parent.extend({ + options: { + params: childSpy, + }, + }); + + const params = { + quu: 66, + }; + await de.run(child, { params }); + + expect(childSpy.mock.calls[ 0 ][ 0 ].params).toBe(params); + expect(parentSpy.mock.calls[ 0 ][ 0 ].params).toBe(childParams); + expect(actionSpy.mock.calls[ 0 ][ 0 ].params).toBe(parentParams); + }); + + }); + +}); diff --git a/tests/options.timeout.test.js b/tests/options.timeout.test.js deleted file mode 100644 index b0d14dd..0000000 --- a/tests/options.timeout.test.js +++ /dev/null @@ -1,42 +0,0 @@ -const de = require( '../lib' ); - -const { - get_result_block, -} = require( './helpers' ); - -describe( 'options.timeout', () => { - - it( 'fail after timeout', async () => { - const block = get_result_block( null, 100 )( { - options: { - timeout: 50, - }, - } ); - - expect.assertions( 2 ); - try { - await de.run( block ); - - } catch ( e ) { - expect( de.is_error( e ) ).toBe( true ); - expect( e.error.id ).toBe( de.ERROR_ID.BLOCK_TIMED_OUT ); - } - } ); - - it( 'success before timeout', async () => { - const data = { - foo: 42, - }; - const block = get_result_block( data, 50 )( { - options: { - timeout: 100, - }, - } ); - - const result = await de.run( block ); - - expect( result ).toBe( data ); - } ); - -} ); - diff --git a/tests/options.timeout.test.ts b/tests/options.timeout.test.ts new file mode 100644 index 0000000..82f77d3 --- /dev/null +++ b/tests/options.timeout.test.ts @@ -0,0 +1,41 @@ +/* eslint-disable jest/no-conditional-expect */ +import * as de from '../lib'; + +import { getResultBlock } from './helpers'; + + +describe('options.timeout', () => { + + it('fail after timeout', async() => { + const block = getResultBlock(null, 100).extend({ + options: { + timeout: 50, + }, + }); + + expect.assertions(2); + try { + await de.run(block); + + } catch (e) { + expect(de.isError(e)).toBe(true); + expect(e.error.id).toBe(de.ERROR_ID.BLOCK_TIMED_OUT); + } + }); + + it('success before timeout', async() => { + const data = { + foo: 42, + }; + const block = getResultBlock(data, 50).extend({ + options: { + timeout: 100, + }, + }); + + const result = await de.run(block); + + expect(result).toBe(data); + }); + +}); diff --git a/tests/pipeBlock.test.ts b/tests/pipeBlock.test.ts new file mode 100644 index 0000000..36197bd --- /dev/null +++ b/tests/pipeBlock.test.ts @@ -0,0 +1,117 @@ +/* eslint-disable jest/no-conditional-expect */ +import * as de from '../lib'; + +describe('de.pipe', () => { + + it('all blocks are successful', async() => { + let result1; + const spy1 = jest.fn(() => { + result1 = { + a: 1, + }; + return result1; + }); + const block1 = de.func({ + block: spy1, + }); + + let result2; + const spy2 = jest.fn(() => { + result2 = { + b: 2, + }; + return result2; + }); + const block2 = de.func({ + block: spy2, + }); + + let result3; + const spy3 = jest.fn(() => { + result3 = { + c: 3, + }; + return result3; + }); + const block3 = de.func({ + block: spy3, + }); + + const block = de.pipe({ + block: [ block1, block2, block3 ], + }); + + const result = await de.run(block); + + expect(spy1.mock.calls[ 0 ][ 0 ].deps.prev).toEqual([]); + expect(spy2.mock.calls[ 0 ][ 0 ].deps.prev).toHaveLength(1); + expect(spy2.mock.calls[ 0 ][ 0 ].deps.prev[ 0 ]).toBe(result1); + expect(spy3.mock.calls[ 0 ][ 0 ].deps.prev).toHaveLength(2); + expect(spy3.mock.calls[ 0 ][ 0 ].deps.prev[ 0 ]).toBe(result1); + expect(spy3.mock.calls[ 0 ][ 0 ].deps.prev[ 1 ]).toBe(result2); + expect(spy1.mock.calls[ 0 ][ 0 ].deps.prev).not.toBe(spy2.mock.calls[ 0 ][ 0 ].deps.prev); + expect(spy2.mock.calls[ 0 ][ 0 ].deps.prev).not.toBe(spy3.mock.calls[ 0 ][ 0 ].deps.prev); + expect(result).toBe(result3); + }); + + it('first block throws', async() => { + let error; + const block1 = de.func({ + block: () => { + error = de.error({ + id: 'ERROR', + }); + throw error; + }, + }); + + const spy2 = jest.fn(); + const block2 = de.func({ + block: spy2, + }); + + const block = de.pipe({ + block: [ block1, block2 ], + }); + + expect.assertions(2); + try { + await de.run(block); + + } catch (e) { + expect(e).toBe(error); + expect(spy2.mock.calls).toHaveLength(0); + } + }); + + it('second block throws', async() => { + const spy1 = jest.fn(); + const block1 = de.func({ + block: spy1, + }); + + let error; + const block2 = de.func({ + block: () => { + error = de.error({ + id: 'ERROR', + }); + throw error; + }, + }); + + const block = de.pipe({ + block: [ block1, block2 ], + }); + + expect.assertions(2); + try { + await de.run(block); + + } catch (e) { + expect(e).toBe(error); + expect(spy1.mock.calls).toHaveLength(1); + } + }); + +}); diff --git a/tests/pipe_block.test.js b/tests/pipe_block.test.js deleted file mode 100644 index bb47410..0000000 --- a/tests/pipe_block.test.js +++ /dev/null @@ -1,117 +0,0 @@ -const de = require( '../lib' ); - -describe( 'de.pipe', () => { - - it( 'all blocks are successful', async () => { - let result_1; - const spy_1 = jest.fn( () => { - result_1 = { - a: 1, - }; - return result_1; - } ); - const block_1 = de.func( { - block: spy_1, - } ); - - let result_2; - const spy_2 = jest.fn( () => { - result_2 = { - b: 2, - }; - return result_2; - } ); - const block_2 = de.func( { - block: spy_2, - } ); - - let result_3; - const spy_3 = jest.fn( () => { - result_3 = { - c: 3, - }; - return result_3; - } ); - const block_3 = de.func( { - block: spy_3, - } ); - - const block = de.pipe( { - block: [ block_1, block_2, block_3 ], - } ); - - const result = await de.run( block ); - - expect( spy_1.mock.calls[ 0 ][ 0 ].deps.prev ).toEqual( [] ); - expect( spy_2.mock.calls[ 0 ][ 0 ].deps.prev ).toHaveLength( 1 ); - expect( spy_2.mock.calls[ 0 ][ 0 ].deps.prev[ 0 ] ).toBe( result_1 ); - expect( spy_3.mock.calls[ 0 ][ 0 ].deps.prev ).toHaveLength( 2 ); - expect( spy_3.mock.calls[ 0 ][ 0 ].deps.prev[ 0 ] ).toBe( result_1 ); - expect( spy_3.mock.calls[ 0 ][ 0 ].deps.prev[ 1 ] ).toBe( result_2 ); - expect( spy_1.mock.calls[ 0 ][ 0 ].deps.prev ).not.toBe( spy_2.mock.calls[ 0 ][ 0 ].deps.prev ); - expect( spy_2.mock.calls[ 0 ][ 0 ].deps.prev ).not.toBe( spy_3.mock.calls[ 0 ][ 0 ].deps.prev ); - expect( result ).toBe( result_3 ); - } ); - - it( 'first block throws', async () => { - let error; - const block_1 = de.func( { - block: () => { - error = de.error( { - id: 'ERROR', - } ); - throw error; - }, - } ); - - const spy_2 = jest.fn(); - const block_2 = de.func( { - block: spy_2, - } ); - - const block = de.pipe( { - block: [ block_1, block_2 ], - } ); - - expect.assertions( 2 ); - try { - await de.run( block ); - - } catch ( e ) { - expect( e ).toBe( error ); - expect( spy_2.mock.calls ).toHaveLength( 0 ); - } - } ); - - it( 'second block throws', async () => { - const spy_1 = jest.fn(); - const block_1 = de.func( { - block: spy_1, - } ); - - let error; - const block_2 = de.func( { - block: () => { - error = de.error( { - id: 'ERROR', - } ); - throw error; - }, - } ); - - const block = de.pipe( { - block: [ block_1, block_2 ], - } ); - - expect.assertions( 2 ); - try { - await de.run( block ); - - } catch ( e ) { - expect( e ).toBe( error ); - expect( spy_1.mock.calls ).toHaveLength( 1 ); - } - } ); - -} ); - diff --git a/tests/request.test.js b/tests/request.test.js deleted file mode 100644 index 86345ce..0000000 --- a/tests/request.test.js +++ /dev/null @@ -1,1178 +0,0 @@ -const url_ = require( 'url' ); -const qs_ = require( 'querystring' ); -const path_ = require( 'path' ); -const fs_ = require( 'fs' ); -const http_ = require( 'http' ); -const https_ = require( 'https' ); -const { gzipSync } = require( 'node:zlib' ); -const { compress } = require( '@fengkx/zstd-napi' ); -const { Duplex } = require( 'stream' ); - -const de = require( '../lib' ); -const request = require( '../lib/request' ); -const Server = require( './server' ); - -const { get_path, wait_for_value } = require( './helpers' ); - -// --------------------------------------------------------------------------------------------------------------- // - -function get_do_request( default_options ) { - return function do_request( options, logger, cancel ) { - logger = logger || new de.Logger( { debug: true } ); - cancel = cancel || new de.Cancel(); - const context = null; - - return request( { ...default_options, ...options }, logger, context, cancel ); - }; -} - -// --------------------------------------------------------------------------------------------------------------- // - -describe( 'request', () => { - - describe( 'http', () => { - - const PORT = 9000; - const PORT_IPV6 = 9006; - - const do_request = get_do_request( { - protocol: 'http:', - hostname: '127.0.0.1', - port: PORT, - pathname: '/', - } ); - - const fake = new Server( { - module: http_, - listen_options: { - port: PORT, - }, - } ); - - const fake_ipv6 = new Server( { - module: http_, - listen_options: { - host: '::1', - port: PORT_IPV6, - ipv6Only: true, - }, - } ); - - beforeAll( () => Promise.all( [ - fake.start(), - fake_ipv6.start(), - ] ) ); - afterAll( () => Promise.all( [ - fake.stop(), - fake_ipv6.stop(), - ] ) ); - - it.each( [ 'GET', 'DELETE' ] )( '%j', async ( method ) => { - const path = get_path(); - - const CONTENT = 'Привет!'; - - fake.add( path, { - status_code: 200, - content: CONTENT, - } ); - - const result = await do_request( { - method: method, - pathname: path, - } ); - - expect( result.status_code ).toBe( 200 ); - expect( Buffer.isBuffer( result.body ) ).toBe( true ); - expect( result.body.toString() ).toBe( CONTENT ); - } ); - - it( 'pathname always starts with /', async () => { - const path = get_path(); - - const spy = jest.fn( ( req, res ) => res.end() ); - - fake.add( path, spy ); - - await do_request( { - pathname: path.replace( /^\//, '' ), - } ); - - const [ req ] = spy.mock.calls[ 0 ]; - - expect( url_.parse( req.url ).pathname ).toBe( path ); - } ); - - it( 'sets accept-encoding to gzip,deflate', async () => { - const path = get_path(); - - const spy = jest.fn( ( req, res ) => res.end() ); - - fake.add( path, spy ); - - await do_request( { - pathname: path, - } ); - - const [ req ] = spy.mock.calls[ 0 ]; - - expect( req.headers[ 'accept-encoding' ] ).toBe( 'gzip,deflate' ); - } ); - - it( 'adds gzip,deflate to accept-encoding', async () => { - const path = get_path(); - - const spy = jest.fn( ( req, res ) => res.end() ); - - fake.add( path, spy ); - - await do_request( { - pathname: path, - headers: { - 'accept-encoding': 'compress', - }, - } ); - - const [ req ] = spy.mock.calls[ 0 ]; - - expect( req.headers[ 'accept-encoding' ] ).toBe( 'gzip,deflate,compress' ); - } ); - - it( 'sends lower-cased headers', async () => { - const path = get_path(); - - const spy = jest.fn( ( req, res ) => res.end() ); - - fake.add( path, spy ); - - await do_request( { - pathname: path, - headers: { - 'x-request-foo': 'Foo', - 'X-REQUEST-BAR': 'bAr', - 'X-Request-Quu': 'quU', - }, - } ); - - const [ req ] = spy.mock.calls[ 0 ]; - - expect( req.headers[ 'x-request-foo' ] ).toBe( 'Foo' ); - expect( req.headers[ 'x-request-bar' ] ).toBe( 'bAr' ); - expect( req.headers[ 'x-request-quu' ] ).toBe( 'quU' ); - } ); - - it( 'query', async () => { - const path = get_path(); - - const spy = jest.fn( ( req, res ) => res.end() ); - - fake.add( path, spy ); - - const QUERY = { - foo: 42, - bar: 'Привет!', - }; - - await do_request( { - pathname: path, - query: QUERY, - } ); - - const [ req ] = spy.mock.calls[ 0 ]; - - expect( url_.parse( req.url, true ).search ).toEqual( '?' + qs_.stringify( QUERY ) ); - } ); - - it( 'basic auth', async () => { - const path = get_path(); - - const spy = jest.fn( ( req, res ) => res.end() ); - - const AUTH = 'user:password'; - - fake.add( path, spy ); - - await do_request( { - pathname: path, - auth: AUTH, - } ); - - const [ req ] = spy.mock.calls[ 0 ]; - - const auth_header = req.headers[ 'authorization' ].replace( /^Basic\s*/, '' ); - expect( Buffer.from( auth_header, 'base64' ).toString() ).toBe( AUTH ); - } ); - - it( 'invalid protocol', async () => { - const path = get_path(); - - const CONTENT = 'Привет!'; - - fake.add( path, { - status_code: 200, - content: CONTENT, - } ); - - expect.assertions( 1 ); - try { - await do_request( { - protocol: 'http', - pathname: path, - } ); - - } catch ( error ) { - expect( de.is_error( error ) ).toBe( true ); - } - } ); - - it.each( [ 'POST', 'PUT', 'PATCH' ] )( '%j, body is a Buffer', async ( method ) => { - const path = get_path(); - - const BODY = Buffer.from( 'Привет!' ); - - const spy = jest.fn( ( req, res ) => res.end() ); - - fake.add( path, spy ); - - await do_request( { - method: method, - pathname: path, - body: BODY, - } ); - - const [ req, , body ] = spy.mock.calls[ 0 ]; - - expect( req.method ).toBe( method ); - expect( req.headers[ 'content-type' ] ).toBe( 'application/octet-stream' ); - expect( Number( req.headers[ 'content-length' ] ) ).toBe( BODY.length ); - expect( Buffer.compare( BODY, body ) ).toBe( 0 ); - } ); - - it.each( [ 'POST', 'PUT', 'PATCH', 'DELETE' ] )( '%j, body is a string', async ( method ) => { - const path = get_path(); - - const BODY = 'Привет!'; - - const spy = jest.fn( ( req, res ) => res.end() ); - - fake.add( path, spy ); - - await do_request( { - method: method, - pathname: path, - body: BODY, - } ); - - const [ req, , body ] = spy.mock.calls[ 0 ]; - - expect( req.method ).toBe( method ); - expect( req.headers[ 'content-type' ] ).toBe( 'text/plain' ); - expect( Number( req.headers[ 'content-length' ] ) ).toBe( Buffer.byteLength( BODY ) ); - expect( body.toString() ).toBe( BODY ); - } ); - - it.each( [ 'POST', 'PUT', 'PATCH', 'DELETE' ] )( '%j, body is a string, custom content-type', async ( method ) => { - const path = get_path(); - - const BODY = 'div { color: red; }'; - - const spy = jest.fn( ( req, res ) => res.end() ); - - fake.add( path, spy ); - - await do_request( { - method: method, - pathname: path, - body: BODY, - headers: { - 'content-type': 'text/css', - }, - } ); - - const [ req, , body ] = spy.mock.calls[ 0 ]; - - expect( req.method ).toBe( method ); - expect( req.headers[ 'content-type' ] ).toBe( 'text/css' ); - expect( Number( req.headers[ 'content-length' ] ) ).toBe( Buffer.byteLength( BODY ) ); - expect( body.toString() ).toBe( BODY ); - } ); - - it.each( [ 'POST', 'PUT', 'PATCH', 'DELETE' ] )( '%j, body is an object', async ( method ) => { - const path = get_path(); - - const BODY = { - id: 42, - text: 'Привет!', - }; - - const spy = jest.fn( ( req, res ) => res.end() ); - - fake.add( path, spy ); - - await do_request( { - method: method, - pathname: path, - body: BODY, - } ); - - const [ req, , body ] = spy.mock.calls[ 0 ]; - const body_string = qs_.stringify( BODY ); - - expect( req.method ).toBe( method ); - expect( req.headers[ 'content-type' ] ).toBe( 'application/x-www-form-urlencoded' ); - expect( Number( req.headers[ 'content-length' ] ) ).toBe( Buffer.byteLength( body_string ) ); - expect( body.toString() ).toBe( body_string ); - } ); - - it.each( [ 'POST', 'PUT', 'PATCH', 'DELETE' ] )( '%j, body is an object, content-type is application/json', async ( method ) => { - const path = get_path(); - - const BODY = { - id: 42, - text: 'Привет!', - }; - - const spy = jest.fn( ( req, res ) => res.end() ); - - fake.add( path, spy ); - - await do_request( { - method: method, - pathname: path, - body: BODY, - headers: { - 'content-type': 'application/json', - }, - } ); - - const [ req, , body ] = spy.mock.calls[ 0 ]; - const body_string = JSON.stringify( BODY ); - - expect( req.method ).toBe( method ); - expect( req.headers[ 'content-type' ] ).toBe( 'application/json' ); - expect( Number( req.headers[ 'content-length' ] ) ).toBe( Buffer.byteLength( body_string ) ); - expect( body.toString() ).toBe( body_string ); - } ); - - it.each( [ 'POST' ] )( '%j, body_compress', async ( method ) => { - const path = get_path(); - - const BODY = 'Привет!'.repeat( 1000 ); - - const spy = jest.fn( ( req, res ) => res.end() ); - - fake.add( path, spy ); - - await do_request( { - method: method, - pathname: path, - body: BODY, - body_compress: true, - } ); - - const [ req, , body ] = spy.mock.calls[ 0 ]; - - expect( req.method ).toBe( method ); - expect( req.headers ).toHaveProperty( 'content-type', 'text/plain' ); - expect( req.headers ).toHaveProperty( 'content-encoding', 'gzip' ); - expect( req.headers ).toHaveProperty( 'transfer-encoding', 'chunked' ); - expect( req.headers ).not.toHaveProperty( 'content-length' ); - - expect( body ).toBeValidGzip(); - expect( body ).toHaveLength( 77 ); - expect( body ).toHaveUngzipValue( BODY ); - } ); - - it.each( [ 'POST' ] )( '%j, body_compress with options', async ( method ) => { - const path = get_path(); - - const BODY = 'Привет!'.repeat( 1000 ); - - const spy = jest.fn( ( req, res ) => res.end() ); - - fake.add( path, spy ); - - await do_request( { - method: method, - pathname: path, - body: BODY, - body_compress: { - level: 1, - }, - } ); - - const [ req, , body ] = spy.mock.calls[ 0 ]; - - expect( req.method ).toBe( method ); - expect( req.headers ).toHaveProperty( 'content-type', 'text/plain' ); - expect( req.headers ).toHaveProperty( 'content-encoding', 'gzip' ); - expect( req.headers ).toHaveProperty( 'transfer-encoding', 'chunked' ); - expect( req.headers ).not.toHaveProperty( 'content-length' ); - - expect( body ).toBeValidGzip(); - expect( body ).toHaveLength( 134 ); - expect( body ).toHaveUngzipValue( BODY ); - } ); - - describe( 'errors', () => { - - it( '2xx, custom is_error', async () => { - const path = get_path(); - const status_code = 200; - - fake.add( path, { - status_code: status_code, - } ); - - expect.assertions( 2 ); - try { - await do_request( { - pathname: path, - is_error: () => true, - } ); - - } catch ( error ) { - expect( de.is_error( error ) ).toBe( true ); - expect( error.error.status_code ).toBe( status_code ); - } - } ); - - it( '4xx, max_retries=1', async () => { - const path = get_path(); - const status_code = 404; - - const spy = jest.fn( ( res ) => res.end() ); - - fake.add( path, [ - { - status_code: status_code, - }, - spy, - ] ); - - expect.assertions( 3 ); - try { - await do_request( { - pathname: path, - max_retries: 1, - } ); - - } catch ( error ) { - expect( de.is_error( error ) ).toBe( true ); - expect( error.error.status_code ).toBe( status_code ); - expect( spy.mock.calls ).toHaveLength( 0 ); - } - } ); - - it( '4xx, max_retries=1, custom is_retry_allowed', async () => { - const path = get_path(); - const status_code = 404; - const CONTENT = 'Привет!'; - - fake.add( path, [ - { - status_code: status_code, - }, - { - status_code: 200, - content: CONTENT, - }, - ] ); - - const result = await do_request( { - pathname: path, - max_retries: 1, - is_retry_allowed: () => true, - } ); - - expect( result.status_code ).toBe( 200 ); - expect( result.body.toString() ).toBe( CONTENT ); - } ); - - it( '5xx, max_retries=0', async () => { - const path = get_path(); - const status_code = 503; - - const spy = jest.fn( ( res ) => res.end() ); - - fake.add( path, [ - { - status_code: status_code, - }, - spy, - ] ); - - expect.assertions( 3 ); - try { - await do_request( { - pathname: path, - max_retries: 0, - } ); - - } catch ( error ) { - expect( de.is_error( error ) ).toBe( true ); - expect( error.error.status_code ).toBe( status_code ); - expect( spy.mock.calls ).toHaveLength( 0 ); - } - } ); - - it( '5xx, max_retries=1, custom is_retry_allowed', async () => { - const path = get_path(); - const status_code = 503; - - const spy = jest.fn( ( res ) => res.end() ); - - fake.add( path, [ - { - status_code: status_code, - }, - spy, - ] ); - - expect.assertions( 3 ); - try { - await do_request( { - pathname: path, - max_retries: 1, - is_retry_allowed: () => false, - } ); - - } catch ( error ) { - expect( de.is_error( error ) ).toBe( true ); - expect( error.error.status_code ).toBe( status_code ); - expect( spy.mock.calls ).toHaveLength( 0 ); - } - } ); - - it( '5xx, max_retries=1', async () => { - const path = get_path(); - const status_code = 503; - const CONTENT = 'Привет!'; - - fake.add( path, [ - { - status_code: status_code, - }, - { - status_code: 200, - content: CONTENT, - }, - ] ); - - const result = await do_request( { - pathname: path, - max_retries: 1, - } ); - - expect( result.status_code ).toBe( 200 ); - expect( result.body.toString() ).toBe( CONTENT ); - } ); - - it( '5xx, max_retries=1, retry_timeout=0', async () => { - const path = get_path(); - const status_code = 503; - const CONTENT = 'Привет!'; - - fake.add( path, [ - { - status_code: status_code, - }, - { - status_code: 200, - content: CONTENT, - }, - ] ); - - const result = await do_request( { - pathname: path, - max_retries: 1, - retry_timeout: 0, - } ); - - expect( result.status_code ).toBe( 200 ); - expect( result.body.toString() ).toBe( CONTENT ); - } ); - - it( '5xx, max_retries=1, retry_timeout=100', async () => { - const path = get_path(); - - let end; - - fake.add( path, [ - { - status_code: 503, - }, - ( req, res ) => { - end = Date.now(); - - res.statusCode = 200; - res.end(); - }, - ] ); - - const RETRY_TIMEOUT = 100; - - const start = Date.now(); - await do_request( { - pathname: path, - max_retries: 1, - retry_timeout: RETRY_TIMEOUT, - } ); - - expect( end - start > RETRY_TIMEOUT ).toBe( true ); - } ); - - it.each( [ 'POST', 'PATCH' ] )( '5xx, %j, max_retries=1, no retry', async ( method ) => { - const path = get_path(); - const status_code = 503; - - const spy = jest.fn( ( res ) => res.end() ); - - fake.add( path, [ - { - status_code: status_code, - }, - spy, - ] ); - - expect.assertions( 2 ); - try { - await do_request( { - pathname: path, - method: method, - max_retries: 1, - } ); - - } catch ( error ) { - expect( de.is_error( error ) ).toBe( true ); - expect( error.error.status_code ).toBe( status_code ); - } - } ); - - it( 'timeout', async () => { - const path = get_path(); - - fake.add( path, { - status_code: 200, - // Тут wait работает так: сперва 100 мс таймаут, а потом уже ответ. - // Следующий пример про наборот, когда сразу statusCode = 200 и res.write(), - // а через таймаут только res.end(). - // - wait: 100, - } ); - - expect.assertions( 2 ); - try { - await do_request( { - pathname: path, - timeout: 50, - } ); - - } catch ( error ) { - expect( de.is_error( error ) ).toBe( true ); - expect( error.error.id ).toBe( de.ERROR_ID.REQUEST_TIMEOUT ); - } - } ); - - it( '200, timeout, incomplete response', async () => { - const path = get_path(); - - fake.add( path, async ( req, res ) => { - res.statusCode = 200; - res.write( 'Привет!' ); - await wait_for_value( null, 100 ); - res.end(); - } ); - - expect.assertions( 2 ); - try { - await do_request( { - pathname: path, - timeout: 50, - } ); - - } catch ( error ) { - expect( de.is_error( error ) ).toBe( true ); - expect( error.error.id ).toBe( de.ERROR_ID.REQUEST_TIMEOUT ); - } - } ); - - } ); - - describe( 'content-encoding', () => { - - describe( 'zlib', () => { - it( 'decompress', async () => { - const path = get_path(); - - const CONTENT = 'Привет!'; - - fake.add( path, function( req, res ) { - const buffer = gzipSync( Buffer.from( CONTENT ) ); - - res.setHeader( 'content-encoding', 'gzip' ); - res.setHeader( 'content-length', Buffer.byteLength( buffer ) ); - res.end( buffer ); - } ); - - const result = await do_request( { - pathname: path, - } ); - - expect( result.body.toString() ).toBe( CONTENT ); - } ); - - it( 'decompress with error', async () => { - const path = get_path(); - - const CONTENT = 'Привет!'; - - fake.add( path, function( req, res ) { - // Шлем контент, не являющийся gzip'ом. - const buffer = Buffer.from( CONTENT ); - - res.setHeader( 'content-encoding', 'gzip' ); - res.setHeader( 'content-length', Buffer.byteLength( buffer ) ); - res.end( buffer ); - } ); - - expect.assertions( 3 ); - try { - await do_request( { - pathname: path, - } ); - - } catch ( error ) { - expect( de.is_error( error ) ).toBe( true ); - expect( error.error.id ).toBe( 'UNKNOWN_ERROR' ); - expect( error.error.code ).toBe( 'Z_DATA_ERROR' ); - } - } ); - } ); - - describe( 'zstd', () => { - it( 'decompress', async () => { - const path = get_path(); - - const CONTENT = 'Привет!'; - const buffer = Buffer.from( CONTENT ); - const stream = new Duplex(); - stream.push( await compress( buffer ) ); - stream.push( null ); - - fake.add( path, function( req, res ) { - res.setHeader( 'content-encoding', 'zstd' ); - - stream.pipe( res ); - } ); - - const result = await do_request( { - pathname: path, - } ); - - expect( result.body.toString() ).toBe( CONTENT ); - } ); - - it( 'decompress with error', async () => { - const path = get_path(); - - const CONTENT = 'Привет!'; - - fake.add( path, function( req, res ) { - const buffer = Buffer.from( CONTENT ); - - res.setHeader( 'content-encoding', 'zstd' ); - res.end( buffer ); - } ); - - expect.assertions( 3 ); - try { - await do_request( { - pathname: path, - } ); - - } catch ( error ) { - expect( de.is_error( error ) ).toBe( true ); - expect( error.error.id ).toBe( 'UNKNOWN_ERROR' ); - expect( error.error.code ).toBe( 'GenericFailure' ); - } - } ); - } ); - - } ); - - describe( 'agent', () => { - - it( 'is an object', async () => { - const path = get_path(); - - fake.add( path, { - status_code: 200, - } ); - - const agent = { - keepAlive: true, - }; - - await do_request( { - pathname: path, - agent: agent, - } ); - const result2 = await do_request( { - pathname: path, - agent: agent, - } ); - - expect( result2.timestamps.socket === result2.timestamps.tcp_connection ).toBe( true ); - } ); - - it( 'is an http.Agent instance', async () => { - const path = get_path(); - - fake.add( path, { - status_code: 200, - } ); - - const agent = new http_.Agent( { - keepAlive: true, - } ); - - await do_request( { - pathname: path, - agent: agent, - } ); - const result2 = await do_request( { - pathname: path, - agent: agent, - } ); - - expect( result2.timestamps.socket === result2.timestamps.tcp_connection ).toBe( true ); - } ); - - } ); - - describe( 'family', () => { - - it( 'family: 6', async () => { - const path = get_path(); - - const CONTENT = 'Привет!'; - - fake_ipv6.add( path, { - status_code: 200, - content: CONTENT, - } ); - - const result = await do_request( { - family: 6, - hostname: 'localhost', - method: 'GET', - pathname: path, - port: PORT_IPV6, - } ); - - expect( result.status_code ).toBe( 200 ); - expect( Buffer.isBuffer( result.body ) ).toBe( true ); - expect( result.body.toString() ).toBe( CONTENT ); - } ); - - } ); - - describe( 'cancel', () => { - - it( 'cancel before request ended', async () => { - const path = get_path(); - - fake.add( path, { - status_code: 200, - wait: 200, - } ); - - const error = de.error( { - id: 'SOME_ERROR', - } ); - const cancel = new de.Cancel(); - setTimeout( () => { - cancel.cancel( error ); - }, 50 ); - - expect.assertions( 3 ); - try { - await do_request( { - pathname: path, - }, undefined, cancel ); - - } catch ( e ) { - expect( de.is_error( e ) ).toBe( true ); - expect( e.error.id ).toBe( de.ERROR_ID.HTTP_REQUEST_ABORTED ); - expect( e.error.reason ).toBe( error ); - } - } ); - - it( 'cancel after request ended', async () => { - const path = get_path(); - - const CONTENT = 'Привет!'; - fake.add( path, { - status_code: 200, - content: CONTENT, - wait: 50, - } ); - - const error = de.error( { - id: 'SOME_ERROR', - } ); - const cancel = new de.Cancel(); - setTimeout( () => { - cancel.cancel( error ); - }, 100 ); - - const result = await do_request( { - pathname: path, - }, undefined, cancel ); - - expect( result.body.toString() ).toBe( CONTENT ); - } ); - - } ); - - } ); - - describe( 'https', () => { - - const PORT = 9001; - - const do_request = get_do_request( { - protocol: 'https:', - hostname: '127.0.0.1', - port: PORT, - pathname: '/', - } ); - - let server_key; - let server_cert; - try { - server_key = fs_.readFileSync( path_.join( __dirname, 'server.key' ) ); - server_cert = fs_.readFileSync( path_.join( __dirname, 'server.crt' ) ); - - } catch ( e ) { - throw Error( - 'Generate https keys:\n' + - ' cd tests\n' + - ' ./gen-certs.sh\n', - ); - } - - const fake = new Server( { - module: https_, - listen_options: { - port: PORT, - }, - options: { - key: server_key, - cert: server_cert, - }, - } ); - - beforeAll( () => fake.start() ); - afterAll( () => fake.stop() ); - - it( 'GET', async () => { - const path = get_path(); - - const CONTENT = 'Привет!'; - - fake.add( path, { - status_code: 200, - content: CONTENT, - } ); - - const result = await do_request( { - rejectUnauthorized: false, - pathname: path, - } ); - - expect( Buffer.isBuffer( result.body ) ).toBe( true ); - expect( result.body.toString() ).toBe( CONTENT ); - } ); - - } ); - - describe( 'default options', () => { - - describe( 'is_error', () => { - - const is_error = request.DEFAULT_OPTIONS.is_error; - - it.each( [ de.ERROR_ID.TCP_CONNECTION_TIMEOUT, de.ERROR_ID.REQUEST_TIMEOUT ] )( 'error_id=%j', ( error_id ) => { - const error = de.error( { - id: error_id, - } ); - - expect( is_error( error ) ).toBe( true ); - } ); - - it.each( [ 200, 301, 302, 303, 304 ] )( 'status_code=%j', ( status_code ) => { - const error = de.error( { - status_code: status_code, - } ); - - expect( is_error( error ) ).toBe( false ); - } ); - - it.each( [ 400, 401, 402, 403, 404, 500, 501, 503 ] )( 'status_code=%j', ( status_code ) => { - const error = de.error( { - status_code: status_code, - } ); - - expect( is_error( error ) ).toBe( true ); - } ); - - } ); - - } ); - - describe( 'aborted request', () => { - - describe( 'no bytes sent', () => { - const server = http_.createServer( ( req, res ) => { - setTimeout( () => { - res.socket.destroy(); - }, 100 ); - } ); - const PORT = 9002; - - beforeAll( () => server_listen( server, PORT ) ); - afterAll( () => server_close( server ) ); - - it( '', async () => { - const do_request = get_do_request( { - protocol: 'http:', - hostname: '127.0.0.1', - port: PORT, - pathname: '/', - } ); - - const path = get_path(); - - expect.assertions( 2 ); - try { - await do_request( { - pathname: path, - } ); - - } catch ( error ) { - expect( de.is_error( error ) ).toBe( true ); - expect( error.error.id ).toBe( de.ERROR_ID.HTTP_UNKNOWN_ERROR ); - } - } ); - } ); - - describe( 'some bytes sent', () => { - const server = http_.createServer( ( req, res ) => { - res.write( 'Hello!' ); - setTimeout( () => { - res.socket.destroy(); - }, 100 ); - } ); - const PORT = 9003; - - const do_request = get_do_request( { - protocol: 'http:', - hostname: '127.0.0.1', - port: PORT, - } ); - - beforeAll( () => server_listen( server, PORT ) ); - afterAll( () => server_close( server ) ); - - it( '', async () => { - expect.assertions( 2 ); - try { - await do_request(); - - } catch ( error ) { - expect( de.is_error( error ) ).toBe( true ); - expect( error.error.id ).toBe( de.ERROR_ID.INCOMPLETE_RESPONSE ); - } - } ); - - it( 'cancelled', async () => { - const cancel = new de.Cancel(); - const error = de.error( { - id: 'SOME_ERROR', - } ); - setTimeout( () => { - cancel.cancel( error ); - }, 50 ); - - expect.assertions( 3 ); - try { - await do_request( {}, undefined, cancel ); - - } catch ( e ) { - expect( de.is_error( e ) ).toBe( true ); - expect( e.error.id ).toBe( de.ERROR_ID.HTTP_REQUEST_ABORTED ); - expect( e.error.reason ).toBe( error ); - } - } ); - - } ); - - describe( 'tcp connection timeout', () => { - const PORT = 9004; - const server = http_.createServer( ( req, res ) => { - setTimeout( () => res.end(), 100 ); - } ); - - const do_request = get_do_request( { - protocol: 'http:', - hostname: '127.0.0.1', - port: PORT, - } ); - - beforeAll( () => server_listen( server, PORT ) ); - afterAll( () => server_close( server ) ); - - it( '', async () => { - expect.assertions( 2 ); - try { - const agent = { - keepAlive: false, - maxSockets: 1, - }; - - // Делаем запрос и занимаем весь один сокет. - do_request( { - agent: agent, - } ); - // Так что этот запрос не сможет законнектиться. - await do_request( { - agent: agent, - // Тут должно быть что-то меньшее, чем время, за которое отвечает сервер. - timeout: 50, - } ); - - } catch ( error ) { - expect( de.is_error( error ) ).toBe( true ); - expect( error.error.id ).toBe( de.ERROR_ID.TCP_CONNECTION_TIMEOUT ); - } - } ); - - } ); - - } ); - -} ); - -function server_listen( server, port ) { - return new Promise( ( resolve ) => { - server.listen( port, resolve ); - } ); -} - -function server_close( server ) { - return new Promise( ( resolve ) => { - server.close( resolve ); - } ); -} diff --git a/tests/request.test.ts b/tests/request.test.ts new file mode 100644 index 0000000..2a62432 --- /dev/null +++ b/tests/request.test.ts @@ -0,0 +1,1197 @@ +/* eslint-disable jest/no-conditional-expect */ +import url_ from 'url'; + +import qs_ from 'querystring'; + +import { getPath, waitForValue } from './helpers'; +import Server from './server'; +import type { DescriptRequestOptions } from '../lib/request'; +import request, { DEFAULT_OPTIONS } from '../lib/request'; +import * as de from '../lib'; +import { Duplex } from 'stream'; +import { compress } from '@fengkx/zstd-napi'; +import { gzipSync } from 'node:zlib'; +import https_ from 'https'; +import http from 'http'; +import fs_ from 'fs'; +import path_ from 'path'; +import type Cancel from '../lib/cancel'; +import type Logger from '../lib/logger'; + + +// --------------------------------------------------------------------------------------------------------------- // + +function getDoRequest(defaultOptions: DescriptRequestOptions) { + return function doRequest(options: DescriptRequestOptions = {}, logger?: Logger, cancel?: Cancel) { + logger = logger || new de.Logger({ debug: true }); + cancel = cancel || new de.Cancel(); + + return request({ ...defaultOptions, ...options }, logger, cancel); + }; +} + +// --------------------------------------------------------------------------------------------------------------- // + +describe('request', () => { + + describe('http', () => { + + const PORT = 9000; + const PORT_IPV6 = 9006; + + const doRequest = getDoRequest({ + protocol: 'http:', + hostname: '127.0.0.1', + port: PORT, + pathname: '/', + }); + + const fake = new Server({ + module: http, + listen_options: { + port: PORT, + }, + }); + + const fakeIpv6 = new Server({ + module: http, + listen_options: { + host: '::1', + port: PORT_IPV6, + ipv6Only: true, + }, + }); + + beforeAll(() => Promise.all([ + fake.start(), + fakeIpv6.start(), + ])); + afterAll(() => Promise.all([ + fake.stop(), + fakeIpv6.stop(), + ])); + + it.each([ 'GET', 'DELETE' ])('%j', async(method) => { + const path = getPath(); + + const CONTENT = 'Привет!'; + + fake.add(path, { + statusCode: 200, + content: CONTENT, + }); + + const result = await doRequest({ + method: method, + pathname: path, + }); + + expect(result.statusCode).toBe(200); + expect(Buffer.isBuffer(result.body)).toBe(true); + expect(result.body?.toString()).toBe(CONTENT); + }); + + it('pathname always starts with /', async() => { + const path = getPath(); + + const spy = jest.fn((req, res) => res.end()); + + fake.add(path, spy); + + await doRequest({ + pathname: path.replace(/^\//, ''), + }); + + const [ req ] = spy.mock.calls[ 0 ]; + + expect(url_.parse(req.url).pathname).toBe(path); + }); + + it('sets accept-encoding to gzip,deflate', async() => { + const path = getPath(); + + const spy = jest.fn((req, res) => res.end()); + + fake.add(path, spy); + + await doRequest({ + pathname: path, + }); + + const [ req ] = spy.mock.calls[ 0 ]; + + expect(req.headers[ 'accept-encoding' ]).toBe('gzip,deflate'); + }); + + it('adds gzip,deflate to accept-encoding', async() => { + const path = getPath(); + + const spy = jest.fn((req, res) => res.end()); + + fake.add(path, spy); + + await doRequest({ + pathname: path, + headers: { + 'accept-encoding': 'compress', + }, + }); + + const [ req ] = spy.mock.calls[ 0 ]; + + expect(req.headers[ 'accept-encoding' ]).toBe('gzip,deflate,compress'); + }); + + it('sends lower-cased headers', async() => { + const path = getPath(); + + const spy = jest.fn((req, res) => res.end()); + + fake.add(path, spy); + + await doRequest({ + pathname: path, + headers: { + 'x-request-foo': 'Foo', + 'X-REQUEST-BAR': 'bAr', + 'X-Request-Quu': 'quU', + }, + }); + + const [ req ] = spy.mock.calls[ 0 ]; + + expect(req.headers[ 'x-request-foo' ]).toBe('Foo'); + expect(req.headers[ 'x-request-bar' ]).toBe('bAr'); + expect(req.headers[ 'x-request-quu' ]).toBe('quU'); + }); + + it('query', async() => { + const path = getPath(); + + const spy = jest.fn((req, res) => res.end()); + + fake.add(path, spy); + + const QUERY = { + foo: 42, + bar: 'Привет!', + }; + + await doRequest({ + pathname: path, + query: QUERY, + }); + + const [ req ] = spy.mock.calls[ 0 ]; + + expect(url_.parse(req.url, true).search).toEqual('?' + qs_.stringify(QUERY)); + }); + + it('basic auth', async() => { + const path = getPath(); + + const spy = jest.fn((req, res) => res.end()); + + const AUTH = 'user:password'; + + fake.add(path, spy); + + await doRequest({ + pathname: path, + auth: AUTH, + }); + + const [ req ] = spy.mock.calls[ 0 ]; + + const authHeader = req.headers[ 'authorization' ].replace(/^Basic\s*/, ''); + expect(Buffer.from(authHeader, 'base64').toString()).toBe(AUTH); + }); + + it('invalid protocol', async() => { + const path = getPath(); + + const CONTENT = 'Привет!'; + + fake.add(path, { + statusCode: 200, + content: CONTENT, + }); + + expect.assertions(1); + try { + await doRequest({ + protocol: 'http', + pathname: path, + }); + + } catch (error) { + expect(de.isError(error)).toBe(true); + } + }); + + it.each([ 'POST', 'PUT', 'PATCH' ])('%j, body is a Buffer', async(method) => { + const path = getPath(); + + const BODY = Buffer.from('Привет!'); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const spy = jest.fn((req: http.IncomingMessage, res: http.OutgoingMessage, body: any) => res.end()); + + fake.add(path, spy); + + await doRequest({ + method: method, + pathname: path, + body: BODY, + }); + + const [ req, , body ] = spy.mock.calls[ 0 ]; + + expect(req.method).toBe(method); + expect(req.headers[ 'content-type' ]).toBe('application/octet-stream'); + expect(Number(req.headers[ 'content-length' ])).toBe(BODY.length); + expect(Buffer.compare(BODY, body)).toBe(0); + }); + + it.each([ 'POST', 'PUT', 'PATCH', 'DELETE' ])('%j, body is a string', async(method) => { + const path = getPath(); + + const BODY = 'Привет!'; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const spy = jest.fn((req: http.IncomingMessage, res: http.OutgoingMessage, body: any) => res.end()); + + fake.add(path, spy); + + await doRequest({ + method: method, + pathname: path, + body: BODY, + }); + + const [ req, , body ] = spy.mock.calls[ 0 ]; + + expect(req.method).toBe(method); + expect(req.headers[ 'content-type' ]).toBe('text/plain'); + expect(Number(req.headers[ 'content-length' ])).toBe(Buffer.byteLength(BODY)); + expect(body.toString()).toBe(BODY); + }); + + it.each([ 'POST', 'PUT', 'PATCH', 'DELETE' ])('%j, body is a string, custom content-type', async(method) => { + const path = getPath(); + + const BODY = 'div { color: red; }'; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const spy = jest.fn((req: http.IncomingMessage, res: http.OutgoingMessage, body: any) => res.end()); + + fake.add(path, spy); + + await doRequest({ + method: method, + pathname: path, + body: BODY, + headers: { + 'content-type': 'text/css', + }, + }); + + const [ req, , body ] = spy.mock.calls[ 0 ]; + + expect(req.method).toBe(method); + expect(req.headers[ 'content-type' ]).toBe('text/css'); + expect(Number(req.headers[ 'content-length' ])).toBe(Buffer.byteLength(BODY)); + expect(body.toString()).toBe(BODY); + }); + + it.each([ 'POST', 'PUT', 'PATCH', 'DELETE' ])('%j, body is an object', async(method) => { + const path = getPath(); + + const BODY = { + id: 42, + text: 'Привет!', + }; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const spy = jest.fn((req: http.IncomingMessage, res: http.OutgoingMessage, body: any) => res.end()); + + fake.add(path, spy); + + await doRequest({ + method: method, + pathname: path, + body: BODY, + }); + + const [ req, , body ] = spy.mock.calls[ 0 ]; + const bodyString = qs_.stringify(BODY); + + expect(req.method).toBe(method); + expect(req.headers[ 'content-type' ]).toBe('application/x-www-form-urlencoded'); + expect(Number(req.headers[ 'content-length' ])).toBe(Buffer.byteLength(bodyString)); + expect(body.toString()).toBe(bodyString); + }); + + it.each([ 'POST', 'PUT', 'PATCH', 'DELETE' ])('%j, body is an object, content-type is application/json', async(method) => { + const path = getPath(); + + const BODY = { + id: 42, + text: 'Привет!', + }; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const spy = jest.fn((req: http.IncomingMessage, res: http.OutgoingMessage, body: any) => res.end()); + + fake.add(path, spy); + + await doRequest({ + method: method, + pathname: path, + body: BODY, + headers: { + 'content-type': 'application/json', + }, + }); + + const [ req, , body ] = spy.mock.calls[ 0 ]; + const bodyString = JSON.stringify(BODY); + + expect(req.method).toBe(method); + expect(req.headers[ 'content-type' ]).toBe('application/json'); + expect(Number(req.headers[ 'content-length' ])).toBe(Buffer.byteLength(bodyString)); + expect(body.toString()).toBe(bodyString); + }); + + it.each([ 'POST' ])('%j, bodyCompress', async(method) => { + const path = getPath(); + + const BODY = 'Привет!'.repeat(1000); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const spy = jest.fn((req: http.IncomingMessage, res: http.OutgoingMessage, body: any) => res.end()); + + fake.add(path, spy); + + await doRequest({ + method: method, + pathname: path, + body: BODY, + bodyCompress: {}, + }); + + const [ req, , body ] = spy.mock.calls[ 0 ]; + + expect(req.method).toBe(method); + expect(req.headers).toHaveProperty('content-type', 'text/plain'); + expect(req.headers).toHaveProperty('content-encoding', 'gzip'); + expect(req.headers).toHaveProperty('transfer-encoding', 'chunked'); + expect(req.headers).not.toHaveProperty('content-length'); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(body).toBeValidGzip(); + expect(body).toHaveLength(77); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(body).toHaveUngzipValue(BODY); + }); + + it.each([ 'POST' ])('%j, bodyCompress with options', async(method) => { + const path = getPath(); + + const BODY = 'Привет!'.repeat(1000); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const spy = jest.fn((req: http.IncomingMessage, res: http.OutgoingMessage, body: any) => res.end()); + + fake.add(path, spy); + + await doRequest({ + method: method, + pathname: path, + body: BODY, + bodyCompress: { + level: 1, + }, + }); + + const [ req, , body ] = spy.mock.calls[ 0 ]; + + expect(req.method).toBe(method); + expect(req.headers).toHaveProperty('content-type', 'text/plain'); + expect(req.headers).toHaveProperty('content-encoding', 'gzip'); + expect(req.headers).toHaveProperty('transfer-encoding', 'chunked'); + expect(req.headers).not.toHaveProperty('content-length'); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(body).toBeValidGzip(); + expect(body).toHaveLength(134); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(body).toHaveUngzipValue(BODY); + }); + + describe('errors', () => { + + it('2xx, custom isError', async() => { + const path = getPath(); + const statusCode = 200; + + fake.add(path, { + statusCode: statusCode, + }); + + expect.assertions(2); + try { + await doRequest({ + pathname: path, + isError: () => true, + }); + + } catch (error) { + expect(de.isError(error)).toBe(true); + expect(error.error.statusCode).toBe(statusCode); + } + }); + + it('4xx, maxRetries=1', async() => { + const path = getPath(); + const statusCode = 404; + + const spy = jest.fn((res) => res.end()); + + fake.add(path, [ + { + statusCode: statusCode, + }, + spy, + ]); + + expect.assertions(3); + try { + await doRequest({ + pathname: path, + maxRetries: 1, + }); + + } catch (error) { + expect(de.isError(error)).toBe(true); + expect(error.error.statusCode).toBe(statusCode); + expect(spy.mock.calls).toHaveLength(0); + } + }); + + it('4xx, maxRetries=1, custom isRetryAllowed', async() => { + const path = getPath(); + const statusCode = 404; + const CONTENT = 'Привет!'; + + fake.add(path, [ + { + statusCode: statusCode, + }, + { + statusCode: 200, + content: CONTENT, + }, + ]); + + const result = await doRequest({ + pathname: path, + maxRetries: 1, + isRetryAllowed: () => true, + }); + + expect(result.statusCode).toBe(200); + expect(result.body?.toString()).toBe(CONTENT); + }); + + it('5xx, maxRetries=0', async() => { + const path = getPath(); + const statusCode = 503; + + const spy = jest.fn((res) => res.end()); + + fake.add(path, [ + { + statusCode: statusCode, + }, + spy, + ]); + + expect.assertions(3); + try { + await doRequest({ + pathname: path, + maxRetries: 0, + }); + + } catch (error) { + expect(de.isError(error)).toBe(true); + expect(error.error.statusCode).toBe(statusCode); + expect(spy.mock.calls).toHaveLength(0); + } + }); + + it('5xx, maxRetries=1, custom isRetryAllowed', async() => { + const path = getPath(); + const statusCode = 503; + + const spy = jest.fn((res) => res.end()); + + fake.add(path, [ + { + statusCode: statusCode, + }, + spy, + ]); + + expect.assertions(3); + try { + await doRequest({ + pathname: path, + maxRetries: 1, + isRetryAllowed: () => false, + }); + + } catch (error) { + expect(de.isError(error)).toBe(true); + expect(error.error.statusCode).toBe(statusCode); + expect(spy.mock.calls).toHaveLength(0); + } + }); + + it('5xx, maxRetries=1', async() => { + const path = getPath(); + const statusCode = 503; + const CONTENT = 'Привет!'; + + fake.add(path, [ + { + statusCode: statusCode, + }, + { + statusCode: 200, + content: CONTENT, + }, + ]); + + const result = await doRequest({ + pathname: path, + maxRetries: 1, + }); + + expect(result.statusCode).toBe(200); + expect(result.body?.toString()).toBe(CONTENT); + }); + + it('5xx, maxRetries=1, retryTimeout=0', async() => { + const path = getPath(); + const statusCode = 503; + const CONTENT = 'Привет!'; + + fake.add(path, [ + { + statusCode: statusCode, + }, + { + statusCode: 200, + content: CONTENT, + }, + ]); + + const result = await doRequest({ + pathname: path, + maxRetries: 1, + retryTimeout: 0, + }); + + expect(result.statusCode).toBe(200); + expect(result.body?.toString()).toBe(CONTENT); + }); + + it('5xx, maxRetries=1, retryTimeout=100', async() => { + const path = getPath(); + + let end = 0; + + fake.add(path, [ + { + statusCode: 503, + }, + (req: http.IncomingMessage, res: http.ServerResponse) => { + end = Date.now(); + + res.statusCode = 200; + res.end(); + }, + ]); + + const retryTimeout = 100; + + const start = Date.now(); + await doRequest({ + pathname: path, + maxRetries: 1, + retryTimeout: retryTimeout, + }); + + expect(end - start > retryTimeout).toBe(true); + }); + + it.each([ 'POST', 'PATCH' ])('5xx, %j, maxRetries=1, no retry', async(method) => { + const path = getPath(); + const statusCode = 503; + + const spy = jest.fn((res) => res.end()); + + fake.add(path, [ + { + statusCode: statusCode, + }, + spy, + ]); + + expect.assertions(2); + try { + await doRequest({ + pathname: path, + method: method, + maxRetries: 1, + }); + + } catch (error) { + expect(de.isError(error)).toBe(true); + expect(error.error.statusCode).toBe(statusCode); + } + }); + + it('timeout', async() => { + const path = getPath(); + + fake.add(path, { + statusCode: 200, + // Тут wait работает так: сперва 100 мс таймаут, а потом уже ответ. + // Следующий пример про наборот, когда сразу statusCode = 200 и res.write(), + // а через таймаут только res.end(). + // + wait: 100, + }); + + expect.assertions(2); + try { + await doRequest({ + pathname: path, + timeout: 50, + }); + + } catch (error) { + expect(de.isError(error)).toBe(true); + expect(error.error.id).toBe(de.ERROR_ID.REQUEST_TIMEOUT); + } + }); + + it('200, timeout, incomplete response', async() => { + const path = getPath(); + + fake.add(path, async(req: http.IncomingMessage, res: http.ServerResponse) => { + res.statusCode = 200; + res.write('Привет!'); + await waitForValue(null, 100); + res.end(); + }); + + expect.assertions(2); + try { + await doRequest({ + pathname: path, + timeout: 50, + }); + + } catch (error) { + expect(de.isError(error)).toBe(true); + expect(error.error.id).toBe(de.ERROR_ID.REQUEST_TIMEOUT); + } + }); + + }); + + describe('content-encoding', () => { + + describe('zlib', () => { + it('decompress', async() => { + const path = getPath(); + + const CONTENT = 'Привет!'; + + fake.add(path, function(req: http.IncomingMessage, res: http.OutgoingMessage) { + const buffer = gzipSync(Buffer.from(CONTENT)); + + res.setHeader('content-encoding', 'gzip'); + res.setHeader('content-length', Buffer.byteLength(buffer)); + res.end(buffer); + }); + + const result = await doRequest({ + pathname: path, + }); + + expect(result.body?.toString()).toBe(CONTENT); + }); + + it('decompress with error', async() => { + const path = getPath(); + + const CONTENT = 'Привет!'; + + fake.add(path, function(req: http.IncomingMessage, res: http.OutgoingMessage) { + // Шлем контент, не являющийся gzip'ом. + const buffer = Buffer.from(CONTENT); + + res.setHeader('content-encoding', 'gzip'); + res.setHeader('content-length', Buffer.byteLength(buffer)); + res.end(buffer); + }); + + expect.assertions(3); + try { + await doRequest({ + pathname: path, + }); + + } catch (error) { + expect(de.isError(error)).toBe(true); + expect(error.error.id).toBe('UNKNOWN_ERROR'); + expect(error.error.code).toBe('Z_DATA_ERROR'); + } + }); + }); + + describe('zstd', () => { + it('decompress', async() => { + const path = getPath(); + + const CONTENT = 'Привет!'; + const buffer = Buffer.from(CONTENT); + const stream = new Duplex(); + stream.push(await compress(buffer)); + stream.push(null); + + fake.add(path, function(req: http.IncomingMessage, res: http.OutgoingMessage) { + res.setHeader('content-encoding', 'zstd'); + + stream.pipe(res); + }); + + const result = await doRequest({ + pathname: path, + }); + + expect(result.body?.toString()).toBe(CONTENT); + }); + + it('decompress with error', async() => { + const path = getPath(); + + const CONTENT = 'Привет!'; + + fake.add(path, function(req: http.IncomingMessage, res: http.OutgoingMessage) { + const buffer = Buffer.from(CONTENT); + + res.setHeader('content-encoding', 'zstd'); + res.end(buffer); + }); + + expect.assertions(3); + try { + await doRequest({ + pathname: path, + }); + + } catch (error) { + expect(de.isError(error)).toBe(true); + expect(error.error.id).toBe('UNKNOWN_ERROR'); + expect(error.error.code).toBe('GenericFailure'); + } + }); + }); + + }); + + describe('agent', () => { + + it('is an object', async() => { + const path = getPath(); + + fake.add(path, { + statusCode: 200, + }); + + const agent = { + keepAlive: true, + }; + + await doRequest({ + pathname: path, + agent: agent, + }); + const result2 = await doRequest({ + pathname: path, + agent: agent, + }); + + expect(result2.timestamps.socket === result2.timestamps.tcpConnection).toBe(true); + }); + + it('is an http.Agent instance', async() => { + const path = getPath(); + + fake.add(path, { + statusCode: 200, + }); + + const agent = new http.Agent({ + keepAlive: true, + }); + + await doRequest({ + pathname: path, + agent: agent, + }); + const result2 = await doRequest({ + pathname: path, + agent: agent, + }); + + expect(result2.timestamps.socket === result2.timestamps.tcpConnection).toBe(true); + }); + + }); + + describe('family', () => { + + it('family: 6', async() => { + const path = getPath(); + + const CONTENT = 'Привет!'; + + fakeIpv6.add(path, { + statusCode: 200, + content: CONTENT, + }); + + const result = await doRequest({ + family: 6, + hostname: 'localhost', + method: 'GET', + pathname: path, + port: PORT_IPV6, + }); + + expect(result.statusCode).toBe(200); + expect(Buffer.isBuffer(result.body)).toBe(true); + expect(result.body?.toString()).toBe(CONTENT); + }); + + }); + + describe('cancel', () => { + + it('cancel before request ended', async() => { + const path = getPath(); + + fake.add(path, { + statusCode: 200, + wait: 200, + }); + + const error = de.error({ + id: 'SOME_ERROR', + }); + const cancel = new de.Cancel(); + setTimeout(() => { + cancel.cancel(error); + }, 50); + + expect.assertions(3); + try { + await doRequest({ + pathname: path, + }, undefined, cancel); + + } catch (e) { + expect(de.isError(e)).toBe(true); + expect(e.error.id).toBe(de.ERROR_ID.HTTP_REQUEST_ABORTED); + expect(e.error.reason).toBe(error); + } + }); + + it('cancel after request ended', async() => { + const path = getPath(); + + const CONTENT = 'Привет!'; + fake.add(path, { + statusCode: 200, + content: CONTENT, + wait: 50, + }); + + const error = de.error({ + id: 'SOME_ERROR', + }); + const cancel = new de.Cancel(); + setTimeout(() => { + cancel.cancel(error); + }, 100); + + const result = await doRequest({ + pathname: path, + }, undefined, cancel); + + expect(result.body?.toString()).toBe(CONTENT); + }); + + }); + + }); + + describe('https', () => { + + const PORT = 9001; + + const doRequest = getDoRequest({ + protocol: 'https:', + hostname: '127.0.0.1', + port: PORT, + pathname: '/', + }); + + let serverKey; + let serverCert; + try { + serverKey = fs_.readFileSync(path_.join(__dirname, 'server.key')); + serverCert = fs_.readFileSync(path_.join(__dirname, 'server.crt')); + + } catch (e) { + throw Error( + 'Generate https keys:\n' + + ' cd tests\n' + + ' ./gen-certs.sh\n', + ); + } + + const fake = new Server({ + module: https_, + listen_options: { + port: PORT, + }, + options: { + key: serverKey, + cert: serverCert, + }, + }); + + beforeAll(() => fake.start()); + afterAll(() => fake.stop()); + + it('GET', async() => { + const path = getPath(); + + const CONTENT = 'Привет!'; + + fake.add(path, { + statusCode: 200, + content: CONTENT, + }); + + const result = await doRequest({ + rejectUnauthorized: false, + pathname: path, + }); + + expect(Buffer.isBuffer(result.body)).toBe(true); + expect(result.body?.toString()).toBe(CONTENT); + }); + + }); + + describe('default options', () => { + + describe('isError', () => { + + const isError = DEFAULT_OPTIONS.isError!; + + it.each([ de.ERROR_ID.TCP_CONNECTION_TIMEOUT, de.ERROR_ID.REQUEST_TIMEOUT ])('errorId=%j', (errorId) => { + const error = de.error({ + id: errorId, + }); + + expect(isError(error)).toBe(true); + }); + + it.each([ 200, 301, 302, 303, 304 ])('statusCode=%j', (statusCode) => { + const error = de.error({ + statusCode: statusCode, + }); + + expect(isError(error)).toBe(false); + }); + + it.each([ 400, 401, 402, 403, 404, 500, 501, 503 ])('statusCode=%j', (statusCode) => { + const error = de.error({ + statusCode: statusCode, + }); + + expect(isError(error)).toBe(true); + }); + + }); + + }); + + describe('aborted request', () => { + + describe('no bytes sent', () => { + const server = http.createServer((req: http.IncomingMessage, res: http.OutgoingMessage) => { + setTimeout(() => { + res.socket?.destroy(); + }, 100); + }); + const PORT = 9002; + + beforeAll(() => serverListen(server, PORT)); + afterAll(() => serverClose(server)); + + it('1', async() => { + const doRequest = getDoRequest({ + protocol: 'http:', + hostname: '127.0.0.1', + port: PORT, + pathname: '/', + }); + + const path = getPath(); + + expect.assertions(2); + try { + await doRequest({ + pathname: path, + }); + + } catch (error) { + expect(de.isError(error)).toBe(true); + expect(error.error.id).toBe(de.ERROR_ID.HTTP_UNKNOWN_ERROR); + } + }); + }); + + describe('some bytes sent', () => { + const server = http.createServer((req: http.IncomingMessage, res: http.OutgoingMessage) => { + res.write('Hello!'); + setTimeout(() => { + res.socket?.destroy(); + }, 100); + }); + const PORT = 9003; + + const doRequest = getDoRequest({ + protocol: 'http:', + hostname: '127.0.0.1', + port: PORT, + }); + + beforeAll(() => serverListen(server, PORT)); + afterAll(() => serverClose(server)); + + it('1', async() => { + expect.assertions(2); + try { + await doRequest(); + + } catch (error) { + expect(de.isError(error)).toBe(true); + expect(error.error.id).toBe(de.ERROR_ID.INCOMPLETE_RESPONSE); + } + }); + + it('cancelled', async() => { + const cancel = new de.Cancel(); + const error = de.error({ + id: 'SOME_ERROR', + }); + setTimeout(() => { + cancel.cancel(error); + }, 50); + + expect.assertions(3); + try { + await doRequest({}, undefined, cancel); + + } catch (e) { + expect(de.isError(e)).toBe(true); + expect(e.error.id).toBe(de.ERROR_ID.HTTP_REQUEST_ABORTED); + expect(e.error.reason).toBe(error); + } + }); + + }); + + describe('tcp connection timeout', () => { + const PORT = 9004; + const server = http.createServer((req, res) => { + setTimeout(() => res.end(), 100); + }); + + const doRequest = getDoRequest({ + protocol: 'http:', + hostname: '127.0.0.1', + port: PORT, + }); + + beforeAll(() => serverListen(server, PORT)); + afterAll(() => serverClose(server)); + + it('1', async() => { + expect.assertions(2); + try { + const agent = { + keepAlive: false, + maxSockets: 1, + }; + + // Делаем запрос и занимаем весь один сокет. + doRequest({ + agent: agent, + }); + // Так что этот запрос не сможет законнектиться. + await doRequest({ + agent: agent, + // Тут должно быть что-то меньшее, чем время, за которое отвечает сервер. + timeout: 50, + }); + + } catch (error) { + expect(de.isError(error)).toBe(true); + expect(error.error.id).toBe(de.ERROR_ID.TCP_CONNECTION_TIMEOUT); + } + }); + + }); + + }); + +}); + +function serverListen(server: http.Server, port: number) { + return new Promise((resolve) => { + server.listen(port, resolve as () => void); + }); +} + +function serverClose(server: http.Server) { + return new Promise((resolve) => { + server.close(resolve); + }); +} diff --git a/tests/server.js b/tests/server.js index fd34804..71c2ccd 100644 --- a/tests/server.js +++ b/tests/server.js @@ -1,28 +1,28 @@ -const url_ = require( 'url' ); +import * as url_ from 'url' ; // --------------------------------------------------------------------------------------------------------------- // class Answer { - constructor( answer = {} ) { - if ( typeof answer === 'function' ) { + constructor(answer = {}) { + if (typeof answer === 'function') { this.answer = answer; } else { this.answer = {}; - this.answer.status_code = answer.status_code || 200; + this.answer.statusCode = answer.statusCode || 200; this.answer.headers = answer.headers || {}; this.answer.content = answer.content || null; this.answer.timeout = answer.timeout || 0; - if ( Array.isArray( answer.stops ) ) { + if (Array.isArray(answer.stops)) { this.answer.stops = answer.stops; - } else if ( ( answer.chunks > 0 ) && ( answer.interval > 0 ) ) { + } else if ((answer.chunks > 0) && (answer.interval > 0)) { this.answer.stops = []; - for ( let i = 0; i < answer.chunks; i++ ) { + for (let i = 0; i < answer.chunks; i++) { this.answer.stops[ i ] = i * answer.interval; } @@ -32,44 +32,44 @@ class Answer { } } - async response( req, res, data ) { + async response(req, res, data) { const answer = this.answer; - if ( typeof answer === 'function' ) { - answer( req, res, data ); + if (typeof answer === 'function') { + answer(req, res, data); return; } - if ( answer.wait > 0 ) { - await wait_for( answer.wait ); + if (answer.wait > 0) { + await waitFor(answer.wait); } - let content = ( typeof answer.content === 'function' ) ? answer.content( req, res, data ) : answer.content; + let content = (typeof answer.content === 'function') ? answer.content(req, res, data) : answer.content; content = await content; // eslint-disable-next-line require-atomic-updates - res.statusCode = answer.status_code; - for ( const header_name in answer.headers ) { - res.setHeader( header_name, answer.headers[ header_name ] ); + res.statusCode = answer.statusCode; + for (const headerName in answer.headers) { + res.setHeader(headerName, answer.headers[ headerName ]); } - if ( typeof content === 'object' ) { - content = JSON.stringify( content ); - set_content_type( 'application/json' ); + if (typeof content === 'object') { + content = JSON.stringify(content); + setContentType('application/json'); } else { - content = String( content ); - set_content_type( 'text/plain' ); + content = String(content); + setContentType('text/plain'); } - res.setHeader( 'content-length', Buffer.byteLength( content ) ); + res.setHeader('content-length', Buffer.byteLength(content)); - res.end( content ); + res.end(content); - function set_content_type( content_type ) { - if ( !res.getHeader( 'content-type' ) ) { - res.setHeader( 'content-type', content_type ); + function setContentType(contentType) { + if (!res.getHeader('content-type')) { + res.setHeader('content-type', contentType); } } } @@ -80,18 +80,18 @@ class Answer { class Route { - constructor( answers ) { - answers = to_array( answers ); - this.answers = answers.map( ( answer ) => new Answer( answer ) ); + constructor(answers) { + answers = toArray(answers); + this.answers = answers.map((answer) => new Answer(answer)); - this.current_answer = 0; + this.currentAnswer = 0; } - response( req, res, data ) { - const answer = this.answers[ this.current_answer ]; - answer.response( req, res, data ); + response(req, res, data) { + const answer = this.answers[ this.currentAnswer ]; + answer.response(req, res, data); - this.current_answer = ( this.current_answer + 1 ) % this.answers.length; + this.currentAnswer = (this.currentAnswer + 1) % this.answers.length; } } @@ -100,72 +100,71 @@ class Route { class Server { - constructor( config ) { + constructor(config) { this.config = config; this.routes = {}; - const response404 = new Answer( { - status_code: 404, - } ); + const response404 = new Answer({ + statusCode: 404, + }); - const handler = ( req, res ) => { - const path = url_.parse( req.url ).pathname; + const handler = (req, res) => { + const path = url_.parse(req.url).pathname; const buffers = []; - let received_length = 0; + let receivedLength = 0; - req.on( 'data', ( data ) => { - buffers.push( data ); - received_length += data.length; - } ); + req.on('data', (data) => { + buffers.push(data); + receivedLength += data.length; + }); - req.on( 'end', () => { - const data = ( received_length ) ? Buffer.concat( buffers, received_length ) : null; + req.on('end', () => { + const data = (receivedLength) ? Buffer.concat(buffers, receivedLength) : null; const route = this.routes[ path ]; - if ( route ) { - route.response( req, res, data ); + if (route) { + route.response(req, res, data); } else { - response404.response( req, res, data ); + response404.response(req, res, data); } - } ); + }); }; - this.server = this.config.module.createServer( this.config.options, handler ); + this.server = this.config.module.createServer(this.config.options, handler); } - add( path, route ) { - this.routes[ path ] = new Route( route ); + add(path, route) { + this.routes[ path ] = new Route(route); } start() { - return new Promise( ( resolve ) => { - this.server.listen( this.config.listen_options, resolve ); - } ); + return new Promise((resolve) => { + this.server.listen(this.config.listen_options, resolve); + }); } stop() { - return new Promise( ( resolve ) => { - this.server.close( resolve ); - } ); + return new Promise((resolve) => { + this.server.close(resolve); + }); } } // --------------------------------------------------------------------------------------------------------------- // -module.exports = Server; +export default Server; // --------------------------------------------------------------------------------------------------------------- // -function to_array( value ) { - return ( Array.isArray( value ) ) ? value : [ value ]; +function toArray(value) { + return (Array.isArray(value)) ? value : [ value ]; } -function wait_for( interval ) { - return new Promise( ( resolve ) => { - setTimeout( resolve, interval ); - } ); +function waitFor(interval) { + return new Promise((resolve) => { + setTimeout(resolve, interval); + }); } - diff --git a/tests/stripNullAndUndefinedValues.test.ts b/tests/stripNullAndUndefinedValues.test.ts new file mode 100644 index 0000000..173fea7 --- /dev/null +++ b/tests/stripNullAndUndefinedValues.test.ts @@ -0,0 +1,33 @@ +import stripNullAndUndefinedValues from '../lib/stripNullAndUndefinedValues'; + +describe('stripNullAndUndefinedValues', () => { + + it('returns copy', () => { + const obj = { + a: 'a', + b: 'b', + }; + const stripped = stripNullAndUndefinedValues(obj); + + expect(stripped).toStrictEqual(obj); + expect(stripped).not.toBe(obj); + }); + + it('strip null and undefined', () => { + const obj = { + a: undefined, + b: null, + c: 0, + d: '', + e: false, + }; + const stripped = stripNullAndUndefinedValues(obj); + + expect(stripped).toStrictEqual({ + c: 0, + d: '', + e: false, + }); + }); + +}); diff --git a/tests/strip_null_and_undefined_values.test.js b/tests/strip_null_and_undefined_values.test.js deleted file mode 100644 index 437ec0e..0000000 --- a/tests/strip_null_and_undefined_values.test.js +++ /dev/null @@ -1,34 +0,0 @@ -const strip_null_and_undefined_values = require( '../lib/strip_null_and_undefined_values' ); - -describe( 'strip_null_and_undefined_values', () => { - - it( 'returns copy', () => { - const obj = { - a: 'a', - b: 'b', - }; - const stripped = strip_null_and_undefined_values( obj ); - - expect( stripped ).toStrictEqual( obj ); - expect( stripped ).not.toBe( obj ); - } ); - - it( 'strip null and undefined', () => { - const obj = { - a: undefined, - b: null, - c: 0, - d: '', - e: false, - }; - const stripped = strip_null_and_undefined_values( obj ); - - expect( stripped ).toStrictEqual( { - c: 0, - d: '', - e: false, - } ); - } ); - -} ); - diff --git a/tsconfig-build.json b/tsconfig-build.json new file mode 100644 index 0000000..000cc6f --- /dev/null +++ b/tsconfig-build.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "include": [ + "./lib/index.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/tsconfig.json b/tsconfig.json index 47fb6e4..24e80c0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,72 +1,35 @@ { "compilerOptions": { - /* Visit https://aka.ms/tsconfig.json to read more about this file */ - - /* Basic Options */ - // "incremental": true, /* Enable incremental compilation */ - "target": "ES2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */ - "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ - // "lib": [], /* Specify library files to be included in the compilation. */ - "allowJs": true, /* Allow javascript files to be compiled. */ - // "checkJs": true, /* Report errors in .js files. */ - // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ - // "declaration": true, /* Generates corresponding '.d.ts' file. */ - // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ - // "sourceMap": true, /* Generates corresponding '.map' file. */ - // "outFile": "./", /* Concatenate and emit output to single file. */ - // "outDir": "./", /* Redirect output structure to the directory. */ - // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ - // "composite": true, /* Enable project compilation */ - // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ - // "removeComments": true, /* Do not emit comments to output. */ - // "noEmit": true, /* Do not emit outputs. */ - // "importHelpers": true, /* Import emit helpers from 'tslib'. */ - // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ - // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ - - /* Strict Type-Checking Options */ - "strict": true, /* Enable all strict type-checking options. */ - "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ - "strictNullChecks": true, /* Enable strict null checks. */ "strictFunctionTypes": true, /* Enable strict checking of function types. */ - // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ - // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ - // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ + "allowJs": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "jsx": "react", + "module": "commonjs", + "target": "es2020", + "moduleResolution": "node", + "noEmitOnError": true, + "noErrorTruncation": true, + "noImplicitAny": true, + "noImplicitThis": true, + "preserveConstEnums": true, + "resolveJsonModule": true, + "skipLibCheck": true, // https://www.typescriptlang.org/tsconfig#skipLibCheck + "strict": false, + "strictNullChecks": true, + "declaration":true, - /* Additional Checks */ - // "noUnusedLocals": true, /* Report errors on unused locals. */ - // "noUnusedParameters": true, /* Report errors on unused parameters. */ - // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ - // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ - // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ - // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */ - // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ - - /* Module Resolution Options */ - // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ - // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ - // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ - // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ - // "typeRoots": [], /* List of folders to include type definitions from. */ - // "types": [], /* Type declaration files to be included in compilation. */ - // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ - "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ - // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ - // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - - /* Source Map Options */ - // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ - // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ - - /* Experimental Options */ - // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ - // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ - - /* Advanced Options */ - "skipLibCheck": false, /* Skip type checking of declaration files. */ - "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ - } + "baseUrl": ".", + "outDir": "./build" + }, + "include": [ + "**/*.ts", + "**/*.d.ts", + "global.d.ts" + ], + "exclude": [ + "node_modules" + ] } From 040a5f22258333d54c5b35edd4a72336548b99be Mon Sep 17 00:00:00 2001 From: Egor Dydykin Date: Thu, 7 Nov 2024 22:37:09 +0300 Subject: [PATCH 2/3] v4: typescript first --- .gitignore | 1 + docs/typescript-examples/func.ts | 19 +- docs/typescript-examples/http.ts | 30 + docs/typescript-examples/options.ts | 129 +++++ lib/arrayBlock.ts | 27 +- lib/block.ts | 44 +- lib/compositeBlock.ts | 2 +- lib/depsDomain.ts | 4 +- lib/error.ts | 3 + lib/firstBlock.ts | 28 +- lib/functionBlock.ts | 40 +- lib/httpBlock.ts | 40 +- lib/index.d2.jtxt | 825 ---------------------------- lib/index.ts | 31 +- lib/objectBlock.ts | 30 +- lib/pipeBlock.ts | 32 +- lib/types.ts | 27 +- package.json | 2 +- 18 files changed, 317 insertions(+), 997 deletions(-) delete mode 100644 lib/index.d2.jtxt diff --git a/.gitignore b/.gitignore index ffd68ff..f55d651 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .idea +.vscode node_modules *.swp tests/server.key diff --git a/docs/typescript-examples/func.ts b/docs/typescript-examples/func.ts index 4b8eb74..8141337 100644 --- a/docs/typescript-examples/func.ts +++ b/docs/typescript-examples/func.ts @@ -66,11 +66,17 @@ de.run(block2, { const block3 = block2.extend({ options: { - after: ({ result }) => { + params: ({ params }: { params: { param2: number } }) => { + return { + param: params.param2, + }; + }, + after: ({ result, params }) => { console.log(result); return { res: result, + x: params.param, }; }, }, @@ -79,7 +85,7 @@ const block3 = block2.extend({ de.run(block3, { params: { - param: 2, + param2: 2, }, }) .then((result) => { @@ -87,13 +93,20 @@ de.run(block3, { }); +const objBlock = de.object({ + block: { + foo: block1, + boo: block2, + }, +}); + const block4 = de.func({ // eslint-disable-next-line @typescript-eslint/no-unused-vars block: ({ params }) => { //TODO не выводится params. // eslint-disable-next-line @typescript-eslint/no-unused-vars //const x = params.id; - return block1; + return objBlock; }, options: { after: ({ result, params }) => { diff --git a/docs/typescript-examples/http.ts b/docs/typescript-examples/http.ts index 4835b59..b7638c7 100644 --- a/docs/typescript-examples/http.ts +++ b/docs/typescript-examples/http.ts @@ -127,3 +127,33 @@ de.run(block2, { .then((result) => { console.log(result.c); }); + +const block3 = de.http({ + block: {}, + options: {}, +}); + +const block4 = block3.extend({ + block: { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + headers: ({ params }) => { + return { + 'x-header': String(params.param2), + }; + }, + }, + options: { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + params: ({ params }: { params: { param: number } }) => { + return { + param2: params.param, + }; + }, + }, +}); + +de.run(block4, { + params: { + param: 1, + }, +}); diff --git a/docs/typescript-examples/options.ts b/docs/typescript-examples/options.ts index e8fc855..9e2dc48 100644 --- a/docs/typescript-examples/options.ts +++ b/docs/typescript-examples/options.ts @@ -117,6 +117,8 @@ de.run(block2, { const block3 = de.http({ block: {}, options: { + //TODO как вывести In из Out? + //params: ({ params }: { params: ParamsIn }) => params, before: ({ params }: { params: ParamsIn }) => { if (!params.id) { return 'foo'; @@ -143,3 +145,130 @@ de.run(block3, { return result; }); + +const block4 = de.http({ + block: { + agent: { maxSockets: 16 }, + maxRetries: 1, + timeout: 1000, + body: ({ params }: { params: { p4: number } }) => params, + }, + options: { + after: ({ result }: { result: de.DescriptHttpBlockResult<{ r: number }> }) => result.result, + }, +}); + +de.run(block4, { + params: { + p4: 1, + }, +}); + +const block5 = block4.extend({ + block: { + body: ({ params }) => params, + }, + + options: { + after: ({ params }) => params, + }, +}); + +const block6 = de.object({ + block: { + block4: block5.extend({ + options: { + params: ({ params }) => { + return { + p4: params.p4, + }; + }, + //after: ({ result }) => result, + }, + }), + block3: block3.extend({}), + }, + + options: { + after: ({ result }) => { + return result.block4; + }, + }, +}); + +de.run(block6, { + params: { + p4: 8, + id: '1', + }, +}); + +const block7 = de.object({ + block: { + block6: block6.extend({ + options: { + params: ({ params }) => { + return { + p4: params.p4, + id: '1', + x: 1, + }; + }, + //after: ({ result }) => result, + }, + }), + block3: block3.extend({}), + }, + options: { + after: ({ result }) => { + return result.block6; + }, + }, +}); + + +de.run(block7, { + params: { + p4: 8, + id: '1', + }, +}); + +const block8 = de.http({ + block: { + agent: { maxSockets: 16 }, + maxRetries: 1, + timeout: 1000, + body: ({ params }: { params: { p1: number } }) => params, + }, + options: { + after: ({ result }: { result: de.DescriptHttpBlockResult<{ r: number }> }) => result.result, + }, +}); + +const block9 = block8.extend({ + block: { + body: ({ params }: { params: { p4: number; p1: number } }) => params, + }, + + options: { + after: ({ params }) => params, + }, +}); + + +de.run(block9, { + params: { + p1: 8, + }, +}); + +const block10 = de.func({ + block: () => { + return block8; + }, +}); + +de.run(block10, { + params: {}, +}); diff --git a/lib/arrayBlock.ts b/lib/arrayBlock.ts index 8ef8103..9b0b569 100644 --- a/lib/arrayBlock.ts +++ b/lib/arrayBlock.ts @@ -75,7 +75,7 @@ class ArrayBlock< // eslint-disable-next-line @typescript-eslint/no-unused-vars ExtendedResultOut extends BlockResultOut, - ExtendedParamsOut = Params, + ExtendedParamsOut extends Params = Params, ExtendedParams = Params, ExtendedBlockResult = ResultOut, ExtendedBeforeResultOut = undefined, @@ -83,28 +83,13 @@ class ArrayBlock< ExtendedErrorResultOut = undefined, >({ options }: { options: DescriptBlockOptions< - Context, Params & ExtendedParamsOut, ExtendedBlockResult, ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams + Context, ExtendedParamsOut, ExtendedBlockResult, ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams >; }) { - return this.extendClass< - ArrayBlock< - Context, - Block, - ExtendedResultOut, - Params & ExtendedParamsOut, - ExtendedBlockResult, - ExtendedBeforeResultOut, - ExtendedAfterResultOut, - ExtendedErrorResultOut, - ExtendedParams - >, - ExtendedBlockResult, - ExtendedParamsOut, - ExtendedParams, - ExtendedBeforeResultOut, - ExtendedAfterResultOut, - ExtendedErrorResultOut - >({ options }); + return new ArrayBlock({ + block: this.extendBlock(this.block), + options: this.extendOptions(this.options, options) as typeof options, + }); } protected initBlock(array: ArrayBlockDefinition) { diff --git a/lib/block.ts b/lib/block.ts index bf30d79..e3e8bdd 100644 --- a/lib/block.ts +++ b/lib/block.ts @@ -58,7 +58,7 @@ abstract class BaseBlock< BeforeResultOut = undefined, AfterResultOut = undefined, ErrorResultOut = undefined, - Params = ParamsOut, + Params = ParamsOut > { protected block: CustomBlock; protected options: BlockOptions; @@ -84,49 +84,51 @@ abstract class BaseBlock< protected extendClass< ClassType, ExtendedBlockResult, - ExtendedParamsOut = Params, + ExtendedParamsOut extends Params = Params, ExtendedParams = Params, - ExtendedBeforeResultOut = undefined, - ExtendedAfterResultOut = undefined, - ExtendedErrorResultOut = undefined, + ExtendedBeforeResultOut = void, + ExtendedAfterResultOut = void, + ExtendedErrorResultOut = void, + ExtendedCustomBlock extends CustomBlock = CustomBlock, >({ block, options }: { - block?: CustomBlock; + block?: ExtendedCustomBlock; options?: DescriptBlockOptions< - Context, Params & ExtendedParamsOut, ExtendedBlockResult, ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams + Context, ExtendedParamsOut, ExtendedBlockResult, ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams >; }): ClassType { return new (> this.constructor)({ - block: this.extendBlock(block), + block: this.extendBlock(block) as ExtendedCustomBlock, options: this.extendOptions(this.options, options), }); } abstract extend< // eslint-disable-next-line @typescript-eslint/no-unused-vars - ExtendedResultOut extends - BlockResultOut, - ExtendedParamsOut = Params, + //ExtendedResultOut extends + //BlockResultOut, + ExtendedParamsOut extends Params = Params, ExtendedParams = Params, + //ExtendedCustomBlock = CustomBlock, ExtendedBlockResult = ResultOut, - ExtendedBeforeResultOut = undefined, - ExtendedAfterResultOut = undefined, - ExtendedErrorResultOut = undefined, + ExtendedBeforeResultOut = void, + ExtendedAfterResultOut = void, + ExtendedErrorResultOut = void, >({ block, options }: { block?: CustomBlock; options?: DescriptBlockOptions< - Context, Params & ExtendedParamsOut, ExtendedBlockResult, ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams + Context, ParamsOut & ExtendedParamsOut, ExtendedBlockResult, ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams >; }): unknown @@ -144,7 +146,7 @@ abstract class BaseBlock< } //eslint-disable-next-line @typescript-eslint/no-unused-vars - protected extendBlock(block?: CustomBlock) { + protected extendBlock(block?: ExtendedCustomBlock) { return this.block; } @@ -201,9 +203,9 @@ abstract class BaseBlock< private extendLifecycle< ExtendedParamsOut, ExtendedBlockResult, - ExtendedBeforeResultOut = undefined, - ExtendedAfterResultOut = undefined, - ExtendedErrorResultOut = undefined, + ExtendedBeforeResultOut = void, + ExtendedAfterResultOut = void, + ExtendedErrorResultOut = void, ExtendedParams = ExtendedParamsOut, // eslint-disable-next-line max-len W extends BlockOptions = diff --git a/lib/compositeBlock.ts b/lib/compositeBlock.ts index b0c84f4..94e9079 100644 --- a/lib/compositeBlock.ts +++ b/lib/compositeBlock.ts @@ -27,7 +27,7 @@ abstract class CompositeBlock< BeforeResultOut = undefined, AfterResultOut = undefined, ErrorResultOut = undefined, - Params = ParamsOut, + Params = ParamsOut > extends BaseBlock< Context, CustomBlock, diff --git a/lib/depsDomain.ts b/lib/depsDomain.ts index bedc2c9..777a5ad 100644 --- a/lib/depsDomain.ts +++ b/lib/depsDomain.ts @@ -1,6 +1,8 @@ //TODO как это типизировать any этот? export type DescriptBlockDeps = Record; export type DescriptBlockId = symbol; +export type GenerateId = (label?: string) => DescriptBlockId; + class DepsDomain { ids: Record; constructor(parent: any) { @@ -8,7 +10,7 @@ class DepsDomain { } - generateId = (label?: string): DescriptBlockId => { + generateId: GenerateId = (label?: string): DescriptBlockId => { const id = Symbol(label); this.ids[ id ] = true; return id; diff --git a/lib/error.ts b/lib/error.ts index 0a0e3e5..42f705d 100644 --- a/lib/error.ts +++ b/lib/error.ts @@ -43,6 +43,8 @@ export interface ErrorData { headers?: OutgoingHttpHeaders; statusCode?: number; + location?: string; + errno?: number | undefined; code?: string | undefined; path?: string | undefined; @@ -105,6 +107,7 @@ export class DescriptError { stack: error.stack, errno: error.errno, syscall: error.syscall, + location: error.location, }; this.error = _error; diff --git a/lib/firstBlock.ts b/lib/firstBlock.ts index ad472bd..0361a3b 100644 --- a/lib/firstBlock.ts +++ b/lib/firstBlock.ts @@ -87,7 +87,8 @@ class FirstBlock< // eslint-disable-next-line @typescript-eslint/no-unused-vars ExtendedResultOut extends BlockResultOut, - ExtendedParamsOut = Params, + //ExtendedCustomBlock extends FirstBlockDefinition, + ExtendedParamsOut extends Params = Params, ExtendedParams = Params, ExtendedBlockResult = ResultOut, ExtendedBeforeResultOut = undefined, @@ -95,28 +96,13 @@ class FirstBlock< ExtendedErrorResultOut = undefined, >({ options }: { options: DescriptBlockOptions< - Context, Params & ExtendedParamsOut, ExtendedBlockResult, ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams + Context, ExtendedParamsOut, ExtendedBlockResult, ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams >; }) { - return this.extendClass< - FirstBlock< - Context, - Block, - ExtendedResultOut, - Params & ExtendedParamsOut, - ExtendedBlockResult, - ExtendedBeforeResultOut, - ExtendedAfterResultOut, - ExtendedErrorResultOut, - ExtendedParams - >, - ExtendedBlockResult, - ExtendedParamsOut, - ExtendedParams, - ExtendedBeforeResultOut, - ExtendedAfterResultOut, - ExtendedErrorResultOut - >({ options }); + return new FirstBlock({ + block: this.extendBlock(this.block), + options: this.extendOptions(this.options, options) as typeof options, + }); } protected initBlock(block: FirstBlockDefinition) { diff --git a/lib/functionBlock.ts b/lib/functionBlock.ts index a326916..b6a34a3 100644 --- a/lib/functionBlock.ts +++ b/lib/functionBlock.ts @@ -2,7 +2,7 @@ import BaseBlock from './block'; import type { DescriptBlockDeps } from './depsDomain'; import DepsDomain from './depsDomain'; import { createError, ERROR_ID } from './error'; -import type { BlockResultOut, DescriptBlockOptions } from './types'; +import type { BlockResultOut, DescriptBlockOptions, InferParamsOutFromBlock } from './types'; import type ContextClass from './context'; import type Cancel from './cancel'; @@ -27,7 +27,7 @@ class FunctionBlock< BeforeResultOut = undefined, AfterResultOut = undefined, ErrorResultOut = undefined, - Params = ParamsOut + Params = never extends InferParamsOutFromBlock ? ParamsOut : InferParamsOutFromBlock > extends BaseBlock< Context, FunctionBlockDefinition, @@ -93,40 +93,26 @@ class FunctionBlock< } extend< - // eslint-disable-next-line @typescript-eslint/no-unused-vars - ExtendedResultOut extends - BlockResultOut, - ExtendedParamsOut = Params, + ExtendedParamsOut extends Params = Params, ExtendedParams = Params, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + //ExtendedCustomBlock = DescriptHttpBlockDescription, + ExtendedBlockResult = ResultOut, ExtendedBeforeResultOut = undefined, ExtendedAfterResultOut = undefined, ExtendedErrorResultOut = undefined, >({ options }: { options: DescriptBlockOptions< - Context, Params & ExtendedParamsOut, ExtendedBlockResult, ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams + Context, ExtendedParamsOut, ExtendedBlockResult, ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams >; }) { - return this.extendClass< - FunctionBlock< - Context, - //FNBlock, - //FunctionBlockDefinition, - Params & ExtendedParamsOut, - ExtendedBlockResult, - ExtendedResultOut, - ExtendedBeforeResultOut, - ExtendedAfterResultOut, - ExtendedErrorResultOut, - ExtendedParams - >, - ExtendedBlockResult, - Params & ExtendedParamsOut, - ExtendedParams, - ExtendedBeforeResultOut, - ExtendedAfterResultOut, - ExtendedErrorResultOut - >({ options }); + return new FunctionBlock({ + block: this.extendBlock(this.block) as typeof this.block, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-expect-error + options: this.extendOptions(this.options, options) as typeof options, + }); } } diff --git a/lib/httpBlock.ts b/lib/httpBlock.ts index f394192..310b3d4 100644 --- a/lib/httpBlock.ts +++ b/lib/httpBlock.ts @@ -24,8 +24,8 @@ type DescriptHttpBlockDescriptionCallback< T, Params, Context > = T | ((args: { deps: DescriptBlockDeps; }) => T); -type DescriptHttpBlockQueryValue = string | number | boolean | undefined | null | Array; -type DescriptHttpBlockQuery = Record< string, DescriptHttpBlockQueryValue >; +export type DescriptHttpBlockQueryValue = string | number | boolean | undefined | null | Array; +export type DescriptHttpBlockQuery = Record< string, DescriptHttpBlockQueryValue >; type HttpQuery = Record< string, @@ -46,7 +46,7 @@ DescriptHttpBlockQueryValue | }) => DescriptHttpBlockQuery ); -type HttpHeaders = Record< string, +export type HttpHeaders = Record< string, string | ((args: { params: Params; @@ -179,41 +179,41 @@ class HttpBlock< Params > { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + extend< - ExtendedResultOut extends - BlockResultOut, - ExtendedParamsOut = Params, + ExtendedResultOut extends BlockResultOut, + ExtendedParamsOut extends Params = Params, ExtendedParams = Params, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + //ExtendedCustomBlock = DescriptHttpBlockDescription, ExtendedBlockResult = ResultOut, ExtendedBeforeResultOut = undefined, ExtendedAfterResultOut = undefined, ExtendedErrorResultOut = undefined, >({ options, block }: { - block?: DescriptHttpBlockDescription; + block?: DescriptHttpBlockDescription; options?: DescriptBlockOptions< - Context, Params & ExtendedParamsOut, ExtendedBlockResult, ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams + Context, ExtendedParamsOut, ExtendedBlockResult, ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams >; }) { - return this.extendClass< - HttpBlock< + const x = new HttpBlock< Context, ExtendedParamsOut, HttpResult, - Params & ExtendedResultOut, + ExtendedResultOut, ExtendedBlockResult, ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams - >, - ExtendedBlockResult, - Params & ExtendedParamsOut, - ExtendedParams, - ExtendedBeforeResultOut, - ExtendedAfterResultOut, - ExtendedErrorResultOut - >({ options, block }); + >({ + block: this.extendBlock(block), + options: this.extendOptions(this.options, options), + }); + + return x; } protected logger: Logger; @@ -237,7 +237,7 @@ class HttpBlock< // this._compiled_props = compile_props( this.block ); } - protected extendBlock(by: DescriptHttpBlockDescription = {}) { + protected extendBlock(by: DescriptHttpBlockDescription = {}) { const what = this.block; const headers = extendOption(what.headers, by.headers); const query = extendOption(what.query, by.query); diff --git a/lib/index.d2.jtxt b/lib/index.d2.jtxt deleted file mode 100644 index fdacaa9..0000000 --- a/lib/index.d2.jtxt +++ /dev/null @@ -1,825 +0,0 @@ -import type { ZlibOptions } from 'node:zlib'; - -type First< T > = - T extends readonly [ infer First, ...infer Rest ] | [ infer First, ...infer Rest ] ? First : never; - -type Tail< T > = - T extends readonly [ infer First, ...infer Rest ] | [ infer First, ...infer Rest ] ? Rest : never; - -type Equal< A, B > = A extends B ? (B extends A ? A : never) : never; - - -type InferContext = Type extends DescriptBlock< infer Context, infer ParamsIn, infer ResultIn, infer ParamsOut, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer ResultOut > ? Context : never; -type InferParamsIn = Type extends DescriptBlock< infer Context, infer ParamsIn, infer ResultIn, infer ParamsOut, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer ResultOut > ? ParamsIn : never; -type InferParamsOut = Type extends DescriptBlock< infer Context, infer ParamsIn, infer ResultIn, infer ParamsOut, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer ResultOut > ? ParamsOut : never; -type InferResultIn = Type extends DescriptBlock< infer Context, infer ParamsIn, infer ResultIn, infer ParamsOut, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer ResultOut > ? ResultIn : never; -type InferResultOut = Type extends DescriptBlock< infer Context, infer ParamsIn, infer ResultIn, infer ParamsOut, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer ResultOut > ? ResultOut : never; - - -type DescriptBlockParams = { - b: BlockParams; - i: ParamsFnIn; - o: ParamsFnOut; -}; -type GetDescriptBlockParamsBlock = T extends DescriptBlockParams ? BlockParams : T; -type GetDescriptBlockParamsFnIn = T extends DescriptBlockParams ? ParamsFnIn : T; -type GetDescriptBlockParamsFnOut = T extends DescriptBlockParams ? ParamsFnOut : T; - -type DescriptBlockResult = { - i: ResultIn; - o: ResultOut; -}; -type GetDescriptBlockResultIn = T extends DescriptBlockResult ? ResultIn : T; -type GetDescriptBlockResultOut = T extends DescriptBlockResult ? ResultOut : T; - -type InferResultInFromBlockOrReturnResultIn = Type extends DescriptBlock< infer Context, infer ParamsIn, infer Result, infer ParamsOut, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer ResultOut > ? Result : Type; -type InferResultOutFromBlockOrReturnResultOut = Type extends DescriptBlock< infer Context, infer ParamsIn, infer Result, infer ParamsOut, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer ResultOut > ? ResultOut : Type; -type InferParamsFromBlockOrReturnParams = Block extends DescriptBlock< infer Context, infer ParamsIn, infer Result, infer ParamsOut, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer ResultOut > ? ParamsIn : Params; - -type InferResultInFromBlocks = Block extends DescriptBlock< infer Context, infer ParamsIn, infer Result, infer ParamsOut, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer ResultOut > ? - { [ K in keyof Result ]: InferResultInFromBlocks } : - Block; - -type NotUnknown = NonNullable; - -type InferResultOutFromBlocks = Block extends DescriptBlock< infer Context, infer ParamsIn, infer Result, infer ParamsOut, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer ResultOut > ? - { [ K in keyof ResultOut ]: InferResultOutFromBlocks } : - Block; - -// --------------------------------------------------------------------------------------------------------------- // - -import type { OutgoingHttpHeaders } from 'http'; -import type { - RequestOptions as HttpsRequestOptions, - Agent as HttpsAgent, - AgentOptions as HttpsAgentOptions, -} from 'https'; - -// --------------------------------------------------------------------------------------------------------------- // - -type DescriptJSON = - boolean | - number | - string | - undefined | - null | - { [ property: string ]: DescriptJSON } | - object | - Array< DescriptJSON >; - -// --------------------------------------------------------------------------------------------------------------- // - -interface DescriptError { - error: { - id: string; - message?: string; - // для http-ошибок - body?: Error; - headers?: OutgoingHttpHeaders; - status_code?: number; - }; -} - -// --------------------------------------------------------------------------------------------------------------- // - -declare class Cancel { - cancel(reason: DescriptError): void; -} - -// --------------------------------------------------------------------------------------------------------------- // - -declare class Cache< Result, Context > { - get(args: { key: string; context: Context }): Result | Promise< Result >; - set(args: { key: string; value: Result; maxage: number; context: Context }): void; -} - -// --------------------------------------------------------------------------------------------------------------- // - -interface DescriptRequestOptions { - http_options: HttpsRequestOptions; - body: string | Buffer | null; - url: string; - - retries: number; - - extra?: { - name: string; - }; -} - -// --------------------------------------------------------------------------------------------------------------- // - -interface DescriptBlockResultJSON { - result: Result; -} - -interface DescriptHttpResult { - status_code: number; - headers: Record< string, string >; - request_options: HttpsRequestOptions; - body: Buffer; -} - -interface LoggerEvent { - type: EVENT; - - request_options: DescriptRequestOptions; - - timestamps: { - start: number; - socket: number; - tcp_connection: number; - body: number; - request_end: number; - end: number; - }; - - result?: DescriptHttpResult; - - error?: DescriptError; -} - -interface DescriptLogger< Context > { - log(event: LoggerEvent, context: Context): void; -} - -declare namespace Logger { - export enum EVENT { - REQUEST_START = 'REQUEST_START', - REQUEST_SUCCESS = 'REQUEST_SUCCESS', - REQUEST_ERROR = 'REQUEST_ERROR', - } -} - -// --------------------------------------------------------------------------------------------------------------- // - -type DescriptBlockId = symbol; -type DescriptBlockGenerateId = () => DescriptBlockId; - -type DescriptBlockDeps = Record< DescriptBlockId, any >; - -type ComplexResult = Result | GetDescriptBlockResultOut | Promise< Result | GetDescriptBlockResultOut > | void | null; - -interface DescriptBlockOptions< - Context, - Params, - ResultIn, - ParamsOut = Params, - BeforeResultOut = ResultIn, - ErrorResultOut = ResultIn, - AfterResultIn = ResultIn, - AfterResultOut = AfterResultIn, - ResultOut = Exclude, -> { - name?: string; - - id?: DescriptBlockId; - deps?: DescriptBlockId | Array< DescriptBlockId >; - - params?: (args: { - params: Params; - context: Context; - deps: DescriptBlockDeps; - }) => ParamsOut; - - before?: (args: { - params: ParamsOut; - context: Context; - deps: DescriptBlockDeps; - cancel: Cancel; - }) => ComplexResult; - - after?: (args: { - params: ParamsOut; - context: Context; - deps: DescriptBlockDeps; - cancel: Cancel; - result: AfterResultIn; - }) => ComplexResult; - - error?: (args: { - params: ParamsOut; - context: Context; - deps: DescriptBlockDeps; - cancel: Cancel; - error: DescriptError; - }) => ComplexResult; - - timeout?: number; - - key?: string | ((args: { - params: ParamsOut; - context: Context; - deps: DescriptBlockDeps; - }) => string); - maxage?: number; - cache?: Cache< ResultOut | GetDescriptBlockResultOut, Context >; - - required?: boolean; - - logger?: DescriptLogger< Context >; -} - -// --------------------------------------------------------------------------------------------------------------- // -// HttpBlock - -type DescriptHttpBlockDescriptionCallback< T, Params, Context > = T | ((args: { - params: Params; - context: Context; - deps: DescriptBlockDeps; -}) => T); - -type DescriptHttpBlockQueryValue = string | number | boolean | undefined | Array; -type DescriptHttpBlockQuery = Record< string, DescriptHttpBlockQueryValue >; - -type DescriptHttpBlockHeaders = Record< string, string >; - -interface DescriptHttpBlockDescription< Params, Context > { - protocol?: DescriptHttpBlockDescriptionCallback< string, Params, Context >; - hostname?: DescriptHttpBlockDescriptionCallback< string, Params, Context >; - port?: DescriptHttpBlockDescriptionCallback< number, Params, Context >; - method?: DescriptHttpBlockDescriptionCallback< string, Params, Context >; - pathname?: DescriptHttpBlockDescriptionCallback< string, Params, Context >; - - query?: - Record< string, - DescriptHttpBlockQueryValue | - null | - ((args: { - params: Params; - context: Context; - deps: DescriptBlockDeps; - query: DescriptHttpBlockQuery; - }) => DescriptHttpBlockQueryValue) - > | - ( - (args: { - params: Params; - context: Context; - deps: DescriptBlockDeps; - query: DescriptHttpBlockQuery; - }) => DescriptHttpBlockQuery - ); - - headers?: - Record< string, - string | - ((args: { - params: Params; - context: Context; - deps: DescriptBlockDeps; - headers: DescriptHttpBlockHeaders; - }) => string) - > | - ( - (args: { - params: Params; - context: Context; - deps: DescriptBlockDeps; - headers: DescriptHttpBlockHeaders; - }) => DescriptHttpBlockHeaders - ); - - body?: - string | - Buffer | - ((args: { - params: Params; - context: Context; - deps: DescriptBlockDeps; - }) => string | Buffer | DescriptJSON); - - body_compress?: boolean | ZlibOptions; - - is_json?: boolean; - - timeout?: number; - - is_error?: (error: DescriptError, request_options: DescriptRequestOptions) => boolean; - - is_retry_allowed?: (error: DescriptError, request_options: DescriptRequestOptions) => boolean; - max_retries?: number; - retry_timeout?: number; - - prepare_request_options?: (options: HttpsRequestOptions) => HttpsRequestOptions; - parse_body?: (result: { headers: Record< string, string >; body?: Buffer }, context: Context) => {}; - - family?: DescriptHttpBlockDescriptionCallback< number, Params, Context >; - - agent?: HttpsAgent | HttpsAgentOptions | false; - - auth?: DescriptHttpBlockDescriptionCallback< string, Params, Context >; - - ca?: DescriptHttpBlockDescriptionCallback< string | Buffer, Params, Context >; - cert?: DescriptHttpBlockDescriptionCallback< string | Buffer, Params, Context >; - ciphers?: DescriptHttpBlockDescriptionCallback< string, Params, Context >; - key?: DescriptHttpBlockDescriptionCallback< string | Buffer, Params, Context >; - passphrase?: DescriptHttpBlockDescriptionCallback< string, Params, Context >; - pfx?: DescriptHttpBlockDescriptionCallback< string | Buffer, Params, Context >; - rejectUnauthorized?: DescriptHttpBlockDescriptionCallback< boolean, Params, Context >; - secureProtocol?: DescriptHttpBlockDescriptionCallback< string, Params, Context >; - servername?: DescriptHttpBlockDescriptionCallback< string, Params, Context >; -} - -interface DescriptHttpBlock< - Context, - Params, - Result, - ParamsOut = Params, - BeforeResultOut = undefined, - ErrorResultOut = undefined, - AfterResultIn = Result, - AfterResultOut = AfterResultIn, - ResultOut = Exclude, -> { - < - ExtendedParams = Params, - ExtendedResult = Result, - ExtendedParamsOut = ExtendedParams, - ExtendedBeforeResultOut = undefined, - ExtendedErrorResultOut = undefined, - ExtendedAfterResultIn = ExtendedResult, - ExtendedAfterResultOut = ExtendedAfterResultIn, - ExtendedResultOut = Exclude, - >(args: { - block?: DescriptHttpBlockDescription< GetDescriptBlockParamsFnOut, Context >; - options?: DescriptBlockOptions< - Context, - GetDescriptBlockParamsFnIn, - GetDescriptBlockResultIn, - GetDescriptBlockParamsFnOut, - ExtendedBeforeResultOut, - ExtendedErrorResultOut, - GetDescriptBlockResultIn, - ExtendedAfterResultOut, - ExtendedResultOut - >; - }): DescriptHttpBlock< - Context, - DescriptBlockParams, GetDescriptBlockParamsFnOut>, - GetDescriptBlockResultOut> - >; -} - -declare function http< - Context, - Params, - Result, - ParamsOut = Params, - BeforeResultOut = undefined, - ErrorResultOut = undefined, - AfterResultIn = Result, - AfterResultOut = AfterResultIn, - ResultOut = Exclude, -> ( - args: { - block: DescriptHttpBlockDescription< GetDescriptBlockParamsFnOut, Context >; - options?: DescriptBlockOptions< - Context, - GetDescriptBlockParamsFnIn, - GetDescriptBlockResultIn, - GetDescriptBlockParamsFnOut, - BeforeResultOut, - ErrorResultOut, - GetDescriptBlockResultIn, - AfterResultOut, - ResultOut - >; - }, -): DescriptHttpBlock< -Context, -DescriptBlockParams, GetDescriptBlockParamsFnOut>, -GetDescriptBlockResultOut ->; - -// --------------------------------------------------------------------------------------------------------------- // -// FuncBlock - -type DescriptFuncBlockDescription< - Context, - Params, - Result, -> = (args: { - params: Params; - context: Context; - deps: DescriptBlockDeps; - generate_id: DescriptBlockGenerateId; - cancel: Cancel; -}) => -Result | -Promise; - -interface DescriptFuncBlock< - Context, - Params, - Result, - ParamsOut = Params, - BeforeResultOut = undefined, - ErrorResultOut = undefined, - AfterResultIn = Result, - AfterResultOut = AfterResultIn, - ResultOut = Exclude, -> { - < - ExtendedParams = Params, - ExtendedResult = Result, - ExtendedParamsOut = ExtendedParams, - ExtendedBeforeResultOut = undefined, - ExtendedErrorResultOut = undefined, - ExtendedAfterResultIn = ExtendedResult, - ExtendedAfterResultOut = ExtendedAfterResultIn, - ExtendedResultOut = ExtendedBeforeResultOut | ExtendedErrorResultOut | ExtendedAfterResultOut, - >(args: { - block?: DescriptFuncBlockDescription< Context, GetDescriptBlockParamsFnOut, GetDescriptBlockResultIn>; - options?: DescriptBlockOptions< - Context, - GetDescriptBlockParamsFnIn, - GetDescriptBlockResultIn>, - GetDescriptBlockParamsFnOut, - ExtendedBeforeResultOut, - ExtendedErrorResultOut, - GetDescriptBlockResultIn>, - ExtendedAfterResultOut, - ExtendedResultOut - >; - }): DescriptFuncBlock< Context, - DescriptBlockParams>, GetDescriptBlockParamsFnOut>>, - GetDescriptBlockResultOut> - >; -} - -declare function func< - Context, - Params, - ResultIn, - ParamsOut = Params, - BeforeResultOut = undefined, - ErrorResultOut = undefined, - AfterResultIn = ResultIn, - AfterResultOut = AfterResultIn, - ResultOut = Exclude, -> ( - args: { - block: DescriptFuncBlockDescription< Context, GetDescriptBlockParamsFnOut, GetDescriptBlockResultIn >; - options?: DescriptBlockOptions< - Context, - GetDescriptBlockParamsFnIn, - GetDescriptBlockResultIn>, - GetDescriptBlockParamsFnOut, - BeforeResultOut, - ErrorResultOut, - GetDescriptBlockResultIn>, - AfterResultOut, - ResultOut - >; - }, -): DescriptFuncBlock< Context, -DescriptBlockParams>, GetDescriptBlockParamsFnOut>>, -GetDescriptBlockResultOut> ->; - -// --------------------------------------------------------------------------------------------------------------- // -// ArrayBlock - -type GetArrayBlockResult< T > = { - 0: never; - 1: [ GetDescriptBlockResult< First< T > > ]; - 2: [ GetDescriptBlockResult< First< T > >, ...GetArrayBlockResult< Tail< T > > ]; -}[ T extends [] ? 0 : T extends ((readonly [ any ]) | [ any ]) ? 1 : 2 ]; - -type GetArrayBlockParams< T > = { - 0: never; - 1: GetDescriptBlockParams< First< T > >; - 2: GetDescriptBlockParams< First< T > > & GetArrayBlockParams< Tail< T > >; -}[ T extends [] ? 0 : T extends ((readonly [ any ]) | [ any ]) ? 1 : 2 ]; - -type GetArrayBlockContext< T > = { - 0: never; - 1: GetDescriptBlockContext< First< T > >; - 2: Equal< GetDescriptBlockContext< First< T > >, GetArrayBlockContext< Tail< T > > >; -}[ T extends [] ? 0 : T extends ((readonly [ any ]) | [ any ]) ? 1 : 2 ]; - -type DescriptArrayBlockDescription< T > = { - [ P in keyof T ]: T[ P ] extends DescriptBlock< infer Context, infer Params, infer ResultIn, infer ParamsOut, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer Result > ? T[ P ] : never -} - -interface DescriptArrayBlock< - Context, - Params, - ResultIn, - ParamsOut = Params, - BeforeResultOut = undefined, - ErrorResultOut = undefined, - AfterResultIn = ResultIn, - AfterResultOut = AfterResultIn, - ResultOut = Exclude, -> { - < - ExtendedParams = Params, - ExtendedResultIn = ResultIn, - ExtendedParamsOut = ExtendedParams, - ExtendedBeforeResultOut = undefined, - ExtendedErrorResultOut = undefined, - ExtendedAfterResultIn = ExtendedResultIn, - ExtendedAfterResultOut = ExtendedAfterResultIn, - ExtendedResultOut = Exclude, - >(args: { - options?: DescriptBlockOptions< - Context, - GetDescriptBlockParamsFnIn, - ExtendedResultIn, - GetDescriptBlockParamsFnOut, - ExtendedBeforeResultOut, - ExtendedErrorResultOut, - ExtendedAfterResultIn, - ExtendedAfterResultOut, - ExtendedResultOut - >; - }): DescriptArrayBlock< - Context, - DescriptBlockParams, GetDescriptBlockParamsFnOut>, - GetDescriptBlockResultOut> - >; -} - -declare function array< - Block extends ReadonlyArray< unknown >, - Context = GetArrayBlockContext< Block >, - Params = GetArrayBlockParams< Block >, - ResultIn = GetArrayBlockResult< Block >, - ParamsOut = Params, - BeforeResultOut = undefined, - ErrorResultOut = undefined, - AfterResultIn = GetArrayBlockResult< Block >, - AfterResultOut = GetArrayBlockResult< Block >, - ResultOut = Exclude, -> ( - args: { - block: DescriptArrayBlockDescription< Block >; - options?: DescriptBlockOptions< - Context, - GetDescriptBlockParamsFnIn, - ResultIn, - GetDescriptBlockParamsFnOut, - BeforeResultOut, - ErrorResultOut, - AfterResultIn, - AfterResultOut, - ResultOut - >; - }, -): DescriptArrayBlock< -Context, -DescriptBlockParams, GetDescriptBlockParamsFnOut>, -GetDescriptBlockResultOut> ->; - -// --------------------------------------------------------------------------------------------------------------- // -// ObjectBlock - -type GetObjectBlockResultIn< T > = { - [ P in keyof T ]: InferResultIn -} -type GetObjectBlockResultOut< T > = { - [ P in keyof T ]: InferResultOut -} -type GetObjectBlockParamsMap< T extends {} > = { - [ P in keyof T ]: InferParamsIn; -} - -type GetObjectBlockParamsBlockMap< T extends {} > = { - [ P in keyof T ]: GetDescriptBlockParamsBlock>; -} - -type GetObjectBlockParamsFnInMap< T extends {} > = { - [ P in keyof T ]: GetDescriptBlockParamsFnIn>; -} - -type GetObjectBlockParamsFnOutMap< T extends {} > = { - [ P in keyof T ]: GetDescriptBlockParamsFnOut>; -} - -type GetObjectBlockParams< - T extends {}, - PB = GetObjectBlockParamsBlockMap< T >, - PI = GetObjectBlockParamsFnInMap< T >, - PO = GetObjectBlockParamsFnOutMap< T >, -> = DescriptBlockParams< -UnionToIntersection, -UnionToIntersection, -UnionToIntersection ->; - -type GetObjectBlockContextMap< T extends {} > = { - [ P in keyof T ]: InferContext; -} -// Тут не совсем правда, но лучше пока непонятно как сделать. -type GetObjectBlockContext< T extends {}, M = GetObjectBlockContextMap< T > > = UnionToIntersection< M[ keyof M ] >; - -type DescriptObjectBlockDescription< T extends {} > = { - [ P in keyof T ]: T[ P ] extends DescriptBlock< infer Context, infer Params, infer ResultIn, infer ParamsOut, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer ResultOut > ? T[ P ] : never -} - -type DescriptObjectBlockDescriptionResults< T extends {} > = { - [ P in keyof T ]: T[ P ] extends DescriptBlock< infer Context, infer Params, infer ResultIn, infer ParamsOut, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer ResultOut > ? ResultOut : never -} - -interface DescriptObjectBlock< - Context, - Params, - ResultIn, - ParamsOut = Params, - BeforeResultOut = undefined, - ErrorResultOut = undefined, - AfterResultIn = ResultIn, - AfterResultOut = AfterResultIn, - ResultOut = Exclude, -> { - < - ExtendedParams = Params, - ExtendedResultIn = ResultIn, - ExtendedParamsOut = ExtendedParams, - ExtendedBeforeResultOut = undefined, - ExtendedErrorResultOut = undefined, - ExtendedAfterResultIn = ExtendedResultIn, - ExtendedAfterResultOut = ExtendedAfterResultIn, - ExtendedResultOut = Exclude, - >(args: { - options?: DescriptBlockOptions< - Context, - GetDescriptBlockParamsFnIn, - GetDescriptBlockResultIn>, - GetDescriptBlockParamsFnOut, - ExtendedBeforeResultOut, - ExtendedErrorResultOut, - ExtendedAfterResultIn, - ExtendedAfterResultOut, - ExtendedResultOut - >; - }): DescriptObjectBlock< - Context, - DescriptBlockParams, GetDescriptBlockParamsFnOut>, - GetDescriptBlockResultOut> - >; -} - -declare function object< - Block extends {}, - Context = GetObjectBlockContext< Block >, - Params = GetObjectBlockParams< Block >, - ResultIn = GetObjectBlockResultIn< Block >, - ParamsOut = Params, - //ResultOut = GetObjectBlockResultOut, - BeforeResultOut = undefined, - ErrorResultOut = undefined, - AfterResultIn = GetObjectBlockResultOut, - AfterResultOut = AfterResultIn, - ResultOut = Exclude, -> ( - args: { - block: DescriptObjectBlockDescription< Block >; - options?: DescriptBlockOptions< - Context, - GetDescriptBlockParamsFnIn, - GetDescriptBlockResultIn>, - GetDescriptBlockParamsFnOut, - BeforeResultOut, - ErrorResultOut, - GetDescriptBlockResultIn>, - AfterResultOut, - ResultOut - - /*GetDescriptBlockResultIn>, - GetDescriptBlockResultIn>, - GetDescriptBlockResultIn>, - InferResultOutFromBlocks, - InferResultOutFromBlocks*/ - >; - }, -): DescriptObjectBlock< -Context, -DescriptBlockParams, GetDescriptBlockParamsFnOut>, -GetDescriptBlockResultOut> ->; - -// --------------------------------------------------------------------------------------------------------------- // -/* -// --------------------------------------------------------------------------------------------------------------- // - -type DescriptPipeBlockDescription = Array< DescriptBlock >; - -interface DescriptPipeBlock { - ( args: { block?: DescriptPipeBlockDescription, options?: DescriptBlockOptions } ): DescriptPipeBlock; -} - -// --------------------------------------------------------------------------------------------------------------- // - -type DescriptFirstBlockDescription = Array< DescriptBlock >; - -interface DescriptFirstBlock { - ( args: { block?: DescriptFirstBlockDescription, options?: DescriptBlockOptions } ): DescriptFirstBlock; -} - -// --------------------------------------------------------------------------------------------------------------- // - -*/ - -type DescriptBlock< Context, Params, Result, ParamsOut = Params, BeforeResultOut = Result, ErrorResultOut = Result, AfterResultIn = Result, AfterResultOut = Result, ResultOut = BeforeResultOut | ErrorResultOut | AfterResultOut > = - DescriptHttpBlock< Context, Params, Result, ParamsOut, BeforeResultOut, ErrorResultOut, AfterResultIn, AfterResultOut, ResultOut > | - DescriptFuncBlock< Context, Params, Result, ParamsOut, BeforeResultOut, ErrorResultOut, AfterResultIn, AfterResultOut, ResultOut > | - DescriptArrayBlock< Context, Params, Result, ParamsOut, BeforeResultOut, ErrorResultOut, AfterResultIn, AfterResultOut, ResultOut > | - DescriptObjectBlock< Context, Params, Result, ParamsOut, BeforeResultOut, ErrorResultOut, AfterResultIn, AfterResultOut, ResultOut >; - -// --------------------------------------------------------------------------------------------------------------- // - -export type GetDescriptBlockResult< T > = - T extends DescriptBlock< infer Context, infer Params, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer Result > ? Result : never; - -export type GetDescriptBlockParams< T > = - T extends DescriptBlock< infer Context, infer Params, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer Result > ? Params : never; - -export type GetDescriptBlockContext< T > = - T extends DescriptBlock< infer Context, infer Params, infer BeforeResultOut, infer ErrorResultOut, infer AfterResultIn, infer AfterResultOut, infer Result > ? Context : never; - -// --------------------------------------------------------------------------------------------------------------- // - -declare function run< - Context, - Params, - Result, - ParamsOut = Params, - BeforeResultOut = undefined, - ErrorResultOut = undefined, - AfterResultIn = Result, - AfterResultOut = AfterResultIn, - ResultOut = Exclude, -> ( - block: DescriptBlock< - Context, - GetDescriptBlockParamsFnIn, - GetDescriptBlockResultIn>, - GetDescriptBlockParamsFnOut, - BeforeResultOut, - ErrorResultOut, - AfterResultIn, - AfterResultOut, - ResultOut - >, - args: { - params?: GetDescriptBlockParamsBlock; - context: Context; - }, -): Promise< GetDescriptBlockResultOut>> ; - -// --------------------------------------------------------------------------------------------------------------- // - -declare function is_block(block: any): boolean; - -// --------------------------------------------------------------------------------------------------------------- // - -declare function error(error: { id: string; [ key: string ]: any }): DescriptError; -declare function is_error(error: any): error is DescriptError; - -declare enum ERROR_ID { - ALL_BLOCKS_FAILED = 'ALL_BLOCKS_FAILED', - BLOCK_TIMED_OUT = 'BLOCK_TIMED_OUT', - DEPS_ERROR = 'DEPS_ERROR', - DEPS_NOT_RESOLVED = 'DEPS_NOT_RESOLVED', - HTTP_REQUEST_ABORTED = 'HTTP_REQUEST_ABORTED', - HTTP_UNKNOWN_ERROR = 'HTTP_UNKNOWN_ERROR', - INCOMPLETE_RESPONSE = 'INCOMPLETE_RESPONSE', - INVALID_BLOCK = 'INVALID_BLOCK', - INVALID_DEPS_ID = 'INVALID_DEPS_ID', - INVALID_JSON = 'INVALID_JSON', - INVALID_OPTIONS_PARAMS = 'INVALID_OPTIONS_PARAMS', - PARSE_BODY_ERROR = 'PARSE_BODY_ERROR', - REQUEST_TIMEOUT = 'REQUEST_TIMEOUT', - REQUIRED_BLOCK_FAILED = 'REQUIRED_BLOCK_FAILED', - TCP_CONNECTION_TIMEOUT = 'TCP_CONNECTION_TIMEOUT', - TOO_MANY_AFTERS_OR_ERRORS = 'TOO_MANY_AFTERS_OR_ERRORS', - UNKNOWN_ERROR = 'UNKNOWN_ERROR', -} - -// --------------------------------------------------------------------------------------------------------------- // - -declare namespace request { - interface DefaultOptions { - is_error: (error: DescriptError, request_options: DescriptRequestOptions) => boolean; - is_retry_allowed: (error: DescriptError, request_options: DescriptRequestOptions) => boolean; - } - - export const DEFAULT_OPTIONS: DefaultOptions; -} - - -declare function inferBlockTypes< - Context, - Params, - ResultIn, - ParamsOut = Params, - BeforeResultOut = ResultIn, - ErrorResultOut = ResultIn, - AfterResultIn = ResultIn, - AfterResultOut = AfterResultIn, - ResultOut = Exclude, ->(block: DescriptBlock): typeof block; diff --git a/lib/index.ts b/lib/index.ts index 403c4b6..9d52dae 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -3,10 +3,10 @@ import { ERROR_ID, createError, isError, DescriptError } from './error'; import Cancel from './cancel'; import Logger from './logger'; -import Cache from './cache'; +import Cache, { CacheItem } from './cache'; import request from './request'; - +import type { GenerateId, DescriptBlockDeps, DescriptBlockId } from './depsDomain'; import Block from './block'; import ArrayBlock from './arrayBlock'; import ObjectBlock from './objectBlock'; @@ -15,9 +15,17 @@ import FunctionBlock from './functionBlock'; import HttpBlock from './httpBlock'; import FirstBlock from './firstBlock'; -import type { DescriptHttpBlockResult, BlockResultOut, DescriptBlockOptions } from './types'; +import type { + DescriptHttpBlockResult, + BlockResultOut, + DescriptBlockOptions, + DescriptHttpBlockHeaders, + InferResultFromBlock, + InferParamsInFromBlock, + InferBlock, +} from './types'; import type BaseBlock from './block'; -import type { DescriptHttpBlockDescription } from './httpBlock'; +import type { DescriptHttpBlockDescription, DescriptHttpBlockQuery, DescriptHttpBlockQueryValue } from './httpBlock'; import type { GetObjectBlockParams, GetObjectBlockResult, ObjectBlockDefinition } from './objectBlock'; import type { GetArrayBlockParams, GetArrayBlockResult, ArrayBlockDefinition } from './arrayBlock'; import type { GetFirstBlockParams, GetFirstBlockResult, FirstBlockDefinition } from './firstBlock'; @@ -169,6 +177,7 @@ const run = function< export { Logger, Cache, + CacheItem, request, ERROR_ID, createError as error, @@ -184,4 +193,18 @@ export { run, DescriptError, DescriptHttpBlockResult, + DescriptHttpBlockHeaders, + DescriptHttpBlockDescription, + DescriptHttpBlockQuery, + DescriptHttpBlockQueryValue, + GenerateId, + DescriptBlockId, + InferResultFromBlock, + InferParamsInFromBlock, + InferBlock, + DescriptBlockDeps, + BaseBlock, + BlockResultOut, + + HttpBlock, }; diff --git a/lib/objectBlock.ts b/lib/objectBlock.ts index fecb11a..2df7443 100644 --- a/lib/objectBlock.ts +++ b/lib/objectBlock.ts @@ -2,7 +2,7 @@ import CompositeBlock from './compositeBlock'; import type { DescriptError } from './error'; import { createError, ERROR_ID } from './error'; import type BaseBlock from './block'; -import type { InferParamsInFromBlock, InferResultFromBlock, DescriptBlockOptions, BlockResultOut, UnionToIntersection } from './types'; +import type { InferParamsInFromBlock, DescriptBlockOptions, BlockResultOut, UnionToIntersection, InferResultFromBlock } from './types'; import type ContextClass from './context'; import type Cancel from './cancel'; import type { DescriptBlockDeps } from './depsDomain'; @@ -112,7 +112,8 @@ class ObjectBlock< // eslint-disable-next-line @typescript-eslint/no-unused-vars ExtendedResultOut extends BlockResultOut, - ExtendedParamsOut = Params, + //ExtendedCustomBlock extends ObjectBlockDefinition, + ExtendedParamsOut extends Params = Params, ExtendedParams = Params, ExtendedBlockResult = ResultOut, ExtendedBeforeResultOut = undefined, @@ -120,28 +121,13 @@ class ObjectBlock< ExtendedErrorResultOut = undefined, >({ options }: { options: DescriptBlockOptions< - Context, Params & ExtendedParamsOut, ExtendedBlockResult, ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams + Context, ExtendedParamsOut, ExtendedBlockResult, ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams >; }) { - return this.extendClass< - ObjectBlock< - Context, - Blocks, - ExtendedResultOut, - Params & ExtendedParamsOut, - ExtendedBlockResult, - ExtendedBeforeResultOut, - ExtendedAfterResultOut, - ExtendedErrorResultOut, - ExtendedParams - >, - ExtendedBlockResult, - Params & ExtendedParamsOut, - ExtendedParams, - ExtendedBeforeResultOut, - ExtendedAfterResultOut, - ExtendedErrorResultOut - >({ options }); + return new ObjectBlock({ + block: this.extendBlock(this.block), + options: this.extendOptions(this.options, options) as typeof options, + }); } } diff --git a/lib/pipeBlock.ts b/lib/pipeBlock.ts index 4ad0749..6c5a07c 100644 --- a/lib/pipeBlock.ts +++ b/lib/pipeBlock.ts @@ -85,9 +85,10 @@ class PipeBlock< extend< // eslint-disable-next-line @typescript-eslint/no-unused-vars - ExtendedResultOut extends - BlockResultOut, - ExtendedParamsOut = Params, + //ExtendedResultOut extends + //BlockResultOut, + //ExtendedCustomBlock extends PipeBlockDefinition, + ExtendedParamsOut extends Params = Params, ExtendedParams = Params, ExtendedBlockResult = ResultOut, ExtendedBeforeResultOut = undefined, @@ -95,28 +96,13 @@ class PipeBlock< ExtendedErrorResultOut = undefined, >({ options }: { options: DescriptBlockOptions< - Context, Params & ExtendedParamsOut, ExtendedBlockResult, ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams + Context, ExtendedParamsOut, ExtendedBlockResult, ExtendedBeforeResultOut, ExtendedAfterResultOut, ExtendedErrorResultOut, ExtendedParams >; }) { - return this.extendClass< - PipeBlock< - Context, - Block, - ExtendedResultOut, - Params & ExtendedParamsOut, - ExtendedBlockResult, - ExtendedBeforeResultOut, - ExtendedAfterResultOut, - ExtendedErrorResultOut, - ExtendedParams - >, - ExtendedBlockResult, - ExtendedParamsOut, - ExtendedParams, - ExtendedBeforeResultOut, - ExtendedAfterResultOut, - ExtendedErrorResultOut - >({ options }); + return new PipeBlock({ + block: this.extendBlock(this.block), + options: this.extendOptions(this.options, options) as typeof options, + }); } diff --git a/lib/types.ts b/lib/types.ts index 0847b6f..aeead71 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -59,12 +59,12 @@ export type BlockResultOut< ErrorResultOut > = (never | undefined)extends ErrorResultOut ? - AfterResultOut extends undefined ? + undefined extends AfterResultOut ? never | undefined extends BeforeResultOut ? BlockResult : BeforeResultOut | BlockResult : AfterResultOut : - AfterResultOut extends undefined ? + undefined extends AfterResultOut ? never | undefined extends BeforeResultOut ? BlockResult | ErrorResultOut : BeforeResultOut | BlockResult | ErrorResultOut : @@ -77,6 +77,13 @@ infer Context, infer CustomBlock, infer ParamsOut, infer ResultOut, infer Interm infer BlockResult, infer BeforeResultOut, infer AfterResultOut, infer ErrorResultOut, infer Params > ? InferResultOrResult : Result; +export type InferResultOrResultOnce = Result extends BaseBlock< +// eslint-disable-next-line @typescript-eslint/no-unused-vars +infer Context, infer CustomBlock, infer ParamsOut, infer ResultOut, infer IntermediateResult, +// eslint-disable-next-line @typescript-eslint/no-unused-vars +infer BlockResult, infer BeforeResultOut, infer AfterResultOut, infer ErrorResultOut, infer Params +> ? ResultOut : Result; + export type InferResultFromBlock = Type extends BaseBlock< // eslint-disable-next-line @typescript-eslint/no-unused-vars infer Context, infer CustomBlock, infer ParamsOut, infer ResultOut, infer IntermediateResult, @@ -89,7 +96,6 @@ export type InferParamsInFromBlock = Type extends BaseBlock< infer Context, infer CustomBlock, infer ParamsOut, infer ResultOut, infer IntermediateResult, // eslint-disable-next-line @typescript-eslint/no-unused-vars infer BlockResult, infer BeforeResultOut, infer AfterResultOut, infer ErrorResultOut, infer Params - > ? Params : never; @@ -109,6 +115,13 @@ infer BlockResult, infer BeforeResultOut, infer AfterResultOut, infer ErrorResul > ? ParamsOut : never; +export type InferBlock = Type extends BaseBlock< +// eslint-disable-next-line @typescript-eslint/no-unused-vars +infer Context, infer CustomBlock, infer ParamsOut, infer ResultOut, infer IntermediateResult, +// eslint-disable-next-line @typescript-eslint/no-unused-vars +infer BlockResult, infer BeforeResultOut, infer AfterResultOut, infer ErrorResultOut, infer Params +> ? Type : never; + export type InferContextFromBlock< T > = T extends BaseBlock< // eslint-disable-next-line @typescript-eslint/no-unused-vars infer Context, infer CustomBlock, infer ParamsOut, infer ResultOut, infer IntermediateResult, @@ -131,9 +144,9 @@ export interface DescriptBlockOptions< Context, ParamsOut, BlockResult, - BeforeResultOut, - AfterResultOut, - ErrorResultOut, + BeforeResultOut = undefined, + AfterResultOut = undefined, + ErrorResultOut = undefined, Params = ParamsOut, > { name?: string; @@ -159,7 +172,7 @@ export interface DescriptBlockOptions< context?: Context; deps: DescriptBlockDeps; cancel: Cancel; - result: BeforeResultOut extends (undefined | void) ? + result: (undefined) extends BeforeResultOut ? InferResultOrResult : InferResultOrResult | InferResultOrResult; }) => AfterResultOut; diff --git a/package.json b/package.json index 4113e76..4e684c7 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "typescript": "^5.6.2" }, "files": [ - "lib" + "build" ], "main": "./build/index.js", "engines": { From a904a2626488dfafca13e16895f91ade306f1c5f Mon Sep 17 00:00:00 2001 From: Egor Dydykin Date: Fri, 8 Nov 2024 10:24:18 +0300 Subject: [PATCH 3/3] v4: typescript first --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4e684c7..3310d70 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ }, "name": "descript", "description": "descript", - "version": "4.0.0", + "version": "3.4.0", "homepage": "https://github.com/pasaran/descript3", "repository": { "type": "git",