├── .dockerignore ├── .github └── workflows │ ├── release.yml │ └── size-limit.yml ├── .gitignore ├── .prettierrc.json ├── .size-limit.js ├── Dockerfile ├── LICENSE ├── README.md ├── docker-compose.yml ├── docs ├── core │ ├── context.md │ ├── execution.md │ ├── index.md │ └── plugin.md ├── how-to │ └── index.md └── plugins │ ├── batch.md │ ├── cache-deduplicate.md │ ├── cache-etag.md │ ├── cache-fallback.md │ ├── cache-memory.md │ ├── cache-persistent.md │ ├── circuit-breaker.md │ ├── index.md │ ├── log.md │ ├── prom-red-metrics.md │ ├── protocol-http.md │ ├── protocol-jsonp.md │ ├── retry.md │ ├── transform-url.md │ └── validate.md ├── jest.config.js ├── lerna.json ├── package.json ├── packages ├── cache-utils │ ├── .npmignore │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ ├── cacheKey.spec.ts │ │ ├── cacheKey.ts │ │ ├── constants │ │ │ └── metaTypes.ts │ │ ├── getCacheKey.spec.ts │ │ ├── getCacheKey.ts │ │ ├── index.ts │ │ ├── shouldCacheExecute.spec.ts │ │ └── shouldCacheExecute.ts │ └── tsconfig.json ├── core │ ├── .npmignore │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ ├── constants │ │ │ └── status.ts │ │ ├── context │ │ │ ├── Context.h.ts │ │ │ ├── Context.spec.ts │ │ │ └── Context.ts │ │ ├── index.ts │ │ ├── request │ │ │ ├── request.h.ts │ │ │ ├── request.spec.ts │ │ │ └── request.ts │ │ └── types.h.ts │ └── tsconfig.json ├── plugin-batch │ ├── .npmignore │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ ├── batch.spec.ts │ │ ├── batch.ts │ │ └── constants │ │ │ └── metaTypes.ts │ └── tsconfig.json ├── plugin-cache-deduplicate │ ├── .npmignore │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ ├── deduplicate.spec.ts │ │ └── deduplicate.ts │ └── tsconfig.json ├── plugin-cache-etag │ ├── .npmignore │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ ├── constants.ts │ │ ├── etag.spec.ts │ │ ├── etag.ts │ │ └── noop.ts │ └── tsconfig.json ├── plugin-cache-fallback │ ├── .npmignore │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ ├── drivers │ │ │ ├── fs.spec.ts │ │ │ ├── fs.ts │ │ │ ├── index.ts │ │ │ ├── md5.spec.ts │ │ │ ├── md5.ts │ │ │ ├── memory.spec.ts │ │ │ ├── memory.ts │ │ │ └── noop.ts │ │ ├── fallback.spec.ts │ │ ├── fallback.ts │ │ └── types.ts │ └── tsconfig.json ├── plugin-cache-memory │ ├── .npmignore │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ ├── memory.spec.ts │ │ ├── memory.ts │ │ └── utils.ts │ └── tsconfig.json ├── plugin-cache-persistent │ ├── .npmignore │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ ├── persistent.spec.ts │ │ └── persistent.ts │ └── tsconfig.json ├── plugin-circuit-breaker │ ├── .npmignore │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ ├── CircuitBreaker.spec.ts │ │ ├── CircuitBreaker.ts │ │ ├── circuit-breaker.spec.ts │ │ ├── circuit-breaker.ts │ │ ├── constants.ts │ │ ├── errors.ts │ │ └── noop.ts │ └── tsconfig.json ├── plugin-log │ ├── .npmignore │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ ├── constants │ │ │ └── metaTypes.ts │ │ ├── log.spec.ts │ │ └── log.ts │ └── tsconfig.json ├── plugin-prom-red-metrics │ ├── .npmignore │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ ├── constants │ │ │ └── metaTypes.ts │ │ ├── httpMetrics.spec.ts │ │ ├── httpMetrics.ts │ │ ├── index.ts │ │ ├── metrics.spec.ts │ │ └── metrics.ts │ └── tsconfig.json ├── plugin-protocol-http │ ├── .npmignore │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ ├── constants.ts │ │ ├── errors.ts │ │ ├── fetch.browser.ts │ │ ├── fetch.ts │ │ ├── form.spec.ts │ │ ├── form.ts │ │ ├── http.spec.ts │ │ ├── http.ts │ │ ├── index.ts │ │ ├── parse.spec.ts │ │ ├── parse.ts │ │ ├── serialize.spec.ts │ │ ├── serialize.ts │ │ ├── utils.spec.ts │ │ ├── utils.ts │ │ └── validators.ts │ └── tsconfig.json ├── plugin-protocol-jsonp │ ├── .npmignore │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ ├── jsonp.spec.ts │ │ ├── jsonp.ts │ │ └── noop.ts │ └── tsconfig.json ├── plugin-retry │ ├── .npmignore │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ ├── constants.ts │ │ ├── retry.spec.ts │ │ └── retry.ts │ └── tsconfig.json ├── plugin-transform-url │ ├── .npmignore │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ ├── url.spec.ts │ │ └── url.ts │ └── tsconfig.json ├── plugin-validate │ ├── .npmignore │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ │ ├── constants.ts │ │ ├── validate.spec.ts │ │ └── validate.ts │ └── tsconfig.json └── url-utils │ ├── .npmignore │ ├── CHANGELOG.md │ ├── package.json │ ├── src │ ├── index.ts │ ├── types.ts │ ├── url.spec.ts │ └── url.ts │ └── tsconfig.json ├── tsconfig.json ├── website ├── README.md ├── core │ └── Footer.js ├── i18n │ └── en.json ├── package.json ├── pages │ └── en │ │ └── index.js ├── sidebars.json ├── siteConfig.js └── static │ ├── css │ └── custom.css │ └── img │ ├── docusaurus.svg │ ├── favicon.png │ ├── favicon │ └── favicon.ico │ ├── logo-tinkoff.svg │ └── oss_logo.png └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | */node_modules 2 | *.log -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | env: 9 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 10 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 11 | 12 | jobs: 13 | release: 14 | if: "!contains(github.event.head_commit.message, 'chore(release)')" 15 | 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Initialize Git user 20 | run: | 21 | git config --global user.name 'tinkoff-bot' 22 | git config --global user.email 'tinkoff-bot@users.noreply.github.com' 23 | 24 | - uses: actions/checkout@v3 25 | with: 26 | fetch-depth: 0 27 | token: ${{ secrets.TINKOFF_BOT_PAT }} 28 | 29 | - uses: actions/setup-node@v3 30 | with: 31 | node-version: 18.x 32 | registry-url: 'https://registry.npmjs.org' 33 | 34 | - name: Install dependencies 35 | run: yarn bootstrap 36 | 37 | - name: Build packages 38 | run: | 39 | yarn build 40 | git diff --exit-code || (echo "Найдены изменения в файлах package.json – выполните `yarn build` в корне репозитория и запушьте изменения" && exit 127) 41 | 42 | - name: Release packages 43 | run: yarn release 44 | env: 45 | GH_TOKEN: ${{ secrets.TINKOFF_BOT_PAT }} 46 | 47 | - name: Publish docs 48 | run: yarn docs:publish 49 | env: 50 | GIT_USER: ${{ secrets.TINKOFF_BOT_PAT }} 51 | -------------------------------------------------------------------------------- /.github/workflows/size-limit.yml: -------------------------------------------------------------------------------- 1 | name: "size" 2 | on: 3 | pull_request: 4 | branches: 5 | - master 6 | jobs: 7 | size: 8 | runs-on: ubuntu-latest 9 | env: 10 | CI_JOB_NUMBER: 1 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: andresz1/size-limit-action@v1 14 | with: 15 | github_token: ${{ secrets.TINKOFF_BOT_PAT }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | **/package-lock.json 8 | 9 | # Dependency directories 10 | node_modules/ 11 | 12 | # IDE 13 | .DS_Store 14 | .vscode 15 | .history 16 | .devcontainer 17 | .idea 18 | 19 | # Coverage 20 | coverage 21 | 22 | # Compiled sources 23 | lib 24 | build 25 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "typescript", 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "proseWrap": "never", 6 | "arrowParens": "always", 7 | "printWidth": 120, 8 | "tabWidth": 4 9 | } 10 | -------------------------------------------------------------------------------- /.size-limit.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const glob = require('fast-glob'); 3 | const path = require('path'); 4 | 5 | const entries = glob.sync('packages/*/package.json') 6 | .map((packageJson) => { 7 | const config = fs.readJsonSync(packageJson); 8 | let modulePath = config.module; 9 | 10 | if (config.browser) { 11 | if (typeof config.browser === 'object') { 12 | modulePath = config.browser[`./${modulePath}`] ?? modulePath; 13 | } else { 14 | modulePath = config.browser; 15 | } 16 | } 17 | 18 | return { name: config.name, path: path.resolve(path.dirname(packageJson), modulePath) }; 19 | }); 20 | 21 | 22 | module.exports = entries.map((entry) => { 23 | return { 24 | name: entry.name, 25 | path: entry.path, 26 | ignore: ['@tinkoff/request-*', 'tslib'] 27 | } 28 | }); 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:8.11.4 2 | 3 | WORKDIR /app/website 4 | 5 | EXPOSE 3000 35729 6 | COPY ./docs /app/docs 7 | COPY ./website /app/website 8 | RUN yarn install 9 | 10 | CMD ["yarn", "start"] 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @tinkoff/request 2 | Modular lightweight request library extendable by plugins. 3 | 4 | [Documentation](https://tinkoff.github.io/tinkoff-request/) 5 | 6 | [![Edit on CodeSandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/tinkoff-request-playground-0t1wrs?file=/src/index.ts) 7 | 8 | ## Example of usage 9 | ```javascript 10 | import request from '@tinkoff/request-core'; 11 | import log from '@tinkoff/request-plugin-log'; 12 | import deduplicateCache from '@tinkoff/request-plugin-cache-deduplicate'; 13 | import memoryCache from '@tinkoff/request-plugin-cache-memory'; 14 | import persistentCache from '@tinkoff/request-plugin-cache-persistent'; 15 | import fallbackCache from '@tinkoff/request-plugin-cache-fallback'; 16 | import validate from '@tinkoff/request-plugin-validate'; 17 | import http from '@tinkoff/request-plugin-protocol-http'; 18 | 19 | const makeRequest = request([ 20 | // The order of plugins is important 21 | log(), // log-plugin is first as we want it always execute 22 | memoryCache({ allowStale: true }), // passing parameters for specific plugin, see plugin docs 23 | persistentCache(), 24 | fallbackCache(), // fallbackCache is the last as it executed only for errored requests 25 | deduplicateCache(), // plugins for cache are coming from simple one to complex as if simple cache has cached value - it will be returned and the others plugins won't be called 26 | validate({ 27 | validator: ({ response }) => { 28 | if (response.type === 'json') { return null; } 29 | return new Error('NOT json format'); 30 | } 31 | }), // validate is placed exactly before plugin for actual request since there is no point to validate values from caches 32 | http() // on the last place the plugin to make actual request, it will be executed only if no plugin before changed the flow of request 33 | ]); 34 | 35 | makeRequest({ 36 | url: 'https://config.mysite.ru/resources?name=example' 37 | }) 38 | .then(result => console.log(result)) 39 | .catch(error => console.error(error)) 40 | ``` 41 | 42 | ## Plugins 43 | Plugins can inject to the request flow to change its execution or adjust request\response data. 44 | 45 | ### List of plugins 46 | 1. `protocol/http` - base plugin to make request with http\https 47 | 1. `protocol/jsonp` - plugin to make jsonp requests 48 | 1. `cache/deduplicate` - deduplicate identical requests 49 | 1. `cache/memory` - caches responses into memory 50 | 1. `cache/etag` - [!server only] cache based on etag http-header 51 | 1. `cache/fallback` - [!server only] stores response data to disk and returns from cache only for errored requests 52 | 1. `cache/persistent` - [!browser only] caching data at IndexedDB to keep cache among page reloads 53 | 1. `log` - logs the data of request execution 54 | 1. `batch` - groups several request into single one 55 | 1. `validate` - validate response or restore on errors 56 | 1. `transform-url` - transforms url string for every request 57 | 1. `circuit-breaker` - [!server only] fault protections 58 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | docusaurus: 5 | build: . 6 | ports: 7 | - 3000:3000 8 | - 35729:35729 9 | volumes: 10 | - ./docs:/app/docs 11 | - ./website/blog:/app/website/blog 12 | - ./website/core:/app/website/core 13 | - ./website/i18n:/app/website/i18n 14 | - ./website/pages:/app/website/pages 15 | - ./website/static:/app/website/static 16 | - ./website/sidebars.json:/app/website/sidebars.json 17 | - ./website/siteConfig.js:/app/website/siteConfig.js 18 | working_dir: /app/website 19 | -------------------------------------------------------------------------------- /docs/core/context.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: context 3 | title: @tinkoff/request-core - Context 4 | sidebar_label: Context 5 | --- 6 | 7 | Special class for storing state of the request. The instance of this class created by @tinkof/request-core for every request and get passed to each plugin. 8 | It is contains current state (e.g. status, request, response, error) and meta data for plugins. 9 | 10 | Meta data for plugins might be either internal or external and only used by plugins (purpose of that division is that external meta usually logged by @tinkoff/request-plugin-log for debbugging purpose, while internal meta is not). 11 | 12 | ## Methods 13 | 14 | ### getState() 15 | 16 | Return current state of the request, with following properties: 17 | - `status` - status of the current request execution, either 'init', 'complete' or 'error' 18 | - `request` - property is set by @tinkoff/request-core, and equal to options passed while making request 19 | - `response` - property is set by plugins and will be used as the result of the request if execution has succeeded (might be null if request in progress, on not succeeded) 20 | - `error` - property is set either by plugins or by @tinkoff/request-core if plugin execution failed, and contains current error (equal to null for success requests) 21 | - any additional properties set by plugins 22 | 23 | ### setState(obj) 24 | 25 | Current state of the context will be shallow merged with passed argument and the result will be set as new state. 26 | 27 | ### getStatus() 28 | 29 | Alias for `context.getState().status` 30 | 31 | ### getRequest() 32 | 33 | Alias for `context.getState().request` 34 | 35 | ### getResponse() 36 | 37 | Alias for `context.getState().response` 38 | 39 | ### getInternalMeta(metaName?) 40 | 41 | Get the internal meta data by specific name, if name is not defined return all internal meta; 42 | 43 | ### getExternalMeta(metaName?) 44 | 45 | Get the external meta data by specific name, if name is not defined return all external meta; 46 | 47 | ### updateInternalMeta(name, obj) 48 | 49 | Extend internal meta for specific name by passed obj. 50 | 51 | ### updateExternalMeta 52 | 53 | Extend external meta for specific name by passed obj. 54 | 55 | -------------------------------------------------------------------------------- /docs/core/execution.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: execution 3 | title: @tinkoff/request-core - Request Execution flow 4 | sidebar_label: Request execution 5 | --- 6 | 7 | Execution flow defines how request is executed and how plugins are get called while execution. 8 | 9 | !!!Plugins always executed in sequence mode (e.g. one by one) and order of plugins is important. 10 | 11 | In short: @tinkoff/request-core executes `init` handlers for each plugin from first plugin to the plugin which changes status to either `complete` or `error`, 12 | then executed handlers for new status, from current plugin (not including) to the first one (e.g. executes plugins in backward order). 13 | 14 | If some handler for status is absent in plugin, execution just goes to next plugin in chain. 15 | 16 | Execution of the request might be at one of three statuses: 17 | - `init` - init status, plugins are executed first to last, until some plugin changes flow to `complete` or `error` status. 18 | - `complete` - means successful execution, plugins are executed from the plugin which changed status to `complete` to first plugin. 19 | - `error` - means response finished with error, plugins are executed from the plugin which changed status to `error` to first plugin. 20 | 21 | At any status plugins can change the inner state of request or switch current status (in that case plugins placed after current plugin won't be executed) 22 | 23 | ## Example 24 | 25 | ```typescript 26 | import request from '@tinkoff/request-core' 27 | // ... other plugins imports 28 | 29 | const makeRequest = request([ 30 | plugin1, // changes status to `complete` if request is executed with option example === 'second' 31 | plugin2, // changes status to `error` if requests is executed with options example === 'third' 32 | plugin3, // changes status to `complete` any way (otherwise execution will hang if no plugin does it) 33 | ]) 34 | 35 | makeRequest({url: 'test', example: 'first' }) // will be executed one by one: plugin1.init => plugin2.init => plugin3.init => plugin2.complete => plugin1.complete. 36 | makeRequest({url: 'test', example: 'second' }) // will be executed: plugin1.init. 37 | makeRequest({url: 'test', example: 'third' }) // will be executed one by one: plugin1.init => plugin2.init => plugin1.error. 38 | 39 | ``` 40 | 41 | -------------------------------------------------------------------------------- /docs/core/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: index 3 | title: @tinkoff/request-core 4 | sidebar_label: Core 5 | --- 6 | 7 | Modular lightweight request library extendable by plugins. 8 | 9 | ## Example of usage 10 | ```javascript 11 | import request from '@tinkoff/request-core'; 12 | import log from '@tinkoff/request-plugin-log'; 13 | import deduplicateCache from '@tinkoff/request-plugin-cache-deduplicate'; 14 | import memoryCache from '@tinkoff/request-plugin-cache-memory'; 15 | import etagCache from '@tinkoff/request-plugin-cache-etag'; 16 | import persistentCache from '@tinkoff/request-plugin-cache-persistent'; 17 | import fallbackCache from '@tinkoff/request-plugin-cache-fallback'; 18 | import validate from '@tinkoff/request-plugin-validate'; 19 | import circuitBreaker from '@tinkoff/request-plugin-circuit-breaker' 20 | import retry from '@tinkoff/request-plugin-retry' 21 | import http from '@tinkoff/request-plugin-protocol-http'; 22 | 23 | const makeRequest = request([ 24 | // The order of plugins is important 25 | log(), // log-plugin is first as we want it always execute 26 | deduplicateCache(), // plugins for cache are coming from simple one to complex as if simple cache has cached value - it will be returned and the others plugins won't be called 27 | memoryCache({ allowStale: true }), // passing parameters for specific plugin, see plugin docs 28 | persistentCache(), 29 | fallbackCache(), // fallbackCache is the last as it executed only for errored requests 30 | etagCache(), 31 | validate({ 32 | validator: ({ response }) => { 33 | if (response.type === 'json') { return null; } 34 | return new Error('NOT json format'); 35 | } 36 | }), // validate is placed exactly before plugin for actual request since there is no point to validate values from caches 37 | circuitBreaker({ 38 | failureThreshold: 60, 39 | failureTimeout: 60000, 40 | }), // if 60% of requests in 1 min are failed, go to special state preventing from making new requests till service is down 41 | retry({ retry: 3, retryDelay: 100 }), // try to retry failed request, should be placed before making actual request 42 | http() // on the last place the plugin to make actual request, it will be executed only if no plugin before changed the flow of request 43 | ]); 44 | 45 | makeRequest({ 46 | url: 'https://config.mysite.ru/resources?name=example' 47 | }) 48 | .then(result => console.log(result)) 49 | .catch(error => console.error(error)) 50 | ``` 51 | -------------------------------------------------------------------------------- /docs/core/plugin.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: plugin 3 | title: Plugin definition 4 | sidebar_label: Plugin 5 | --- 6 | 7 | Plugin is just a plain object with specific methods. 8 | 9 | Plugin interface (all entries are optional): 10 | - `shouldExecute(context)` - boolean function indicating is plugin should execute at current status 11 | - `init(context, next, makeRequest?)` - this function will be called at `init` status 12 | - `complete(context, next, makeRequest?)` - this function will be called at `complete` status 13 | - `error(context, next, makeRequest?)` - this function will be called at `error` status 14 | 15 | Where arguments are: 16 | - `context` - the current request execution context, see [Context](./context) 17 | - `next` - callback function which should be called after plugin did its job. 18 | - `makeRequest` - function to make request (it will be executed with current request library settings) 19 | 20 | ## Example 21 | 22 | ```typescript 23 | import request from '@tinkoff/request-core'; 24 | 25 | const plugin = { 26 | shouldExecute: (context) => { 27 | return context.getRequest().log === true; 28 | }, 29 | init: (context, next) => { 30 | setTimeout(() => { 31 | context.updateExternalMeta('my-plugin', { log: true }); // update meta for easier debugging 32 | console.debug(context.getRequest()); // log request settings 33 | next(); // next plugin wont be executed until `next` get called 34 | }, 1000) 35 | }, 36 | complete: (context, next) => { 37 | console.log(context.getResponse()); // log response value 38 | next(); 39 | }, 40 | error: (context, next, makeRequest) => { 41 | const { error } = context.getState(); 42 | 43 | makeRequest({ // execute request, it is same as calling `req`, but plugins usually are defined in different module, so it is just a helpful option 44 | url: './error-reporter', 45 | payload: error, 46 | log: false, // disable this plugin from logging, otherwise call to ./error-reporter will be logged by this.plugin 47 | }).finally(next); // calling next is neccessary 48 | } 49 | } 50 | 51 | const req = request([ 52 | plugin, // plugins order is important, see 'Request exection' doc 53 | //...other plugins 54 | ]) 55 | 56 | req({url: 'test', log: true}); // enable our plugin, the request info will be logged 57 | req({url: 'test'}); // log options is ommited so our plugin wont be executed 58 | ``` 59 | -------------------------------------------------------------------------------- /docs/how-to/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: index 3 | title: How to 4 | sidebar_label: How to 5 | --- 6 | 7 | ## Write your own plugin 8 | Plugin is just a plain object with specific keys. 9 | 10 | Plugin interface (all entries are optional): 11 | 1. `shouldExecute(context)` - boolean function indicating is plugin should execute at current phase 12 | 1. `init(context, next, makeRequest?)` - this function will be called at `init` phase 13 | 1. `complete(context, next, makeRequest?)` - this function will be called at `complete` phase 14 | 1. `error(context, next, makeRequest?)` - this function will be called at `error` phase 15 | 16 | `context` - the current request execution context, an object with specific methods to change request state (request data, response, error, meta data) 17 | `next` - callback function which should be called after plugin did its job. 18 | `makeRequest` - function to make request (it will be executed with current settings) 19 | -------------------------------------------------------------------------------- /docs/plugins/batch.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: batch 3 | title: Batch Plugin 4 | sidebar_label: Batch 5 | --- 6 | 7 | Batches multiple requests into a single request. Only requests with option `batchKey` are get batched. Requests with equal `batchKey` are grouped together if they were initiated in window of `batchTimeout` ms. 8 | 9 | ## Parameters 10 | 11 | ### Create options 12 | - `timeout`: number [=100] - time to wait in ms. After timeout all requests with equal `batchKey` will be send in a single grouped request 13 | - `makeGroupedRequest`: function - function accepts an array of requests and returns promise which should be resolved with an array of responses 14 | - `shouldExecute`: boolean [=true] - plugin enable flag 15 | 16 | ### Request params 17 | - `batchKey`: string - id to group request by 18 | - `batchTimeout`: number[=timeout] - time to wait in ms. After timeout all requests with equal `batchKey` will be send in a single grouped request 19 | 20 | ### External meta 21 | - `batch.batched`: boolean - flag indicating that current request has been batched 22 | 23 | ## Example 24 | ```typescript 25 | import request from '@tinkoff/request-core'; 26 | import batch from '@tinkoff/request-plugin-batch'; 27 | 28 | const makeGroupedRequest = (requests: any[]) => { 29 | return batchRequest(requests) 30 | .then(() => { 31 | return requests.map(request => 'res' + request.option); 32 | }) 33 | } 34 | 35 | const req = request([ 36 | // ...plugins for caching and memory 37 | // should be set after caching and enhance plugins to prevent requesting batch api for cached requests or with wrong data 38 | batch({ 39 | timeout: 200, // wait 200ms before executin request to allow group serveral requests 40 | shouldExecute: typeof window !== 'undefined', // for browsers only, as grouping is not so efficient on server-side. 41 | makeGroupedRequest, 42 | }), 43 | // should be set just before protocol plugin, to prevent request to common non-batch api 44 | // ...plugins for making actual request 45 | ]); 46 | 47 | req({url: 'test1', batchKey: 'test', option: '1'}) // => resolves with 'res1' 48 | req({url: 'test2', batchKey: 'test', option: '2'}) // => resolves with 'res2' 49 | 50 | setTimeout(() => { 51 | // will be executed in another batched request, because it didnt fit in 200ms timeout 52 | req({url: 'test3', batchKey: 'test', option: '3'}) // => resolves with 'res3' 53 | }, 300) 54 | 55 | // makeGroupedRequest will be called with argument [{url: 'test1', batchKey: 'test', option: '1'}, {url: 'test1', batchKey: 'test', option: '1'}] first time 56 | // and with [{url: 'test3', batchKey: 'test', option: '3'}] second time 57 | // each request will be resolved with according response by index from result of makeGroupedRequest 58 | 59 | ``` 60 | -------------------------------------------------------------------------------- /docs/plugins/cache-deduplicate.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: cache-deduplicate 3 | title: Cache Plugin - Deduplicate 4 | sidebar_label: Cache - Deduplicate 5 | --- 6 | 7 | Deduplicate requests with equal cache keys before making a request. If plugin is executed it will check all currently running requests, all requests with equal cache key will transform into single request, and resolve or reject accordingly to that single request. 8 | 9 | ## Parameters 10 | 11 | ### Create options 12 | - `getCacheKey`: function [=see @tinkoff/request-cache-utils] - function used for generate cache key 13 | - `shouldExecute`: boolean [=true] - plugin enable flag 14 | 15 | ### Request params 16 | - `cache`: boolean [=true] - should any cache plugin be executed. 17 | - `deduplicateCache`: boolean [=true] - should this specific cache plugin be executed 18 | 19 | ### External meta 20 | - `cache.deduplicated`: boolean - flag indicating that current request has been deduplicated 21 | 22 | ## Example 23 | ```typescript 24 | import request from '@tinkoff/request-core'; 25 | import deduplicateCache from '@tinkoff/request-plugin-cache-deduplicate'; 26 | 27 | const req = request([ 28 | // ...plugins for any request transforms 29 | // should be set after transforming plugins and before any other heavy plugins 30 | deduplicateCache(), 31 | // should be set before protocol plugins or other heavy cache plugins 32 | // ...plugins for making actual request 33 | ]); 34 | 35 | req({url: 'test1'}) 36 | req({url: 'test1'}) // this request will be deduplicated in prior of first one 37 | req({url: 'test2'}) // cacheKey for that request by default is differ, so another request will be send 38 | 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/plugins/cache-etag.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: cache-etag 3 | title: Cache Plugin - Etag 4 | sidebar_label: Cache - Etag 5 | --- 6 | 7 | !Executes only on server, for browser this plugin is noop. 8 | 9 | Caches requests response into memory. 10 | Caching is based on etag http-header: for every request which contains 'etag' header response is stored in cache, on 11 | subsequent calls for the same requests it adds 'If-None-Match' header and checks for 304 status of response - if status 12 | is 304 response returns from cache. 13 | 14 | Uses library `@tinkoff/lru-cache-nano` as memory storage. 15 | 16 | ## Parameters 17 | 18 | ### Create options 19 | - `getCacheKey`: function [=see @tinkoff/request-cache-utils] - function used for generate cache key 20 | - `shouldExecute`: boolean [=true] - plugin enable flag 21 | - `memoryConstructor`: function [=require('@tinkoff/lru-cache-nano')] - cache factory 22 | - `lruOptions`: object [={max: 1000}] - options passed to `memoryConstuctor` 23 | 24 | ### Request params 25 | - `cache`: boolean [=true] - should any cache plugin be executed. 26 | - `cacheForce`: boolean [=false] - when enabled all cache plugin will be executed only on complete status (request wont be resolved with cache value in that case and will only store result cache on completed requests) 27 | - `etagCache`: boolean [=true] - should this specific cache plugin be executed 28 | - `etagCacheForce`: boolean [=false] - specific case of `cacheForce` for this plugin only. 29 | 30 | ### External meta 31 | - `cache.etagCache`: boolean - flag indicating that current request has been cached with etag 32 | 33 | ### Internal meta 34 | - `CACHE_ETAG.value`: string - value for etag header of previous request 35 | 36 | ## Example 37 | ```typescript 38 | import request from '@tinkoff/request-core'; 39 | import etagCache from '@tinkoff/request-plugin-cache-etag'; 40 | 41 | const req = request([ 42 | // ...plugins for any request transforms and other cache plugins 43 | // should be set after transforming plugins and after other cache plugins, as this plugin sends real request to api 44 | etagCache(), 45 | // should be set before protocol plugins 46 | // ...plugins for making actual request 47 | ]); 48 | 49 | ``` 50 | -------------------------------------------------------------------------------- /docs/plugins/cache-fallback.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: cache-fallback 3 | title: Cache Plugin - Fallback 4 | sidebar_label: Cache - Fallback 5 | --- 6 | 7 | Fallback cache plugin. This cache used only if request ends with error response and returns previous success response from cache. 8 | Actual place to store cache data depends on passed driver (file system by default). 9 | 10 | ## Parameters 11 | 12 | ### Create options 13 | - `getCacheKey`: function [=see @tinkoff/request-cache-utils] - function used for generate cache key 14 | - `shouldExecute`: boolean [=true] - plugin enable flag 15 | - `shouldFallback`: function [(context) => true] - should fallback value be returned from cache 16 | - `driver`: CacheDriver [fsCacheDriver] - driver used to store fallback data 17 | 18 | ### Request params 19 | - `cache`: boolean [=true] - should any cache plugin be executed. 20 | - `cacheForce`: boolean [=false] - when enabled all cache plugin will be executed only on complete status (request wont be resolved with cache value in that case and will only store result cache on completed requests) 21 | - `fallbackCache`: boolean [=true] - should this specific cache plugin be executed 22 | - `fallbackCacheForce`: boolean [=false] - specific case of `cacheForce` for this plugin only. 23 | 24 | ### External meta 25 | - `cache.fallbackCache`: boolean - flag indicating that current request has been return from fallback cache 26 | 27 | 28 | ## Example 29 | 30 | By default uses fileSystem cache driver: 31 | ```typescript 32 | import request from '@tinkoff/request-core'; 33 | import fallbackCache from '@tinkoff/request-plugin-cache-fallback'; 34 | 35 | const req = request([ 36 | // ...plugins for any request transforms and other cache plugins 37 | // should be set after transforming plugins and after other cache plugins, as this plugin is pretty heavy for execution 38 | fallbackCache(), 39 | // should be set before protocol plugins 40 | // ...plugins for making actual request 41 | ]); 42 | ``` 43 | 44 | To override driver: 45 | ```typescript 46 | import request from '@tinkoff/request-core'; 47 | import fallbackCache from '@tinkoff/request-plugin-cache-fallback'; 48 | import { memoryCacheDriver } from '@tinkoff/request-plugin-cache-fallback/lib/drivers'; 49 | 50 | const req = request([ 51 | // ...plugins for any request transforms and other cache plugins 52 | // should be set after transforming plugins and after other cache plugins, as this plugin is pretty heavy for execution 53 | fallbackCache({ driver: memoryCacheDriver() }), 54 | // should be set before protocol plugins 55 | // ...plugins for making actual request 56 | ]); 57 | ``` 58 | -------------------------------------------------------------------------------- /docs/plugins/cache-memory.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: cache-memory 3 | title: Cache Plugin - Memory 4 | sidebar_label: Cache - Memory 5 | --- 6 | 7 | Caches requests response into memory. 8 | Uses library `@tinkoff/lru-cache-nano` as memory storage. 9 | 10 | ## Parameters 11 | 12 | ### Create options 13 | - `getCacheKey`: function [=see @tinkoff/request-cache-utils] - function used for generate cache key 14 | - `shouldExecute`: boolean [=true] - plugin enable flag 15 | - `memoryConstructor`: function [=require('@tinkoff/lru-cache-nano')] - cache factory 16 | - `lruOptions`: object [={max: 1000, ttl: 300000}] - options passed to `memoryConstuctor` 17 | - `allowStale`: boolean [=false] - is allowed to use outdated value from cache (if true outdated value will be returned and request to update it will be run in background) 18 | - `staleTtl`: number [=lruOptions.ttl] - time in ms while outdated value is preserved in cache while executing background update 19 | 20 | ### Request params 21 | - `cache`: boolean [=true] - should any cache plugin be executed. 22 | - `cacheForce`: boolean [=false] - when enabled all cache plugin will be executed only on complete status (request wont be resolved with cache value in that case and will only store result cache on completed requests) 23 | - `memoryCache`: boolean [=true] - should this specific cache plugin be executed 24 | - `memoryCacheForce`: boolean [=false] - specific case of `cacheForce` for this plugin only. 25 | - `memoryCacheTtl`: number - ttl of cache of the current request 26 | - `memoryCacheAllowStale`: boolean [=allowStale] - flag indicating that is it allowed to return outdated value from cache 27 | 28 | ### External meta 29 | - `cache.memoryCache`: boolean - flag indicating that current request has been return from memory 30 | - `cache.memoryCacheOutdated`: boolean - flag indicating that returned cache value is outdated 31 | - `cache.memoryCacheBackground`: boolean - flag indicating that current request was made in background to update value in cache 32 | 33 | 34 | ## Example 35 | ```typescript 36 | import request from '@tinkoff/request-core'; 37 | import memoryCache from '@tinkoff/request-plugin-cache-memory'; 38 | 39 | const req = request([ 40 | // ...plugins for any request transforms and other cache plugins 41 | // should be set after transforming plugins 42 | memoryCache(), 43 | // should be set before protocol plugins 44 | // ...plugins for making actual request 45 | ]); 46 | 47 | ``` 48 | -------------------------------------------------------------------------------- /docs/plugins/cache-persistent.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: cache-persistent 3 | title: Cache Plugin - Persistent 4 | sidebar_label: Cache - Persistent 5 | --- 6 | 7 | !Executes only in browser, for server this plugin is noop. 8 | 9 | Caches requests result into IndexedDB. 10 | Uses library `idb-keyval` as wrapper to IndexedDB. 11 | 12 | ## Parameters 13 | 14 | ### Create options 15 | - `getCacheKey`: function [=see @tinkoff/request-cache-utils] - function used for generate cache key 16 | - `shouldExecute`: boolean [=true] - plugin enable flag 17 | 18 | ### Request params 19 | - `cache`: boolean [=true] - should any cache plugin be executed. 20 | - `cacheForce`: boolean [=false] - when enabled all cache plugin will be executed only on complete status (request wont be resolved with cache value in that case and will only store result cache on completed requests) 21 | - `persistentCache`: boolean [=true] - should this specific cache plugin be executed 22 | - `persistentCacheForce`: boolean [=false] - specific case of `cacheForce` for this plugin only. 23 | 24 | ### External meta 25 | - `cache.persistentCache`: boolean - flag indicating that current request has been returned from persistent cache 26 | 27 | ## Example 28 | ```typescript 29 | import request from '@tinkoff/request-core'; 30 | import persistentCache from '@tinkoff/request-plugin-cache-persistent'; 31 | 32 | const req = request([ 33 | // ...plugins for any request transforms 34 | // should be set after transforming plugins and before other more lightweighted cache plugins 35 | persistentCache(), 36 | // should be set before protocol plugins or other heavy cache plugins 37 | // ...plugins for making actual request 38 | ]); 39 | ``` 40 | 41 | -------------------------------------------------------------------------------- /docs/plugins/circuit-breaker.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: circuit-breaker 3 | title: Circuit Breaker Plugin 4 | sidebar_label: Circuit Breaker 5 | --- 6 | 7 | Plugin implementing `Circuit Breaker` design pattern. 8 | 9 | Request maker will have 3 states: 10 | - `Closed` - all requests are passing to the next plugin allowing to create requests 11 | - `Open` - no requests are passing to next step, every request throws error from last 'real' request 12 | - `Half-Open` - only limited number of requests are allowed to actually executes, if these requests were successful 13 | state changes to Closed, otherwise goes back to Open 14 | 15 | ## Api 16 | 17 | ### Create options 18 | - `getKey`: function [= () => ''] - allows to divide requests to different instances of Circuit Breaker, by default only one Circuit Breaker instance is created 19 | - `isSystemError`: function [=() => true] specifies that error should be treated as a system error and therefore will lead to an increasing counter of failed requests in Circuit Breaker, if the error is not a system error then error is treated as normal behavior and therefore will lead to a decreasing counter of failed requests 20 | - `failureTimeout`: number [=120000] - time interval in which failed requests will considered to get state 21 | - `failureThreshold`: number [=50] - percentage of failed requests inside `failureTimeout` interval, if that number is exceeded state changes to Open 22 | - `minimumFailureCount`: number [=5] - number of minimum request which should be failed to consider stats from current time interval 23 | - `openTimeout`: number [=60000] - time interval in which all requests will forcedly fail, after that timeout `halfOpenThreshold` number of requests will be executed as usual 24 | - `halfOpenThreshold`: number [=5] - percentage of requests allowed to execute while state is Half-Open 25 | 26 | ### External meta 27 | - `CIRCUIT_BREAKER.open`: boolean - flag indicating that current request was blocked by Circuit Breaker 28 | 29 | ### Internal meta 30 | - `CIRCUIT_BREAKER.breaker` - Circuit Breaker instance 31 | 32 | ## How to 33 | 34 | ### Base example 35 | 36 | ```typescript 37 | import request from '@tinkoff/request-core'; 38 | import circuitBreaker from '@tinkoff/request-plugin-circuit-breaker'; 39 | 40 | const req = request([ 41 | // ...plugins for any request transforms and cache 42 | // should be set after transforming plugins and cache plugins 43 | circuitBreaker(), 44 | // should be set before protocol plugins 45 | // ...plugins for making actual request 46 | ]); 47 | ``` 48 | 49 | ### Validators 50 | 51 | ```ts 52 | import request from '@tinkoff/request-core'; 53 | import http, { isNetworkFail } from '@tinkoff/request-plugin-protocol-http'; 54 | import circuitBreaker from '@tinkoff/request-plugin-circuit-breaker'; 55 | 56 | const req = request([ 57 | // ...plugins for any request transforms and cache 58 | // should be set after transforming plugins and cache plugins 59 | circuitBreaker({ 60 | isSystemError: isNetworkFail, 61 | }), 62 | // should be set before protocol plugins 63 | // ...plugins for making actual request 64 | http(), 65 | ]); 66 | ``` 67 | -------------------------------------------------------------------------------- /docs/plugins/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: index 3 | title: Plugins 4 | sidebar_label: Plugins 5 | --- 6 | ## Plugins 7 | Plugins can inject to the request flow to change its execution or adjust request\response data. For further see [Plugin](../core/plugin) and [Request Execution](../core/execution). 8 | 9 | ## List of plugins 10 | 1. `protocol/http` - base plugin to make request with http\https 11 | 1. `protocol/jsonp` - plugin to make jsonp requests 12 | 1. `cache/deduplicate` - deduplicate identical requests 13 | 1. `cache/memory` - caches responses into memory 14 | 1. `cache/etag` - [!server only] cache based on etag http-header 15 | 1. `cache/fallback` - stores response data and returns it from cache only for errored requests 16 | 1. `cache/persistent` - [!browser only] caching data at IndexedDB to keep cache among page reloads 17 | 1. `log` - logs the data of request execution 18 | 1. `batch` - groups several request into single one 19 | 1. `validate` - validate response or restore on errors 20 | 1. `transform-url` - transforms url string for every request 21 | 1. `circuit-breaker` - [!server only] fault protections 22 | 1. `prom-red-metrics` - red metrics about sent requests for prometheus 23 | 1. `retry` - retry plugin for failed request 24 | -------------------------------------------------------------------------------- /docs/plugins/log.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: log 3 | title: Log Plugin 4 | sidebar_label: Log 5 | --- 6 | 7 | Logs request events and timing 8 | 9 | ## Parameters 10 | 11 | ### Create options 12 | - `name`: string [=''] - string used as logger name, passed to logger 13 | - `logger`: function [() => console] - logger factory 14 | - `showQueryFields`: boolean | string[] [=false] - whether the plugin should show request query values. 15 | - `showPayloadFields`: boolean | string[] [=false] - whether the plugin should show request payload values. 16 | 17 | ### Request params 18 | - `silent`: boolean [=false] - if set info and error level logs will be ignored and only debug level enabled. 19 | - `showQueryFields`: boolean | string[] [=false] - whether the plugin should show request query values. 20 | - `showPayloadFields`: boolean | string[] [=false] - whether the plugin should show request payload values. 21 | 22 | ### External meta 23 | - `log.start`: number - request start Date.now() 24 | - `log.end`: number - request end Date.now() 25 | - `log.duration`: number - request duration (end - start) 26 | 27 | ## Example 28 | ```typescript 29 | import request from '@tinkoff/request-core'; 30 | import log from '@tinkoff/request-plugin-log'; 31 | 32 | const req = request([ 33 | // should be set first at most cases to enable logging for every requests, despite caching or other plugins logic 34 | log(), 35 | // ...other plugins 36 | ]); 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/plugins/prom-red-metrics.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: prom-red-metrics 3 | title: Prom RED metrics 4 | sidebar_label: Prom RED metrics 5 | --- 6 | 7 | Adds RED request metrics for prometheus. 8 | 9 | ## Parameters 10 | 11 | ### Create options 12 | - `metrics`: object [={counter, histogram}] - factories for creating [prom-client](https://www.npmjs.com/package/prom-client) instances; 13 | - `labelNames`: array - list of labels for metrics; 14 | - `getLabelsValuesFromContext`: function - function for extracting label values from context. 15 | - `prefix`: string - prefix for metric names, used to add metrics for more than one plugin instance 16 | 17 | ### Internal meta 18 | - `TIMER_DONE`: function - function created at the time of sending and called at the end of the request to calculate its duration. 19 | 20 | ## Example 21 | 22 | ### Base usage 23 | 24 | ```typescript 25 | import request from '@tinkoff/request-core'; 26 | import promRedMetrics from '@tinkoff/request-plugin-prom-red-metrics'; 27 | import promClient from 'prom-client'; 28 | 29 | const req = request([ 30 | promRedMetrics({ 31 | metrics: { 32 | counter: (options) => new promClient.Counter(options), // here you can mix any of your own parameters 33 | histogram: (options) => new promClient.Histogram(options), 34 | }, 35 | prefix: 'api', 36 | labelNames: ['host', 'port'], 37 | getLabelsValuesFromContext: (context) => { 38 | const { host, port } = context.getRequest() 39 | 40 | return { host, port }; 41 | }, 42 | }), 43 | // ...other plugins 44 | ]); 45 | ``` 46 | 47 | ### Use with @tinkoff/request-plugin-protocol-http 48 | 49 | ```typescript 50 | import request from '@tinkoff/request-core'; 51 | import { httpMetrics } from '@tinkoff/request-plugin-prom-red-metrics'; 52 | import http from '@tinkoff/request-plugin-protocol-http'; 53 | import promClient from 'prom-client'; 54 | 55 | const req = request([ 56 | httpMetrics({ 57 | metrics: { 58 | counter: (options) => new promClient.Counter(options), // here you can mix any of your own parameters 59 | histogram: (options) => new promClient.Histogram(options), 60 | }, 61 | // variables for the http protocol are predefined in the httpMetrics file 62 | }), 63 | http(), 64 | ]); 65 | ``` 66 | -------------------------------------------------------------------------------- /docs/plugins/protocol-jsonp.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: protocol-jsonp 3 | title: Protocol Plugin - Jsonp 4 | sidebar_label: Protocol - Jsonp 5 | --- 6 | 7 | !Executes only in browser, on server this plugin is noop. 8 | 9 | Makes jsonp request. 10 | Uses `fetch-jsonp` library. 11 | 12 | ## Parameters 13 | 14 | ### Create options 15 | Options are passed to [`fetch-jsonp`](https://github.com/camsong/fetch-jsonp) on every request. 16 | 17 | ### Request params 18 | - `url`: string - url to request 19 | - `query`: object - query parameters of the request 20 | - `queryNoCache`: object - same as `query` but value wont be used when generating cache key 21 | - `jsonp`: object - configuration passed for `fetch-jsonp`, this value is merge with created options and get passed for every request 22 | 23 | ## Example 24 | ```typescript 25 | import request from '@tinkoff/request-core'; 26 | import jsonp from '@tinkoff/request-plugin-protocol-jsonp'; 27 | 28 | const req = request([ 29 | // ... other plugins 30 | // should be set last as this plugin makes actual reqest 31 | jsonp(), 32 | ]); 33 | ``` 34 | -------------------------------------------------------------------------------- /docs/plugins/retry.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: retry 3 | title: Retry Plugin 4 | sidebar_label: Retry 5 | --- 6 | 7 | Retries failed requests 8 | 9 | ## Parameters 10 | 11 | ### Create options 12 | - `retry`: number [=0] - number of attempts to execute failed request 13 | - `retryDelay`: number | Function [=100] - time in ms to wait before execute new attempt 14 | - `maxTimeout`: number [=60000] - final timeout for complete request including retry attempts 15 | 16 | ### Request params 17 | - `retry`: number - number of attempts to execute failed request 18 | - `retryDelay`: number | Function - time in ms to wait before execute new attempt 19 | 20 | ### External meta 21 | - `retry.attempts`: number - request start Date.now() 22 | 23 | ## Example 24 | ```typescript 25 | import request from '@tinkoff/request-core'; 26 | import retry from '@tinkoff/request-plugin-retry'; 27 | 28 | const req = request([ 29 | // .. cache plugin and other plugins for transform request 30 | retry({ retry: 3, retryDelay: 500 }), 31 | // ...other plugins to make actual request and validate it 32 | ]); 33 | ``` 34 | -------------------------------------------------------------------------------- /docs/plugins/transform-url.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: transform-url 3 | title: Transform Plugin - Url 4 | sidebar_label: Transform - Url 5 | --- 6 | 7 | Transforms request url using passed function. 8 | 9 | ## Parameters 10 | 11 | ### Create options 12 | - `baseUrl`: string [=''] - option used in transform function 13 | - `transform`: function [({ baseUrl, url }: Request) => `${baseUrl}${url}`] - function to transform url, accepts request options and should return string; 14 | 15 | ### Request params 16 | - `baseUrl`: string [='''] - overwrite `baseUrl` for single request 17 | 18 | 19 | ## Example 20 | ```typescript 21 | import request from '@tinkoff/request-core'; 22 | import transformUrl from '@tinkoff/request-plugin-transform-url'; 23 | 24 | const req = request([ 25 | // should be set first at most cases to transform url as soon as possible 26 | transformUrl({ 27 | baseUrl: '/api/', 28 | transform: ({baseUrl, method, session}) => { 29 | return `${baseUrl}${method}?session=${session}` 30 | } 31 | }), 32 | // ...other plugins 33 | ]); 34 | 35 | req({method: 'test'}) // request will be send to /api/test?session= 36 | req({method: 'test2', baseUrl: '/api2'}) // to /api2/test2?session= 37 | req({method: 'test3', session: '123'}) // to /api/test3?session=123 38 | ``` 39 | 40 | -------------------------------------------------------------------------------- /docs/plugins/validate.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: validate 3 | title: Validate Plugin 4 | sidebar_label: Validate 5 | --- 6 | 7 | Plugin to validate response. 8 | 9 | If `validator` returns falsy value plugin does nothing, otherwise return value used as a error and requests goes to the error phase. 10 | 11 | If `errorValidator` returns truthy for request in error phase then plugin switch phase to complete. 12 | 13 | ## Parameters 14 | 15 | ### Create options 16 | - `validator`: function - function to validate success request, accepts single parameter: the state of current request, should return error if request should be treated as errored request 17 | - `errorValidator`: function - function to validate errored request, accepts single parameter: the state of current request, should return `truthy` value if request should be treated as success request 18 | - `allowFallback`: boolean [= true] - if false adds `fallbackCache`=false option to request to prevent activating fallback cache 19 | 20 | ### External meta 21 | - `validate.validated`: boolean - is completed request has passed validation 22 | - `validate.errorValidated`: boolean - is errored request passed validation and switched to complete status 23 | - `validate.error`: Error - saved error after `errorValidator` success check 24 | 25 | ## Example 26 | ```typescript 27 | import request from '@tinkoff/request-core'; 28 | import validate from '@tinkoff/request-plugin-validate'; 29 | 30 | const req = request([ 31 | // ...plugins for any request transforms and cache 32 | // should be set after transforming plugins and cache plugins 33 | validate({ 34 | validator: ({response}) => { 35 | if (response.resultCode !== 'OK') { 36 | return new Error('Not valid') 37 | } 38 | }, 39 | errorValidator: ({error}) => { 40 | return error.status === 404; 41 | } 42 | }), 43 | // should be set before protocol plugins 44 | // ...plugins for making actual request 45 | ]); 46 | 47 | // if request was ended succesfully and response contains resultCode === 'OK' req will be resolved with response 48 | // if request was ended succesfully and resultCode !== 'OK' req will be reject with Not valid error 49 | // if success failed with status 404 then req will be resolved with response 50 | // otherwise req will be rejected 51 | req({url: 'test'}) 52 | ``` 53 | 54 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | timers: 'legacy', 3 | testURL: 'http://localhost/', 4 | transform: { 5 | '^.+\\.ts$': 'ts-jest', 6 | '^.+\\.js$': 'babel-jest', 7 | }, 8 | globals: { 9 | 'ts-jest': { 10 | tsconfig: 'tsconfig.json', 11 | }, 12 | }, 13 | coveragePathIgnorePatterns: ['/node_modules/', '/lib/'], 14 | testMatch: ['**/*.spec.(ts|js)'], 15 | moduleFileExtensions: ['ts', 'js'], 16 | setupFiles: ['jest-date-mock'], 17 | }; 18 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "useWorkspaces": true, 6 | "version": "independent", 7 | "command": { 8 | "version": { 9 | "allowBranch": "master", 10 | "message": "chore(release): publish packages" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tinkoff/request", 3 | "version": "0.6.0", 4 | "description": "Request library extendable by plugins", 5 | "private": true, 6 | "workspaces": [ 7 | "packages/**/*", 8 | "website" 9 | ], 10 | "scripts": { 11 | "test": "jest --coverage", 12 | "size": "size-limit", 13 | "bootstrap": "yarn --frozen-lockfile", 14 | "build": "lerna run prepack", 15 | "watch": "lerna run watch --parallel", 16 | "release": "lerna publish --no-private --conventional-commits --ignore-scripts --yes --create-release github", 17 | "clean": "git clean -Xdf .", 18 | "docs:build": "yarn --cwd ./website build", 19 | "docs:start": "yarn --cwd ./website start", 20 | "docs:publish": "yarn --cwd ./website publish-gh-pages" 21 | }, 22 | "author": { 23 | "name": "Tinkoff Team", 24 | "email": "frontend@tinkoff.ru" 25 | }, 26 | "keywords": [ 27 | "request" 28 | ], 29 | "license": "ISC", 30 | "devDependencies": { 31 | "@size-limit/preset-small-lib": "^7.0.8", 32 | "@tramvai/build": "^2.6.11", 33 | "@types/jest": "^27.4.1", 34 | "@types/node": "^17.0.21", 35 | "fast-glob": "^3.2.11", 36 | "fs-extra": "^10.0.1", 37 | "jest": "^27.5.1", 38 | "jest-date-mock": "^1.0.8", 39 | "lerna": "^5.6.2", 40 | "prettier": "^2.5.1", 41 | "size-limit": "^7.0.8", 42 | "ts-jest": "^27.1.3", 43 | "typescript": "^4.5.5" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/cache-utils/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | src/ 3 | -------------------------------------------------------------------------------- /packages/cache-utils/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [0.9.3](https://github.com/Tinkoff/tinkoff-request/compare/@tinkoff/request-cache-utils@0.9.2...@tinkoff/request-cache-utils@0.9.3) (2023-08-02) 7 | 8 | **Note:** Version bump only for package @tinkoff/request-cache-utils 9 | 10 | 11 | 12 | 13 | 14 | ## [0.9.2](https://github.com/Tinkoff/tinkoff-request/compare/@tinkoff/request-cache-utils@0.9.2...@tinkoff/request-cache-utils@0.9.2) (2023-07-14) 15 | 16 | 17 | ### Bug Fixes 18 | 19 | * **cache-utils:** enable caching with cache: true for disabled by default requests ([005d953](https://github.com/Tinkoff/tinkoff-request/commit/005d953604daf473565af6f533743c8da3831ef8)) 20 | * extend typings ([1fcc2cb](https://github.com/Tinkoff/tinkoff-request/commit/1fcc2cb32597b10d788de36303507e385042fc96)) 21 | 22 | 23 | ### Features 24 | 25 | * split context.meta into context.internalMeta and context.externalMeta ([31f00e0](https://github.com/Tinkoff/tinkoff-request/commit/31f00e0ae14767f213a67eb2df349c9f75adcfe7)) 26 | * use @tramvai/build as builder to provide modern es version ([3a26224](https://github.com/Tinkoff/tinkoff-request/commit/3a26224221d4fc073938cf32c2f147515620c28e)) 27 | -------------------------------------------------------------------------------- /packages/cache-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tinkoff/request-cache-utils", 3 | "version": "0.9.3", 4 | "description": "Support library for @tinkoff/request", 5 | "main": "lib/index.js", 6 | "typings": "lib/index.d.ts", 7 | "sideEffects": false, 8 | "scripts": { 9 | "prepack": "tramvai-build --for-publish", 10 | "build": "tramvai-build", 11 | "tsc": "tsc", 12 | "watch": "tsc -w", 13 | "test": "echo \"Error: no test specified\" && exit 1" 14 | }, 15 | "keywords": [ 16 | "request" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/Tinkoff/tinkoff-request/" 21 | }, 22 | "author": "Oleg Sorokotyaga (http://tinkoff.ru)", 23 | "publishConfig": { 24 | "access": "public" 25 | }, 26 | "license": "ISC", 27 | "dependencies": { 28 | "@tinkoff/utils": "^2.0.0", 29 | "tslib": "^2.1.3" 30 | }, 31 | "devDependencies": { 32 | "@tinkoff/request-core": "^0.9.3" 33 | }, 34 | "peerDependencies": { 35 | "@tinkoff/request-core": "0.x" 36 | }, 37 | "module": "lib/index.es.js" 38 | } 39 | -------------------------------------------------------------------------------- /packages/cache-utils/src/cacheKey.spec.ts: -------------------------------------------------------------------------------- 1 | import cacheKey from './cacheKey'; 2 | 3 | describe('utils/cacheKey', () => { 4 | it('test', () => { 5 | expect( 6 | cacheKey({ 7 | httpMethod: 'GET', 8 | url: '/test123', 9 | payload: { a: '1', b: '2' }, 10 | query: { q: '1', q2: '3' }, 11 | rawQueryString: 'a=b&b=3', 12 | }) 13 | ).toBe('get/test123{"a":"1","b":"2"}{"q":"1","q2":"3"}a=b&b=3""'); 14 | 15 | expect( 16 | cacheKey({ 17 | httpMethod: 'POST', 18 | url: '/test', 19 | payload: {}, 20 | query: { q: '6' }, 21 | rawQueryString: 'a=b', 22 | }) 23 | ).toBe('post/test{}{"q":"6"}a=b""'); 24 | 25 | expect( 26 | cacheKey({ 27 | httpMethod: 'PUT', 28 | url: '/test1235353636/tetete', 29 | query: {}, 30 | rawQueryString: '', 31 | additionalCacheKey: { a: 5 }, 32 | }) 33 | ).toBe('put/test1235353636/tetete""{}{"a":5}'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /packages/cache-utils/src/cacheKey.ts: -------------------------------------------------------------------------------- 1 | import type { Request } from '@tinkoff/request-core'; 2 | 3 | declare module '@tinkoff/request-core/lib/types.h' { 4 | export interface Request { 5 | rawQueryString?: string; 6 | additionalCacheKey?: any; 7 | } 8 | } 9 | 10 | export default ({ httpMethod = 'GET', url, payload, query, rawQueryString = '', additionalCacheKey = '' }: Request) => 11 | httpMethod.toLowerCase() + 12 | url + 13 | JSON.stringify(payload || '') + 14 | JSON.stringify(query || '') + 15 | rawQueryString + 16 | JSON.stringify(additionalCacheKey); 17 | -------------------------------------------------------------------------------- /packages/cache-utils/src/constants/metaTypes.ts: -------------------------------------------------------------------------------- 1 | export const CACHE = 'cache'; 2 | -------------------------------------------------------------------------------- /packages/cache-utils/src/getCacheKey.spec.ts: -------------------------------------------------------------------------------- 1 | import { Context } from '@tinkoff/request-core'; 2 | import getCacheKey from './getCacheKey'; 3 | import { CACHE } from './constants/metaTypes'; 4 | 5 | const context = { 6 | getInternalMeta: jest.fn(), 7 | getRequest: jest.fn(), 8 | updateInternalMeta: jest.fn(), 9 | }; 10 | 11 | describe('utils/getCacheKey', () => { 12 | beforeEach(() => { 13 | context.getInternalMeta.mockClear(); 14 | context.getRequest.mockClear(); 15 | context.updateInternalMeta.mockClear(); 16 | }); 17 | 18 | it('if already in meta just return it', () => { 19 | context.getInternalMeta.mockImplementation(() => ({ key: 'test' })); 20 | 21 | expect(getCacheKey((context as any) as Context)).toBe('test'); 22 | expect(context.getInternalMeta).toHaveBeenCalledWith(CACHE); 23 | expect(context.getRequest).not.toHaveBeenCalled(); 24 | }); 25 | 26 | it('if not in meta create new and returns', () => { 27 | const request = { a: 1, b: 2 }; 28 | const getKey = jest.fn(() => 'test'); 29 | 30 | context.getInternalMeta.mockImplementation(() => ({})); 31 | context.getRequest.mockImplementation(() => request); 32 | 33 | expect(getCacheKey((context as any) as Context, getKey)).toBe('test'); 34 | expect(getKey).toHaveBeenCalledWith(request); 35 | expect(context.updateInternalMeta).toHaveBeenCalledWith(CACHE, { key: 'test' }); 36 | expect(context.getInternalMeta).toHaveBeenCalledWith(CACHE); 37 | expect(context.getRequest).toHaveBeenCalled(); 38 | }); 39 | 40 | it('if not in meta create new and returns, generated key is too long', () => { 41 | const request = { a: 1, b: 2 }; 42 | const key = 43 | 'aaaaaaaaaaaa aaaaaaaaaaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbbbbb aaaaaaaaaaaaaaaaaaa bbbbbbbbbbbbbbb aaaaaaaaaaaaaaa'; 44 | const getKey = jest.fn(() => key); 45 | 46 | context.getInternalMeta.mockImplementation(() => ({})); 47 | context.getRequest.mockImplementation(() => request); 48 | 49 | expect(getCacheKey((context as any) as Context, getKey)).toBe(key); 50 | expect(getKey).toHaveBeenCalledWith(request); 51 | expect(context.updateInternalMeta).toHaveBeenCalledWith(CACHE, { key: key }); 52 | expect(context.getInternalMeta).toHaveBeenCalledWith(CACHE); 53 | expect(context.getRequest).toHaveBeenCalled(); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /packages/cache-utils/src/getCacheKey.ts: -------------------------------------------------------------------------------- 1 | import prop from '@tinkoff/utils/object/prop'; 2 | import type { Context } from '@tinkoff/request-core'; 3 | import { CACHE } from './constants/metaTypes'; 4 | import defaultCacheKey from './cacheKey'; 5 | 6 | export default (context: Context, cacheKey = defaultCacheKey): string => { 7 | let key = prop('key', context.getInternalMeta(CACHE)); 8 | 9 | if (!key) { 10 | key = cacheKey(context.getRequest()); 11 | 12 | context.updateInternalMeta(CACHE, { 13 | key, 14 | }); 15 | } 16 | 17 | return key; 18 | }; 19 | -------------------------------------------------------------------------------- /packages/cache-utils/src/index.ts: -------------------------------------------------------------------------------- 1 | import shouldCacheExecute from './shouldCacheExecute'; 2 | import cacheKey from './cacheKey'; 3 | import getCacheKey from './getCacheKey'; 4 | import * as metaTypes from './constants/metaTypes'; 5 | 6 | export { shouldCacheExecute, cacheKey, getCacheKey, metaTypes }; 7 | -------------------------------------------------------------------------------- /packages/cache-utils/src/shouldCacheExecute.ts: -------------------------------------------------------------------------------- 1 | import prop from '@tinkoff/utils/object/prop'; 2 | import type { Context } from '@tinkoff/request-core'; 3 | import { Status } from '@tinkoff/request-core'; 4 | import { CACHE } from './constants/metaTypes'; 5 | 6 | declare module '@tinkoff/request-core/lib/types.h' { 7 | export interface Request { 8 | cache?: boolean; 9 | cacheForce?: boolean; 10 | } 11 | } 12 | 13 | export default (name: string, dflt: boolean) => (context: Context) => { 14 | const request = context.getRequest(); 15 | const forced = prop('cacheForce', request) ?? false; 16 | const forcedSpecific = prop(`${name}CacheForce`, request) ?? forced; 17 | const enabled = prop('cache', request) ?? dflt; 18 | const enabledSpecific = prop(`${name}Cache`, request) ?? enabled; 19 | 20 | if (context.getStatus() === Status.INIT) { 21 | context.updateExternalMeta(CACHE, { 22 | forced, 23 | enabled, 24 | [`${name}Enabled`]: enabledSpecific, 25 | [`${name}Force`]: forcedSpecific, 26 | }); 27 | } 28 | 29 | if (forcedSpecific) { 30 | return context.getStatus() === Status.COMPLETE; 31 | } 32 | 33 | return enabledSpecific; 34 | }; 35 | -------------------------------------------------------------------------------- /packages/cache-utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib" 5 | }, 6 | "include": [ 7 | "./src" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /packages/core/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | src/ 3 | -------------------------------------------------------------------------------- /packages/core/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [0.9.3](https://github.com/Tinkoff/tinkoff-request/compare/@tinkoff/request-core@0.9.2...@tinkoff/request-core@0.9.3) (2023-08-02) 7 | 8 | **Note:** Version bump only for package @tinkoff/request-core 9 | 10 | 11 | 12 | 13 | 14 | ## [0.9.2](https://github.com/Tinkoff/tinkoff-request/compare/@tinkoff/request-core@0.9.2...@tinkoff/request-core@0.9.2) (2023-07-14) 15 | 16 | 17 | ### Bug Fixes 18 | 19 | * **core:** remove sensitive data from error object ([#40](https://github.com/Tinkoff/tinkoff-request/issues/40)) ([872dcfe](https://github.com/Tinkoff/tinkoff-request/commit/872dcfe9225c92fcc420c8bcb189ccd78e062a21)) 20 | * extend typings ([1fcc2cb](https://github.com/Tinkoff/tinkoff-request/commit/1fcc2cb32597b10d788de36303507e385042fc96)) 21 | 22 | 23 | ### Features 24 | 25 | * use @tramvai/build as builder to provide modern es version ([3a26224](https://github.com/Tinkoff/tinkoff-request/commit/3a26224221d4fc073938cf32c2f147515620c28e)) 26 | 27 | 28 | 29 | ## 0.8.4 (2020-02-17) 30 | 31 | 32 | ### Bug Fixes 33 | 34 | * **core:** extend error object by request info ([#33](https://github.com/Tinkoff/tinkoff-request/issues/33)) ([7883c73](https://github.com/Tinkoff/tinkoff-request/commit/7883c73d0376ba0bcabe8f7ef78a47e462411c53)) 35 | 36 | 37 | ### Features 38 | 39 | * **core:** add error handling ([b8b875c](https://github.com/Tinkoff/tinkoff-request/commit/b8b875c6684def3d2652a01d5dcfa31f0bcd1298)) 40 | * split context.meta into context.internalMeta and context.externalMeta ([31f00e0](https://github.com/Tinkoff/tinkoff-request/commit/31f00e0ae14767f213a67eb2df349c9f75adcfe7)) 41 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tinkoff/request-core", 3 | "version": "0.9.3", 4 | "description": "Request library extendable by plugins", 5 | "main": "lib/index.js", 6 | "typings": "lib/index.d.ts", 7 | "sideEffects": false, 8 | "scripts": { 9 | "prepack": "tramvai-build --for-publish", 10 | "build": "tramvai-build", 11 | "tsc": "tsc", 12 | "watch": "tsc -w", 13 | "test": "echo \"Error: no test specified\" && exit 1" 14 | }, 15 | "keywords": [ 16 | "request" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/Tinkoff/tinkoff-request/" 21 | }, 22 | "author": "Oleg Sorokotyaga (http://tinkoff.ru)", 23 | "publishConfig": { 24 | "access": "public" 25 | }, 26 | "license": "ISC", 27 | "dependencies": { 28 | "@tinkoff/utils": "^2.0.0", 29 | "tslib": "^2.1.3" 30 | }, 31 | "module": "lib/index.es.js" 32 | } 33 | -------------------------------------------------------------------------------- /packages/core/src/constants/status.ts: -------------------------------------------------------------------------------- 1 | enum Status { 2 | INIT = 'init', 3 | COMPLETE = 'complete', 4 | ERROR = 'error', 5 | } 6 | 7 | export default Status; 8 | -------------------------------------------------------------------------------- /packages/core/src/context/Context.h.ts: -------------------------------------------------------------------------------- 1 | import Status from '../constants/status'; 2 | import { Request, Response, RequestError } from '../types.h'; 3 | 4 | export interface ContextState { 5 | status: Status; 6 | request: Request; 7 | response: Response; 8 | error: RequestError; 9 | } 10 | -------------------------------------------------------------------------------- /packages/core/src/context/Context.spec.ts: -------------------------------------------------------------------------------- 1 | import Context from './Context'; 2 | import Status from '../constants/status'; 3 | 4 | describe('request/Context', () => { 5 | it('sets initial state', () => { 6 | expect( 7 | new Context({ 8 | request: { a: 1, url: '' }, 9 | }).getState() 10 | ).toEqual({ 11 | request: { a: 1, url: '' }, 12 | response: null, 13 | status: Status.INIT, 14 | error: null, 15 | }); 16 | }); 17 | 18 | it('setState updates state', () => { 19 | const context = new Context(); 20 | 21 | expect(context.getState()).toEqual({ 22 | status: Status.INIT, 23 | request: null, 24 | response: null, 25 | error: null, 26 | }); 27 | 28 | context.setState({ status: Status.COMPLETE }); 29 | expect(context.getState()).toEqual({ 30 | status: Status.COMPLETE, 31 | request: null, 32 | response: null, 33 | error: null, 34 | }); 35 | 36 | const error = new Error('pfpf'); 37 | 38 | context.setState({ error, status: Status.ERROR }); 39 | expect(context.getState()).toEqual({ 40 | error, 41 | status: Status.ERROR, 42 | request: null, 43 | response: null, 44 | }); 45 | }); 46 | 47 | it('work with meta', () => { 48 | const context = new Context(); 49 | const name = 'test'; 50 | 51 | expect(context.getInternalMeta(name)).toBeUndefined(); 52 | expect(context.getExternalMeta(name)).toBeUndefined(); 53 | 54 | context.updateInternalMeta(name, { a: 1, b: 2 }); 55 | expect(context.getInternalMeta(name)).toEqual({ a: 1, b: 2 }); 56 | expect(context.getExternalMeta(name)).toBeUndefined(); 57 | 58 | context.updateInternalMeta(name, { b: 3, c: 4 }); 59 | expect(context.getInternalMeta(name)).toEqual({ a: 1, b: 3, c: 4 }); 60 | expect(context.getExternalMeta(name)).toBeUndefined(); 61 | 62 | context.updateExternalMeta(name, { b: 5, c: 6 }); 63 | expect(context.getInternalMeta(name)).toEqual({ a: 1, b: 3, c: 4 }); 64 | expect(context.getExternalMeta(name)).toEqual({ b: 5, c: 6 }); 65 | }); 66 | 67 | it('get status', () => { 68 | const context = new Context({ status: Status.ERROR }); 69 | 70 | expect(context.getStatus()).toBe(Status.ERROR); 71 | }); 72 | 73 | it('get request', () => { 74 | const request = { a: 325, url: '' }; 75 | const context = new Context({ request }); 76 | 77 | expect(context.getRequest()).toBe(request); 78 | }); 79 | 80 | it('get response', () => { 81 | const response = { b: 398 }; 82 | const context = new Context({ response }); 83 | 84 | expect(context.getResponse()).toBe(response); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /packages/core/src/context/Context.ts: -------------------------------------------------------------------------------- 1 | import applyOrReturn from '@tinkoff/utils/function/applyOrReturn'; 2 | import Status from '../constants/status'; 3 | import { ContextState } from './Context.h'; 4 | import { Meta } from '../types.h'; 5 | 6 | export default class Context { 7 | private state: ContextState; 8 | private internalMeta: Meta; 9 | private externalMeta: Meta; 10 | 11 | constructor(initialState?: Partial) { 12 | this.state = { 13 | status: Status.INIT, 14 | request: null, 15 | response: null, 16 | error: null, 17 | ...initialState, 18 | }; 19 | this.internalMeta = {}; 20 | this.externalMeta = {}; 21 | } 22 | 23 | getState() { 24 | return this.state; 25 | } 26 | 27 | setState(stateOrFunc: Partial) { 28 | const newState = applyOrReturn([this.state], stateOrFunc); 29 | 30 | if (newState) { 31 | this.state = { 32 | ...this.state, 33 | ...newState, 34 | }; 35 | } 36 | } 37 | 38 | getInternalMeta(metaName?: string) { 39 | return this.getMeta(metaName, this.internalMeta); 40 | } 41 | 42 | getExternalMeta(metaName?: string) { 43 | return this.getMeta(metaName, this.externalMeta); 44 | } 45 | 46 | updateInternalMeta(metaName: string, value: Record) { 47 | this.internalMeta = this.extendMeta(metaName, value, this.internalMeta); 48 | } 49 | 50 | updateExternalMeta(metaName: string, value: Record) { 51 | this.externalMeta = this.extendMeta(metaName, value, this.externalMeta); 52 | } 53 | 54 | getStatus() { 55 | return this.state.status; 56 | } 57 | 58 | getResponse() { 59 | return this.state.response; 60 | } 61 | 62 | getRequest() { 63 | return this.state.request; 64 | } 65 | 66 | private getMeta(metaName?: string, meta: Meta = this.externalMeta) { 67 | if (metaName) { 68 | return meta[metaName]; 69 | } 70 | 71 | return meta; 72 | } 73 | 74 | private extendMeta(metaName: string, value: Record, meta: Meta = this.externalMeta) { 75 | if (!value) { 76 | return; 77 | } 78 | 79 | return { 80 | ...meta, 81 | [metaName]: { 82 | ...meta[metaName], 83 | ...value, 84 | }, 85 | }; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default } from './request/request'; 2 | export { default as Context } from './context/Context'; 3 | export * from './types.h'; 4 | export { default as Status } from './constants/status'; 5 | -------------------------------------------------------------------------------- /packages/core/src/request/request.h.ts: -------------------------------------------------------------------------------- 1 | import { Plugin, MakeRequest } from '../types.h'; 2 | 3 | export default interface TinkoffRequest { 4 | (plugins: Plugin[]): MakeRequest; 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/src/request/request.ts: -------------------------------------------------------------------------------- 1 | import applyOrReturn from '@tinkoff/utils/function/applyOrReturn'; 2 | import once from '@tinkoff/utils/function/once'; 3 | import propOr from '@tinkoff/utils/object/propOr'; 4 | 5 | import Status from '../constants/status'; 6 | import Context from '../context/Context'; 7 | import RequestMaker from './request.h'; 8 | import { Next, Request } from '../types.h'; 9 | 10 | const DEFAULT_STATUS_TRANSITION = { 11 | [Status.INIT]: Status.COMPLETE, 12 | }; 13 | 14 | const FORWARD = 1; 15 | const BACKWARD = -1; 16 | 17 | const requestMaker: RequestMaker = function (plugins) { 18 | const makeRequest = (request: Request) => { 19 | let i = -1; 20 | const len = plugins.length; 21 | 22 | const context = new Context(); 23 | 24 | context.setState({ request }); 25 | 26 | const promise = new Promise((resolve, reject) => { 27 | const cb = (statusChanged: boolean) => { 28 | const state = context.getState(); 29 | const currentStatus = state.status; 30 | const nextDefaultStatus = DEFAULT_STATUS_TRANSITION[currentStatus]; 31 | 32 | if (statusChanged) { 33 | return traversePlugins(currentStatus, BACKWARD); 34 | } 35 | 36 | if (nextDefaultStatus) { 37 | context.setState({ 38 | status: nextDefaultStatus, 39 | }); 40 | return traversePlugins(nextDefaultStatus, BACKWARD); 41 | } 42 | 43 | if (currentStatus === Status.COMPLETE) { 44 | resolve(state.response); 45 | } else { 46 | reject( 47 | Object.assign(state.error || {}, { 48 | url: state.request.url, 49 | }) 50 | ); 51 | } 52 | }; 53 | 54 | const traversePlugins = (event: Status, direction: number) => { 55 | const initialStatus = context.getState().status; 56 | 57 | const next: Next = (newState) => { 58 | context.setState(newState); 59 | const state = context.getState(); 60 | 61 | if (state.status !== initialStatus) { 62 | return cb(true); 63 | } 64 | 65 | i += direction; 66 | if (i < 0 || i >= len) { 67 | return cb(false); 68 | } 69 | 70 | const plugin = plugins[i]; 71 | const pluginAction = plugin[event]; 72 | 73 | if (!pluginAction || !applyOrReturn([context], propOr('shouldExecute', true, plugin))) { 74 | return next(); 75 | } 76 | 77 | try { 78 | pluginAction(context, once(next), makeRequest); 79 | } catch (err) { 80 | return next({ status: Status.ERROR, error: err }); 81 | } 82 | }; 83 | 84 | next(); // with no state 85 | }; 86 | 87 | traversePlugins(Status.INIT, FORWARD); 88 | }); 89 | 90 | return Object.assign(promise, { 91 | getState: context.getState.bind(context), 92 | getInternalMeta: context.getInternalMeta.bind(context), 93 | getExternalMeta: context.getExternalMeta.bind(context), 94 | }); 95 | }; 96 | 97 | return makeRequest; 98 | }; 99 | 100 | export default requestMaker; 101 | -------------------------------------------------------------------------------- /packages/core/src/types.h.ts: -------------------------------------------------------------------------------- 1 | import { ContextState } from './context/Context.h'; 2 | import Context from './context/Context'; 3 | 4 | export type Meta = Record; 5 | 6 | export interface RequestErrorCode {} 7 | 8 | export interface RequestError extends Error { 9 | code?: keyof RequestErrorCode; 10 | message: string; 11 | [key: string]: any; 12 | } 13 | 14 | export interface Request { 15 | [key: string]: any; 16 | } 17 | 18 | export interface Response {} 19 | 20 | export interface Next { 21 | (newState?: Partial): void; 22 | } 23 | 24 | export interface MakeRequestResult extends Promise { 25 | getState: () => ContextState; 26 | getInternalMeta: Context['getInternalMeta']; 27 | getExternalMeta: Context['getExternalMeta']; 28 | } 29 | 30 | export interface MakeRequest { 31 | (request: Request): MakeRequestResult; 32 | } 33 | 34 | export interface Handler { 35 | (context: Context, next: Next, makeRequest: MakeRequest): void; 36 | } 37 | 38 | export interface Plugin { 39 | init?: Handler; 40 | complete?: Handler; 41 | error?: Handler; 42 | shouldExecute?: (context: Context) => boolean; 43 | } 44 | 45 | export { ContextState }; 46 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib" 5 | }, 6 | "include": [ 7 | "./src" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /packages/plugin-batch/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | src/ 3 | -------------------------------------------------------------------------------- /packages/plugin-batch/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [0.9.3](https://github.com/Tinkoff/tinkoff-request/compare/@tinkoff/request-plugin-batch@0.9.2...@tinkoff/request-plugin-batch@0.9.3) (2023-08-02) 7 | 8 | **Note:** Version bump only for package @tinkoff/request-plugin-batch 9 | 10 | 11 | 12 | 13 | 14 | ## [0.9.2](https://github.com/Tinkoff/tinkoff-request/compare/@tinkoff/request-plugin-batch@0.9.2...@tinkoff/request-plugin-batch@0.9.2) (2023-07-14) 15 | 16 | 17 | ### Bug Fixes 18 | 19 | * extend typings ([1fcc2cb](https://github.com/Tinkoff/tinkoff-request/commit/1fcc2cb32597b10d788de36303507e385042fc96)) 20 | 21 | 22 | ### Features 23 | 24 | * split context.meta into context.internalMeta and context.externalMeta ([31f00e0](https://github.com/Tinkoff/tinkoff-request/commit/31f00e0ae14767f213a67eb2df349c9f75adcfe7)) 25 | * use @tramvai/build as builder to provide modern es version ([3a26224](https://github.com/Tinkoff/tinkoff-request/commit/3a26224221d4fc073938cf32c2f147515620c28e)) 26 | -------------------------------------------------------------------------------- /packages/plugin-batch/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tinkoff/request-plugin-batch", 3 | "version": "0.9.3", 4 | "description": "Group requests at one batch for optimization of network usage", 5 | "main": "lib/batch.js", 6 | "typings": "lib/batch.d.ts", 7 | "sideEffects": false, 8 | "scripts": { 9 | "prepack": "tramvai-build --for-publish", 10 | "build": "tramvai-build", 11 | "tsc": "tsc", 12 | "watch": "tsc -w", 13 | "test": "echo \"Error: no test specified\" && exit 1" 14 | }, 15 | "keywords": [ 16 | "request" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/Tinkoff/tinkoff-request/" 21 | }, 22 | "author": "Oleg Sorokotyaga (http://tinkoff.ru)", 23 | "publishConfig": { 24 | "access": "public" 25 | }, 26 | "license": "ISC", 27 | "dependencies": { 28 | "@tinkoff/utils": "^2.0.0", 29 | "tslib": "^2.1.3" 30 | }, 31 | "devDependencies": { 32 | "@tinkoff/request-core": "^0.9.3" 33 | }, 34 | "peerDependencies": { 35 | "@tinkoff/request-core": "0.x" 36 | }, 37 | "module": "lib/batch.es.js" 38 | } 39 | -------------------------------------------------------------------------------- /packages/plugin-batch/src/batch.spec.ts: -------------------------------------------------------------------------------- 1 | import applyOrReturn from '@tinkoff/utils/function/applyOrReturn'; 2 | import { Context, Status } from '@tinkoff/request-core'; 3 | import batch from './batch'; 4 | import { BATCH } from './constants/metaTypes'; 5 | 6 | const requests = [ 7 | { url: 'first', batchKey: 'test', batchTimeout: 25 }, 8 | { url: 'second', batchKey: 'test' }, 9 | { url: 'third', batchKey: 'test1231452135' }, 10 | { url: 'forth' }, 11 | ]; 12 | const makeGroupedRequest = jest.fn((r) => Promise.resolve(r.map((x) => x.url))); 13 | 14 | describe('plugins/batch', () => { 15 | let context; 16 | 17 | beforeEach(() => { 18 | context = new Context(); 19 | context.updateExternalMeta = jest.fn(context.updateExternalMeta.bind(context)); 20 | }); 21 | 22 | it('test shouldExecute', () => { 23 | let shouldExecute = batch({ makeGroupedRequest }).shouldExecute; 24 | 25 | expect(applyOrReturn([context], shouldExecute)).toBeFalsy(); 26 | shouldExecute = batch({ makeGroupedRequest }).shouldExecute; 27 | expect(applyOrReturn([context], shouldExecute)).toBeFalsy(); 28 | 29 | context.setState({ 30 | request: { 31 | batchKey: '123', 32 | }, 33 | }); 34 | 35 | expect(applyOrReturn([context], shouldExecute)).toBeTruthy(); 36 | }); 37 | 38 | it('test init with batch requests', async () => { 39 | const init = batch({ makeGroupedRequest, timeout: 123 }).init; 40 | const next = jest.fn(); 41 | 42 | context.setState({ request: requests[0] }); 43 | init(context, next, null); 44 | expect(context.updateExternalMeta).toHaveBeenLastCalledWith(BATCH, { batched: true }); 45 | expect(next).not.toHaveBeenCalled(); 46 | expect(makeGroupedRequest).not.toHaveBeenCalled(); 47 | 48 | context.setState({ request: requests[1] }); 49 | init(context, next, null); 50 | context.setState({ request: requests[2] }); 51 | init(context, next, null); 52 | expect(next).not.toHaveBeenCalled(); 53 | context.setState({ request: requests[3] }); 54 | init(context, next, null); 55 | expect(makeGroupedRequest).not.toHaveBeenCalled(); 56 | 57 | jest.runAllTimers(); 58 | await Promise.resolve(); 59 | expect(makeGroupedRequest).toHaveBeenCalledWith([requests[0], requests[1]]); 60 | expect(makeGroupedRequest).toHaveBeenCalledWith([requests[2]]); 61 | 62 | expect(next).toHaveBeenCalledTimes(4); 63 | expect(next).toHaveBeenCalledWith({ status: Status.COMPLETE, response: 'first' }); 64 | expect(next).toHaveBeenCalledWith({ status: Status.COMPLETE, response: 'second' }); 65 | expect(next).toHaveBeenCalledWith({ status: Status.COMPLETE, response: 'third' }); 66 | }); 67 | 68 | it('test init with batch requests, but grouped failed', async () => { 69 | const error = new Error('test1213'); 70 | const makeGroupedRequest = () => Promise.reject(error); 71 | const init = batch({ makeGroupedRequest, timeout: 123 }).init; 72 | const next = jest.fn(); 73 | 74 | context.setState({ request: requests[0] }); 75 | init(context, next, null); 76 | context.setState({ request: requests[1] }); 77 | init(context, next, null); 78 | context.setState({ request: requests[2] }); 79 | init(context, next, null); 80 | 81 | jest.runAllTimers(); 82 | await Promise.resolve().catch(() => null); 83 | expect(next).toHaveBeenCalledTimes(3); 84 | expect(next).toHaveBeenLastCalledWith({ 85 | error, 86 | status: Status.ERROR, 87 | }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /packages/plugin-batch/src/batch.ts: -------------------------------------------------------------------------------- 1 | import each from '@tinkoff/utils/array/each'; 2 | import prop from '@tinkoff/utils/object/prop'; 3 | import propOr from '@tinkoff/utils/object/propOr'; 4 | import type { Plugin, Request, Next } from '@tinkoff/request-core'; 5 | import { Status } from '@tinkoff/request-core'; 6 | import { BATCH } from './constants/metaTypes'; 7 | 8 | const DEFAULT_BATCH_TIMEOUT = 100; 9 | 10 | interface BatchRequest { 11 | requests: Request[]; 12 | nexts: Next[]; 13 | } 14 | 15 | declare module '@tinkoff/request-core/lib/types.h' { 16 | export interface Request { 17 | batchKey?: string; 18 | batchTimeout?: number; 19 | } 20 | } 21 | 22 | /** 23 | * Batch multiple requests into a single request 24 | * 25 | * requestParams: 26 | * batchKey {string} 27 | * batchTimeout {number} 28 | * 29 | * metaInfo: 30 | * batched {boolean} - shows that current request was batched 31 | * 32 | * @param {number} [timeout = 100] - time after which plugin will initiate a grouped request 33 | * @param {function} makeGroupedRequest - function accepts an array of requests and returns promise 34 | * which should be resolved with an array of responses 35 | * @param {boolean} shouldExecute - enable plugin 36 | * @return {{shouldExecute: function(*): *, init: init}} 37 | */ 38 | export default ({ timeout = DEFAULT_BATCH_TIMEOUT, shouldExecute = true, makeGroupedRequest }): Plugin => { 39 | const batchRequests: Record = {}; 40 | 41 | return { 42 | shouldExecute: (context) => { 43 | return makeGroupedRequest && shouldExecute && prop('batchKey', context.getRequest()); 44 | }, 45 | init: (context, next) => { 46 | const request = context.getRequest(); 47 | const batchKey: string = prop('batchKey', request); 48 | const batchTimeout = propOr('batchTimeout', timeout, request); 49 | 50 | context.updateExternalMeta(BATCH, { 51 | batched: true, 52 | }); 53 | 54 | const running = batchRequests[batchKey]; 55 | 56 | if (running) { 57 | running.requests.push(request); 58 | running.nexts.push(next); 59 | return; 60 | } 61 | 62 | batchRequests[batchKey] = { requests: [request], nexts: [next] }; 63 | 64 | setTimeout(() => { 65 | const { requests, nexts } = batchRequests[batchKey]; 66 | 67 | delete batchRequests[batchKey]; 68 | 69 | makeGroupedRequest(requests) 70 | .then( 71 | each((response, i) => { 72 | nexts[i]({ 73 | response, 74 | status: Status.COMPLETE, 75 | }); 76 | }) 77 | ) 78 | .catch((error) => { 79 | const state = { error, status: Status.ERROR }; 80 | 81 | each((nxt) => nxt(state), nexts); 82 | }); 83 | }, batchTimeout); 84 | }, 85 | }; 86 | }; 87 | -------------------------------------------------------------------------------- /packages/plugin-batch/src/constants/metaTypes.ts: -------------------------------------------------------------------------------- 1 | export const BATCH = 'batch'; 2 | -------------------------------------------------------------------------------- /packages/plugin-batch/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib" 5 | }, 6 | "include": [ 7 | "./src" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /packages/plugin-cache-deduplicate/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | src/ 3 | -------------------------------------------------------------------------------- /packages/plugin-cache-deduplicate/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [0.9.3](https://github.com/Tinkoff/tinkoff-request/compare/@tinkoff/request-plugin-cache-deduplicate@0.9.2...@tinkoff/request-plugin-cache-deduplicate@0.9.3) (2023-08-02) 7 | 8 | **Note:** Version bump only for package @tinkoff/request-plugin-cache-deduplicate 9 | 10 | 11 | 12 | 13 | 14 | ## [0.9.2](https://github.com/Tinkoff/tinkoff-request/compare/@tinkoff/request-plugin-cache-deduplicate@0.9.2...@tinkoff/request-plugin-cache-deduplicate@0.9.2) (2023-07-14) 15 | 16 | 17 | ### Bug Fixes 18 | 19 | * extend typings ([1fcc2cb](https://github.com/Tinkoff/tinkoff-request/commit/1fcc2cb32597b10d788de36303507e385042fc96)) 20 | 21 | 22 | ### Features 23 | 24 | * split context.meta into context.internalMeta and context.externalMeta ([31f00e0](https://github.com/Tinkoff/tinkoff-request/commit/31f00e0ae14767f213a67eb2df349c9f75adcfe7)) 25 | * use @tramvai/build as builder to provide modern es version ([3a26224](https://github.com/Tinkoff/tinkoff-request/commit/3a26224221d4fc073938cf32c2f147515620c28e)) 26 | -------------------------------------------------------------------------------- /packages/plugin-cache-deduplicate/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tinkoff/request-plugin-cache-deduplicate", 3 | "version": "0.9.3", 4 | "description": "Deduplicate identical request into single one", 5 | "main": "lib/deduplicate.js", 6 | "typings": "lib/deduplicate.d.ts", 7 | "sideEffects": false, 8 | "scripts": { 9 | "prepack": "tramvai-build --for-publish", 10 | "build": "tramvai-build", 11 | "tsc": "tsc", 12 | "watch": "tsc -w", 13 | "test": "echo \"Error: no test specified\" && exit 1" 14 | }, 15 | "keywords": [ 16 | "request" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/Tinkoff/tinkoff-request/" 21 | }, 22 | "author": "Oleg Sorokotyaga (http://tinkoff.ru)", 23 | "publishConfig": { 24 | "access": "public" 25 | }, 26 | "license": "ISC", 27 | "dependencies": { 28 | "@tinkoff/request-cache-utils": "^0.9.3", 29 | "@tinkoff/utils": "^2.0.0", 30 | "tslib": "^2.1.3" 31 | }, 32 | "devDependencies": { 33 | "@tinkoff/request-core": "^0.9.3" 34 | }, 35 | "peerDependencies": { 36 | "@tinkoff/request-core": "0.x" 37 | }, 38 | "module": "lib/deduplicate.es.js" 39 | } 40 | -------------------------------------------------------------------------------- /packages/plugin-cache-deduplicate/src/deduplicate.spec.ts: -------------------------------------------------------------------------------- 1 | import applyOrReturn from '@tinkoff/utils/function/applyOrReturn'; 2 | import { Context, Status } from '@tinkoff/request-core'; 3 | import { metaTypes } from '@tinkoff/request-cache-utils'; 4 | import deduplicate from './deduplicate'; 5 | 6 | const requests = [ 7 | { url: 'first', r: 1 }, 8 | { url: 'first', r: 2 }, 9 | { url: 'third', r: 3 }, 10 | { url: 'first', r: 4 }, 11 | ]; 12 | 13 | describe('plugins/cache/deduplicate', () => { 14 | const deduplicateFunc = jest.fn((req) => req.url); 15 | 16 | it('test plugin shouldExecute', () => { 17 | const context = new Context(); 18 | 19 | expect(applyOrReturn([context], deduplicate({ shouldExecute: false }).shouldExecute)).toBeFalsy(); 20 | expect(applyOrReturn([context], deduplicate({ shouldExecute: true }).shouldExecute)).toBeTruthy(); 21 | context.setState({ request: { deduplicateCache: false, url: '' } }); 22 | expect(applyOrReturn([context], deduplicate({ shouldExecute: true }).shouldExecute)).toBeFalsy(); 23 | context.setState({ request: { deduplicateCache: true, url: '' } }); 24 | expect(applyOrReturn([context], deduplicate({ shouldExecute: true }).shouldExecute)).toBeTruthy(); 25 | }); 26 | 27 | it('test plugin after success request', () => { 28 | const plugin = deduplicate({ shouldExecute: true, getCacheKey: deduplicateFunc }); 29 | const next = jest.fn(); 30 | const response = { test: 123 }; 31 | 32 | requests.forEach((request, index) => { 33 | const context = new Context({ request }); 34 | 35 | context.updateExternalMeta = jest.fn(context.updateExternalMeta.bind(context)); 36 | context.updateInternalMeta = jest.fn(context.updateInternalMeta.bind(context)); 37 | 38 | plugin.init(context, next, null); 39 | expect(context.updateInternalMeta).toHaveBeenCalledWith(metaTypes.CACHE, { key: request.url }); 40 | if (request.url === 'first' && index !== 0) { 41 | expect(context.updateExternalMeta).toHaveBeenCalledWith(metaTypes.CACHE, { deduplicated: true }); 42 | } 43 | }); 44 | expect(next).toHaveBeenCalledTimes(2); 45 | plugin.complete(new Context({ response, request: requests[0], status: Status.COMPLETE }), next, null); 46 | expect(next).toHaveBeenCalledTimes(5); // 4 requests init + 1 complete 47 | requests.forEach((request) => { 48 | expect(next).toHaveBeenCalledWith({ response, status: Status.COMPLETE, error: null }); 49 | }); 50 | }); 51 | 52 | it('test plugin after error request', () => { 53 | const plugin = deduplicate({ shouldExecute: true, getCacheKey: deduplicateFunc }); 54 | const next = jest.fn(); 55 | const error = new Error('text'); 56 | 57 | requests.forEach((request, index) => { 58 | const context = new Context({ request }); 59 | 60 | context.updateExternalMeta = jest.fn(context.updateExternalMeta.bind(context)); 61 | context.updateInternalMeta = jest.fn(context.updateInternalMeta.bind(context)); 62 | 63 | plugin.init(context, next, null); 64 | expect(context.updateInternalMeta).toHaveBeenCalledWith(metaTypes.CACHE, { key: request.url }); 65 | if (request.url === 'first' && index !== 0) { 66 | expect(context.updateExternalMeta).toHaveBeenCalledWith(metaTypes.CACHE, { deduplicated: true }); 67 | } 68 | }); 69 | expect(next).toHaveBeenCalledTimes(2); 70 | plugin.error(new Context({ error, request: requests[0], status: Status.ERROR }), next, null); 71 | expect(next).toHaveBeenCalledTimes(5); // 4 requests init + 1 complete 72 | requests.forEach((request) => { 73 | expect(next).toHaveBeenCalledWith({ error, status: Status.ERROR, response: null }); 74 | }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /packages/plugin-cache-deduplicate/src/deduplicate.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from '@tinkoff/request-core'; 2 | import { getCacheKey as getCacheKeyUtil, shouldCacheExecute, metaTypes } from '@tinkoff/request-cache-utils'; 3 | 4 | declare module '@tinkoff/request-core/lib/types.h' { 5 | export interface Request { 6 | deduplicateCache?: boolean; 7 | deduplicateCacheForce?: boolean; 8 | } 9 | } 10 | 11 | /** 12 | * Deduplicate requests with equal cache keys before making a request 13 | * 14 | * requestParams: 15 | * deduplicateCache {boolean} - disable this plugin at all 16 | * deduplicateCacheForce {boolean} - plugin will only be executed on complete phase 17 | * 18 | * metaInfo: 19 | * deduplicated {boolean} - is current request was deduplicated (is not set for the first request of equals requests) 20 | * 21 | * @param {boolean} [shouldExecute = true] is plugin activated by default 22 | * @param {function} getCacheKey function used for generate cache key 23 | */ 24 | export default ({ shouldExecute = true, getCacheKey = undefined } = {}): Plugin => { 25 | const activeRequests = {}; 26 | 27 | const traverseActiveRequests = (context) => { 28 | const state = context.getState(); 29 | const deduplicationKey = getCacheKeyUtil(context, getCacheKey); 30 | 31 | if (deduplicationKey && activeRequests[deduplicationKey]) { 32 | const arr = activeRequests[deduplicationKey]; 33 | 34 | delete activeRequests[deduplicationKey]; 35 | 36 | arr.forEach((next) => { 37 | next({ 38 | status: state.status, 39 | response: state.response, 40 | error: state.error, 41 | }); 42 | }); 43 | } 44 | }; 45 | 46 | return { 47 | shouldExecute: shouldCacheExecute('deduplicate', shouldExecute), 48 | init: (context, next) => { 49 | const deduplicationKey = getCacheKeyUtil(context, getCacheKey); 50 | 51 | if (activeRequests[deduplicationKey]) { 52 | context.updateExternalMeta(metaTypes.CACHE, { 53 | deduplicated: true, 54 | }); 55 | activeRequests[deduplicationKey].push(next); 56 | return; 57 | } 58 | 59 | activeRequests[deduplicationKey] = []; 60 | next(); 61 | }, 62 | complete: (context, next) => { 63 | traverseActiveRequests(context); 64 | next(); 65 | }, 66 | error: (context, next) => { 67 | traverseActiveRequests(context); 68 | next(); 69 | }, 70 | }; 71 | }; 72 | -------------------------------------------------------------------------------- /packages/plugin-cache-deduplicate/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib" 5 | }, 6 | "include": [ 7 | "./src" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /packages/plugin-cache-etag/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | src/ 3 | -------------------------------------------------------------------------------- /packages/plugin-cache-etag/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [0.3.9](https://github.com/Tinkoff/tinkoff-request/compare/@tinkoff/request-plugin-cache-etag@0.3.8...@tinkoff/request-plugin-cache-etag@0.3.9) (2023-08-02) 7 | 8 | **Note:** Version bump only for package @tinkoff/request-plugin-cache-etag 9 | 10 | 11 | 12 | 13 | 14 | ## [0.3.8](https://github.com/Tinkoff/tinkoff-request/compare/@tinkoff/request-plugin-cache-etag@0.3.8...@tinkoff/request-plugin-cache-etag@0.3.8) (2023-07-14) 15 | 16 | 17 | ### Bug Fixes 18 | 19 | * **cache-etag:** fix @tinkoff/request-plugin-protocol-http version ([036f506](https://github.com/Tinkoff/tinkoff-request/commit/036f506d9c5948aa89091a9034e9ba1575957187)) 20 | * extend typings ([1fcc2cb](https://github.com/Tinkoff/tinkoff-request/commit/1fcc2cb32597b10d788de36303507e385042fc96)) 21 | 22 | 23 | ### Features 24 | 25 | * **plugin-cache-etag:** add new plugin for caching basing on etag http-header ([#11](https://github.com/Tinkoff/tinkoff-request/issues/11)) ([3172d56](https://github.com/Tinkoff/tinkoff-request/commit/3172d56f9d36c8999d8984a004e8567d7d02cf6c)) 26 | * **protocol-http:** replace superagent with fetch (or node-fetch for node) ([353dabb](https://github.com/Tinkoff/tinkoff-request/commit/353dabbffebe18060f62ff2527353137e4b63a8f)) 27 | * split context.meta into context.internalMeta and context.externalMeta ([31f00e0](https://github.com/Tinkoff/tinkoff-request/commit/31f00e0ae14767f213a67eb2df349c9f75adcfe7)) 28 | * update lru-cache ([dc65ec9](https://github.com/Tinkoff/tinkoff-request/commit/dc65ec92fb185b0100d5a87f4aecadc39f2a9cd5)) 29 | * use @tramvai/build as builder to provide modern es version ([3a26224](https://github.com/Tinkoff/tinkoff-request/commit/3a26224221d4fc073938cf32c2f147515620c28e)) 30 | -------------------------------------------------------------------------------- /packages/plugin-cache-etag/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tinkoff/request-plugin-cache-etag", 3 | "version": "0.3.9", 4 | "description": "Caching based on etag http-header", 5 | "main": "lib/etag.js", 6 | "browser": "lib/noop.js", 7 | "typings": "lib/etag.d.ts", 8 | "sideEffects": false, 9 | "scripts": { 10 | "prepack": "tramvai-build --for-publish", 11 | "build": "tramvai-build", 12 | "tsc": "tsc", 13 | "watch": "tsc -w", 14 | "test": "echo \"Error: no test specified\" && exit 1" 15 | }, 16 | "keywords": [ 17 | "request" 18 | ], 19 | "author": "Kirill Meleshko (http://tinkoff.ru)", 20 | "publishConfig": { 21 | "access": "public" 22 | }, 23 | "license": "ISC", 24 | "dependencies": { 25 | "@tinkoff/lru-cache-nano": "^7.8.1", 26 | "@tinkoff/request-cache-utils": "^0.9.3", 27 | "@tinkoff/utils": "^2.0.0", 28 | "tslib": "^2.1.3" 29 | }, 30 | "devDependencies": { 31 | "@tinkoff/request-core": "^0.9.3", 32 | "@tinkoff/request-plugin-protocol-http": "^0.11.9", 33 | "@types/node": "^10.11.7" 34 | }, 35 | "peerDependencies": { 36 | "@tinkoff/request-core": "0.x", 37 | "@tinkoff/request-plugin-protocol-http": "^0.7.0" 38 | }, 39 | "module": "lib/etag.es.js" 40 | } 41 | -------------------------------------------------------------------------------- /packages/plugin-cache-etag/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const ETAG = 'CACHE_ETAG'; 2 | -------------------------------------------------------------------------------- /packages/plugin-cache-etag/src/etag.ts: -------------------------------------------------------------------------------- 1 | import type LRUCache from '@tinkoff/lru-cache-nano'; 2 | import type { Options } from '@tinkoff/lru-cache-nano'; 3 | import type { Plugin, Response } from '@tinkoff/request-core'; 4 | import { Status } from '@tinkoff/request-core'; 5 | import { shouldCacheExecute, getCacheKey as getCacheKeyUtil, metaTypes } from '@tinkoff/request-cache-utils'; 6 | import { getHeader } from '@tinkoff/request-plugin-protocol-http'; 7 | 8 | import { ETAG } from './constants'; 9 | 10 | declare module '@tinkoff/request-core/lib/types.h' { 11 | export interface Request { 12 | etagCache?: boolean; 13 | etagCacheForce?: boolean; 14 | } 15 | } 16 | 17 | interface CacheValue { 18 | key: string; 19 | value: Response; 20 | } 21 | 22 | export interface EtagPluginOptions { 23 | lruOptions?: Options; 24 | shouldExecute?: boolean; 25 | memoryConstructor?: (options: Options) => LRUCache; 26 | getCacheKey?: (arg) => string; 27 | } 28 | 29 | /** 30 | * Caches requests response into memory. 31 | * Caching is based on etag http-header: for every request which contains 'etag' header response is stored in cache, on 32 | * subsequent calls for the same requests it adds 'If-None-Match' header and checks for 304 status of response - if status 33 | * is 304 response returns from cache. 34 | * 35 | * Uses library `@tinkoff/lru-cache-nano` as memory storage. 36 | * 37 | * requestParams: 38 | * etagCache {boolean} - disable this plugin at all 39 | * etagCacheForce {boolean} - plugin will only be executed on complete phase 40 | * 41 | * metaInfo: 42 | * etagCache {boolean} - is current request was returned from this cache 43 | * 44 | * @param {object} [lruOptions = {max: 1000}] - options passed to @tinkoff/lru-cache-nano library 45 | * @param {boolean} [shouldExecute = true] is plugin activated by default 46 | * @param {function} memoryConstructor cache factory 47 | * @param {function} getCacheKey function used for generate cache key 48 | */ 49 | export default ({ 50 | lruOptions = { max: 1000 }, 51 | shouldExecute = true, 52 | memoryConstructor = (options) => new (require('@tinkoff/lru-cache-nano'))(options), 53 | getCacheKey = undefined, 54 | }: EtagPluginOptions = {}): Plugin => { 55 | const lruCache = memoryConstructor({ 56 | ...lruOptions, 57 | allowStale: true, // should be true for the opportunity to control it for individual requests 58 | }); 59 | 60 | return { 61 | shouldExecute: shouldCacheExecute('etag', shouldExecute), 62 | 63 | init: (context, next) => { 64 | const cacheKey = getCacheKeyUtil(context, getCacheKey); 65 | 66 | if (lruCache.has(cacheKey)) { 67 | const { key, value } = lruCache.get(cacheKey); 68 | const request = context.getRequest(); 69 | 70 | context.updateInternalMeta(ETAG, { 71 | value, 72 | }); 73 | 74 | return next({ 75 | request: { 76 | ...request, 77 | headers: { 78 | ...request.headers, 79 | 'If-None-Match': key, 80 | }, 81 | }, 82 | }); 83 | } 84 | 85 | next(); 86 | }, 87 | complete: (context, next) => { 88 | const cacheKey = getCacheKeyUtil(context, getCacheKey); 89 | const etag = getHeader(context as any, 'etag'); 90 | 91 | if (etag) { 92 | lruCache.set(cacheKey, { 93 | key: etag, 94 | value: context.getResponse(), 95 | }); 96 | } 97 | 98 | next(); 99 | }, 100 | error: (context, next) => { 101 | const { error } = context.getState(); 102 | 103 | if ((error as any).status === 304) { 104 | const { value = null } = context.getInternalMeta(ETAG) || {}; 105 | 106 | if (value) { 107 | context.updateExternalMeta(metaTypes.CACHE, { 108 | etagCache: true, 109 | }); 110 | 111 | return next({ 112 | status: Status.COMPLETE, 113 | response: value, 114 | }); 115 | } 116 | } 117 | 118 | next(); 119 | }, 120 | }; 121 | }; 122 | -------------------------------------------------------------------------------- /packages/plugin-cache-etag/src/noop.ts: -------------------------------------------------------------------------------- 1 | import always from '@tinkoff/utils/function/always'; 2 | import etag from './etag'; 3 | 4 | export default always({}) as typeof etag; 5 | -------------------------------------------------------------------------------- /packages/plugin-cache-etag/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib" 5 | }, 6 | "include": [ 7 | "./src" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /packages/plugin-cache-fallback/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | src/ 3 | -------------------------------------------------------------------------------- /packages/plugin-cache-fallback/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [0.10.3](https://github.com/Tinkoff/tinkoff-request/compare/@tinkoff/request-plugin-cache-fallback@0.10.2...@tinkoff/request-plugin-cache-fallback@0.10.3) (2023-08-02) 7 | 8 | **Note:** Version bump only for package @tinkoff/request-plugin-cache-fallback 9 | 10 | 11 | 12 | 13 | 14 | ## [0.10.2](https://github.com/Tinkoff/tinkoff-request/compare/@tinkoff/request-plugin-cache-fallback@0.10.2...@tinkoff/request-plugin-cache-fallback@0.10.2) (2023-07-14) 15 | 16 | 17 | ### Bug Fixes 18 | 19 | * extend typings ([1fcc2cb](https://github.com/Tinkoff/tinkoff-request/commit/1fcc2cb32597b10d788de36303507e385042fc96)) 20 | 21 | 22 | ### Features 23 | 24 | * **cache-fallback:** add ability to specify driver used to store fallback cache ([#43](https://github.com/Tinkoff/tinkoff-request/issues/43)) ([5778e01](https://github.com/Tinkoff/tinkoff-request/commit/5778e01a0281f5772f2c2d879649e89c045209fe)) 25 | * split context.meta into context.internalMeta and context.externalMeta ([31f00e0](https://github.com/Tinkoff/tinkoff-request/commit/31f00e0ae14767f213a67eb2df349c9f75adcfe7)) 26 | * update lru-cache ([dc65ec9](https://github.com/Tinkoff/tinkoff-request/commit/dc65ec92fb185b0100d5a87f4aecadc39f2a9cd5)) 27 | * use @tramvai/build as builder to provide modern es version ([3a26224](https://github.com/Tinkoff/tinkoff-request/commit/3a26224221d4fc073938cf32c2f147515620c28e)) 28 | -------------------------------------------------------------------------------- /packages/plugin-cache-fallback/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tinkoff/request-plugin-cache-fallback", 3 | "version": "0.10.3", 4 | "description": "Cache your responses in file cache to use it while resources are not available", 5 | "main": "lib/fallback.js", 6 | "browser": { 7 | "./lib/drivers/fs.js": "./lib/drivers/noop.js", 8 | "./lib/fallback.es.js": "./lib/fallback.browser.js" 9 | }, 10 | "typings": "lib/fallback.d.ts", 11 | "sideEffects": false, 12 | "scripts": { 13 | "prepack": "tramvai-build --for-publish", 14 | "build": "tramvai-build", 15 | "tsc": "tsc", 16 | "watch": "tsc -w" 17 | }, 18 | "keywords": [ 19 | "request" 20 | ], 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/Tinkoff/tinkoff-request/" 24 | }, 25 | "author": "Oleg Sorokotyaga (http://tinkoff.ru)", 26 | "publishConfig": { 27 | "access": "public" 28 | }, 29 | "license": "ISC", 30 | "dependencies": { 31 | "@tinkoff/lru-cache-nano": "^7.8.1", 32 | "@tinkoff/request-cache-utils": "^0.9.3", 33 | "@tinkoff/utils": "^2.0.0", 34 | "fs-extra": "^9.0.0", 35 | "spark-md5": "^3.0.0", 36 | "tslib": "^2.1.3" 37 | }, 38 | "devDependencies": { 39 | "@tinkoff/request-core": "^0.9.3", 40 | "@types/fs-extra": "^8.1.0", 41 | "@types/node": "^10.11.7", 42 | "@types/spark-md5": "^3.0.1" 43 | }, 44 | "peerDependencies": { 45 | "@tinkoff/request-core": "0.x" 46 | }, 47 | "module": "lib/fallback.es.js" 48 | } 49 | -------------------------------------------------------------------------------- /packages/plugin-cache-fallback/src/drivers/fs.ts: -------------------------------------------------------------------------------- 1 | import noop from '@tinkoff/utils/function/noop'; 2 | import { ensureDirSync, writeJSON, readJSON } from 'fs-extra'; 3 | import path from 'path'; 4 | import { CacheDriver } from '../types'; 5 | import md5 from './md5'; 6 | 7 | const KEY_LIMIT = 100; 8 | 9 | export const driver = ({ 10 | name = 'fallback', 11 | basePath = './.tmp/server-cache/', 12 | }: { 13 | name?: string; 14 | basePath?: string; 15 | } = {}): CacheDriver => { 16 | const cacheDir = path.normalize(`${basePath}/${name}`); 17 | 18 | ensureDirSync(cacheDir); 19 | 20 | const getFileName = (key: string) => { 21 | const name = encodeURIComponent(key.length > KEY_LIMIT ? md5(key) : key); 22 | 23 | return path.normalize(`${cacheDir}/${name}.json`); 24 | }; 25 | 26 | return { 27 | get(key) { 28 | return readJSON(getFileName(key)); 29 | }, 30 | set(key, response) { 31 | writeJSON(getFileName(key), response).catch(noop); 32 | }, 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /packages/plugin-cache-fallback/src/drivers/index.ts: -------------------------------------------------------------------------------- 1 | import { driver as fsCacheDriver } from './fs'; 2 | import { driver as memoryCacheDriver } from './memory'; 3 | import { driver as noopCacheDriver } from './noop'; 4 | 5 | export { fsCacheDriver, memoryCacheDriver, noopCacheDriver }; 6 | -------------------------------------------------------------------------------- /packages/plugin-cache-fallback/src/drivers/md5.spec.ts: -------------------------------------------------------------------------------- 1 | import md5 from './md5'; 2 | 3 | describe('utils/md5', () => { 4 | it('test', () => { 5 | expect(md5('some test string')).toBe('c320d73e0eca9029ab6ab49c99e9795d'); 6 | expect(md5(123)).toBe('202cb962ac59075b964b07152d234b70'); 7 | expect(md5(JSON.stringify({ a: 1, b: 2 }))).toBe('608de49a4600dbb5b173492759792e4a'); 8 | expect(md5(123)).toBe('202cb962ac59075b964b07152d234b70'); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /packages/plugin-cache-fallback/src/drivers/md5.ts: -------------------------------------------------------------------------------- 1 | import SparkMD5 from 'spark-md5'; 2 | 3 | export default (payload: any): string => SparkMD5.hash(payload.toString()); 4 | -------------------------------------------------------------------------------- /packages/plugin-cache-fallback/src/drivers/memory.spec.ts: -------------------------------------------------------------------------------- 1 | import { driver as memoryCacheDriver } from './memory'; 2 | 3 | describe('plugins/cache/fallback/drivers/memory', () => { 4 | describe('default options', () => { 5 | it('should read/write from lruCache', () => { 6 | const driver = memoryCacheDriver(); 7 | 8 | expect(driver.get('test')).toBeUndefined(); 9 | expect(driver.set('test', { a: 1 })).toBeUndefined(); 10 | expect(driver.get('test')).toEqual({ a: 1 }); 11 | }); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /packages/plugin-cache-fallback/src/drivers/memory.ts: -------------------------------------------------------------------------------- 1 | import LRUCache, { Options } from '@tinkoff/lru-cache-nano'; 2 | import { Response } from '@tinkoff/request-core'; 3 | import { CacheDriver } from '../types'; 4 | 5 | export const driver = ({ 6 | lruOptions = { max: 1000 }, 7 | memoryConstructor = (options) => new (require('@tinkoff/lru-cache-nano'))(options), 8 | }: { 9 | lruOptions?: Options; 10 | memoryConstructor?: (options: Options) => LRUCache; 11 | } = {}): CacheDriver => { 12 | const cache = memoryConstructor(lruOptions); 13 | 14 | return { 15 | get(key) { 16 | return cache.get(key); 17 | }, 18 | set(key, response) { 19 | cache.set(key, response); 20 | }, 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /packages/plugin-cache-fallback/src/drivers/noop.ts: -------------------------------------------------------------------------------- 1 | import { CacheDriver } from '../types'; 2 | 3 | export const driver: (...args) => CacheDriver = () => null; 4 | -------------------------------------------------------------------------------- /packages/plugin-cache-fallback/src/fallback.spec.ts: -------------------------------------------------------------------------------- 1 | import { Context, Status } from '@tinkoff/request-core'; 2 | import { metaTypes } from '@tinkoff/request-cache-utils'; 3 | import fallback from './fallback'; 4 | 5 | const next = jest.fn(); 6 | const getCacheKey = jest.fn((req) => req.url); 7 | const driver = { 8 | get: jest.fn(), 9 | set: jest.fn(), 10 | }; 11 | 12 | const wait = async (n = 1) => { 13 | for (let i = 0; i < n; i++) { 14 | await Promise.resolve(); 15 | } 16 | }; 17 | 18 | describe('plugins/cache/fallback', () => { 19 | beforeEach(() => { 20 | next.mockClear(); 21 | getCacheKey.mockClear(); 22 | driver.get.mockClear(); 23 | driver.set.mockClear(); 24 | }); 25 | 26 | it('save to cache on complete', async () => { 27 | const request = { url: 'test[123]//pf' }; 28 | const response = { a: 1, b: 2 }; 29 | const plugin = fallback({ getCacheKey, shouldExecute: true, driver }); 30 | const context = new Context({ request, response }); 31 | 32 | plugin.complete(context, next, null); 33 | await wait(); 34 | expect(getCacheKey).toHaveBeenLastCalledWith(request); 35 | expect(driver.set).toHaveBeenLastCalledWith(request.url, response); 36 | expect(driver.get).not.toHaveBeenCalled(); 37 | }); 38 | 39 | it('tries return from cache on error', async () => { 40 | const fromCache = { test: 'pfpf' }; 41 | const request = { url: 'test[123]//pf' }; 42 | const error = new Error('123'); 43 | const plugin = fallback({ getCacheKey, shouldExecute: true, driver }); 44 | const context = new Context({ request, error }); 45 | const next = jest.fn(); 46 | 47 | driver.get.mockImplementation(() => fromCache); 48 | context.updateExternalMeta = jest.fn(context.updateExternalMeta.bind(context)); 49 | 50 | plugin.error(context, next, null); 51 | await wait(2); 52 | expect(getCacheKey).toHaveBeenLastCalledWith(request); 53 | expect(driver.set).not.toHaveBeenCalled(); 54 | expect(driver.get).toHaveBeenLastCalledWith(request.url); 55 | expect(context.updateExternalMeta).toHaveBeenLastCalledWith(metaTypes.CACHE, { fallbackCache: true }); 56 | expect(next).toHaveBeenLastCalledWith({ status: Status.COMPLETE, response: fromCache }); 57 | }); 58 | 59 | it('tries return from cache on error, but persist cache errors', async () => { 60 | const request = { url: 'test[123]//pf' }; 61 | const error = new Error('123'); 62 | const plugin = fallback({ getCacheKey, shouldExecute: true, driver }); 63 | const context = new Context({ request, error }); 64 | const next = jest.fn(); 65 | 66 | driver.get.mockImplementation(() => { 67 | throw error; 68 | }); 69 | context.updateExternalMeta = jest.fn(context.updateExternalMeta.bind(context)); 70 | 71 | plugin.error(context, next, null); 72 | await wait(2); 73 | expect(getCacheKey).toHaveBeenLastCalledWith(request); 74 | expect(driver.set).not.toHaveBeenCalled(); 75 | expect(driver.get).toHaveBeenLastCalledWith(request.url); 76 | expect(context.updateExternalMeta).not.toHaveBeenLastCalledWith(metaTypes.CACHE, { fromFallback: true }); 77 | expect(next).toHaveBeenLastCalledWith(); 78 | }); 79 | 80 | it('if shouldFallback returns false, do not use cache', () => { 81 | const request = { url: 'test[123]//pf' }; 82 | const error = new Error('123'); 83 | const shouldFallback = jest.fn(() => false); 84 | const plugin = fallback({ getCacheKey, shouldFallback, shouldExecute: true, driver }); 85 | const context = new Context({ request, error }); 86 | const next = jest.fn(); 87 | 88 | context.updateExternalMeta = jest.fn(context.updateExternalMeta.bind(context)); 89 | 90 | plugin.error(context, next, null); 91 | expect(shouldFallback).toHaveBeenCalledWith(context.getState()); 92 | expect(getCacheKey).not.toHaveBeenCalled(); 93 | expect(driver.set).not.toHaveBeenCalled(); 94 | expect(driver.get).not.toHaveBeenCalled(); 95 | expect(context.updateExternalMeta).not.toHaveBeenCalled(); 96 | expect(next).toHaveBeenLastCalledWith(); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /packages/plugin-cache-fallback/src/fallback.ts: -------------------------------------------------------------------------------- 1 | import noop from '@tinkoff/utils/function/noop'; 2 | import T from '@tinkoff/utils/function/T'; 3 | 4 | import type { Plugin, ContextState, Request } from '@tinkoff/request-core'; 5 | import { Status } from '@tinkoff/request-core'; 6 | import { shouldCacheExecute, getCacheKey as getCacheKeyUtil, metaTypes } from '@tinkoff/request-cache-utils'; 7 | import { CacheDriver } from './types'; 8 | import { fsCacheDriver } from './drivers'; 9 | 10 | declare module '@tinkoff/request-core/lib/types.h' { 11 | export interface Request { 12 | fallbackCache?: boolean; 13 | fallbackCacheForce?: boolean; 14 | } 15 | } 16 | 17 | /** 18 | * Fallback cache plugin. This cache used only if request ends with error response and returns previous success response from cache. 19 | * Actual place to store cache data depends on passed driver (file system by default). 20 | * 21 | * requestParams: 22 | * fallbackCache {boolean} - disable this plugin at all 23 | * fallbackCacheForce {boolean} - plugin will only be executed on complete phase 24 | * 25 | * metaInfo: 26 | * fallbackCache {boolean} - is current request was returned from fallback 27 | * 28 | * @param {boolean} [shouldExecute = true] is plugin activated by default 29 | * @param {function} shouldFallback should fallback value be returned from cache 30 | * @param {function} getCacheKey function used for generate cache key 31 | * @param {CacheDriver} [driver = fsCacheDriver] driver used to store fallback data 32 | */ 33 | export default ({ 34 | shouldExecute = true, 35 | shouldFallback = T, 36 | getCacheKey, 37 | driver = fsCacheDriver(), 38 | }: { 39 | shouldExecute?: boolean; 40 | shouldFallback?: (state: ContextState) => boolean; 41 | getCacheKey?: (request: Request) => string; 42 | driver?: CacheDriver; 43 | } = {}): Plugin => { 44 | if (!driver) { 45 | return {}; 46 | } 47 | 48 | return { 49 | shouldExecute: shouldCacheExecute('fallback', shouldExecute), 50 | complete: (context, next) => { 51 | const cacheKey = getCacheKeyUtil(context, getCacheKey); 52 | 53 | Promise.resolve() 54 | .then(() => driver.set(cacheKey, context.getResponse())) 55 | .catch(noop) 56 | .then(() => next()); 57 | }, 58 | error: (context, next) => { 59 | if (!shouldFallback(context.getState())) { 60 | return next(); 61 | } 62 | 63 | const cacheKey = getCacheKeyUtil(context, getCacheKey); 64 | 65 | Promise.resolve() 66 | .then(() => driver.get(cacheKey)) 67 | .then((response) => { 68 | if (!response) { 69 | return next(); 70 | } 71 | 72 | context.updateExternalMeta(metaTypes.CACHE, { 73 | fallbackCache: true, 74 | }); 75 | 76 | next({ 77 | status: Status.COMPLETE, 78 | response, 79 | }); 80 | }) 81 | .catch(() => next()); 82 | }, 83 | }; 84 | }; 85 | -------------------------------------------------------------------------------- /packages/plugin-cache-fallback/src/types.ts: -------------------------------------------------------------------------------- 1 | import { Response } from '@tinkoff/request-core'; 2 | 3 | export interface CacheDriver { 4 | get(key: string): Response | Promise; 5 | set(key: string, response: Response): void | Promise; 6 | } 7 | -------------------------------------------------------------------------------- /packages/plugin-cache-fallback/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib" 5 | }, 6 | "include": [ 7 | "./src" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /packages/plugin-cache-memory/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | src/ 3 | -------------------------------------------------------------------------------- /packages/plugin-cache-memory/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [0.9.4](https://github.com/Tinkoff/tinkoff-request/compare/@tinkoff/request-plugin-cache-memory@0.9.3...@tinkoff/request-plugin-cache-memory@0.9.4) (2023-08-02) 7 | 8 | **Note:** Version bump only for package @tinkoff/request-plugin-cache-memory 9 | 10 | 11 | 12 | 13 | 14 | ## [0.9.3](https://github.com/Tinkoff/tinkoff-request/compare/@tinkoff/request-plugin-cache-memory@0.9.3...@tinkoff/request-plugin-cache-memory@0.9.3) (2023-07-14) 15 | 16 | 17 | ### Bug Fixes 18 | 19 | * **cache-memory:** do not abort background memory requests ([80395f9](https://github.com/Tinkoff/tinkoff-request/commit/80395f9be96cb73e62d09590aa89f043ab8ca679)) 20 | * **cache-memory:** fix unhandled rejection when renew cache in background fails ([a5a50a4](https://github.com/Tinkoff/tinkoff-request/commit/a5a50a463f632614b8be4bc39d540d3503b44914)) 21 | * extend typings ([1fcc2cb](https://github.com/Tinkoff/tinkoff-request/commit/1fcc2cb32597b10d788de36303507e385042fc96)) 22 | 23 | 24 | ### Features 25 | 26 | * **cache-memory:** allow to specify time of life for outdated value in memory cache ([48bd8ad](https://github.com/Tinkoff/tinkoff-request/commit/48bd8adb52cac7aea3f5a42ab6f1999edec4c704)) 27 | * parametrize background requests timeout ([#88](https://github.com/Tinkoff/tinkoff-request/issues/88)) ([d391fae](https://github.com/Tinkoff/tinkoff-request/commit/d391fae684a0d4ff2a5990ad4114c82f1208e09e)) 28 | * split context.meta into context.internalMeta and context.externalMeta ([31f00e0](https://github.com/Tinkoff/tinkoff-request/commit/31f00e0ae14767f213a67eb2df349c9f75adcfe7)) 29 | * update lru-cache ([dc65ec9](https://github.com/Tinkoff/tinkoff-request/commit/dc65ec92fb185b0100d5a87f4aecadc39f2a9cd5)) 30 | * use @tramvai/build as builder to provide modern es version ([3a26224](https://github.com/Tinkoff/tinkoff-request/commit/3a26224221d4fc073938cf32c2f147515620c28e)) 31 | -------------------------------------------------------------------------------- /packages/plugin-cache-memory/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tinkoff/request-plugin-cache-memory", 3 | "version": "0.9.4", 4 | "description": "Memory cache for tinkoff request", 5 | "main": "lib/memory.js", 6 | "typings": "lib/memory.d.ts", 7 | "sideEffects": false, 8 | "scripts": { 9 | "prepack": "tramvai-build --for-publish", 10 | "build": "tramvai-build", 11 | "tsc": "tsc", 12 | "watch": "tsc -w" 13 | }, 14 | "keywords": [ 15 | "request", 16 | "memory" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/Tinkoff/tinkoff-request/" 21 | }, 22 | "author": "Oleg Sorokotyaga (http://tinkoff.ru)", 23 | "publishConfig": { 24 | "access": "public" 25 | }, 26 | "license": "ISC", 27 | "dependencies": { 28 | "@tinkoff/lru-cache-nano": "^7.8.1", 29 | "@tinkoff/request-cache-utils": "^0.9.3", 30 | "@tinkoff/utils": "^2.0.0", 31 | "tslib": "^2.1.3" 32 | }, 33 | "devDependencies": { 34 | "@tinkoff/request-core": "^0.9.3" 35 | }, 36 | "peerDependencies": { 37 | "@tinkoff/request-core": "0.x" 38 | }, 39 | "module": "lib/memory.es.js" 40 | } 41 | -------------------------------------------------------------------------------- /packages/plugin-cache-memory/src/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * determine how long should we wait for background requests relative to the timeout of the initial request 3 | */ 4 | const BACKGROUND_REQUEST_TIMEOUT_FACTOR = 2; 5 | 6 | export const getStaleBackgroundRequestTimeout = (params: { requestTimeout?: number; configTimeout?: number }) => { 7 | const { requestTimeout, configTimeout } = params; 8 | if (configTimeout) { 9 | return configTimeout; 10 | } 11 | 12 | if (requestTimeout) { 13 | return requestTimeout * BACKGROUND_REQUEST_TIMEOUT_FACTOR; 14 | } 15 | 16 | return undefined; 17 | }; 18 | -------------------------------------------------------------------------------- /packages/plugin-cache-memory/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib" 5 | }, 6 | "include": [ 7 | "./src" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /packages/plugin-cache-persistent/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | src/ 3 | -------------------------------------------------------------------------------- /packages/plugin-cache-persistent/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [0.9.3](https://github.com/Tinkoff/tinkoff-request/compare/@tinkoff/request-plugin-cache-persistent@0.9.2...@tinkoff/request-plugin-cache-persistent@0.9.3) (2023-08-02) 7 | 8 | **Note:** Version bump only for package @tinkoff/request-plugin-cache-persistent 9 | 10 | 11 | 12 | 13 | 14 | ## [0.9.2](https://github.com/Tinkoff/tinkoff-request/compare/@tinkoff/request-plugin-cache-persistent@0.9.2...@tinkoff/request-plugin-cache-persistent@0.9.2) (2023-07-14) 15 | 16 | 17 | ### Bug Fixes 18 | 19 | * extend typings ([1fcc2cb](https://github.com/Tinkoff/tinkoff-request/commit/1fcc2cb32597b10d788de36303507e385042fc96)) 20 | 21 | 22 | ### Features 23 | 24 | * split context.meta into context.internalMeta and context.externalMeta ([31f00e0](https://github.com/Tinkoff/tinkoff-request/commit/31f00e0ae14767f213a67eb2df349c9f75adcfe7)) 25 | * use @tramvai/build as builder to provide modern es version ([3a26224](https://github.com/Tinkoff/tinkoff-request/commit/3a26224221d4fc073938cf32c2f147515620c28e)) 26 | -------------------------------------------------------------------------------- /packages/plugin-cache-persistent/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tinkoff/request-plugin-cache-persistent", 3 | "version": "0.9.3", 4 | "description": "Stores responses in IndexedDB to preserve data in browser", 5 | "main": "lib/persistent.js", 6 | "typings": "lib/persistent.d.ts", 7 | "sideEffects": false, 8 | "scripts": { 9 | "prepack": "tramvai-build --for-publish", 10 | "build": "tramvai-build", 11 | "tsc": "tsc", 12 | "watch": "tsc -w", 13 | "test": "echo \"Error: no test specified\" && exit 1" 14 | }, 15 | "keywords": [ 16 | "request" 17 | ], 18 | "author": "Oleg Sorokotyaga (http://tinkoff.ru)", 19 | "publishConfig": { 20 | "access": "public" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/Tinkoff/tinkoff-request/" 25 | }, 26 | "license": "ISC", 27 | "dependencies": { 28 | "@tinkoff/request-cache-utils": "^0.9.3", 29 | "idb-keyval": "3.0.3", 30 | "tslib": "^2.1.3" 31 | }, 32 | "devDependencies": { 33 | "@tinkoff/request-core": "^0.9.3", 34 | "@types/node": "^10.12.0" 35 | }, 36 | "peerDependencies": { 37 | "@tinkoff/request-core": "0.x" 38 | }, 39 | "module": "lib/persistent.es.js" 40 | } 41 | -------------------------------------------------------------------------------- /packages/plugin-cache-persistent/src/persistent.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | import { Context, Status } from '@tinkoff/request-core'; 6 | import { metaTypes } from '@tinkoff/request-cache-utils'; 7 | import persistent from './persistent'; 8 | 9 | const mockStore = { store: 'test' }; 10 | const mockIDB = { 11 | get: jest.fn(), 12 | set: jest.fn(), 13 | Store: function () { 14 | return mockStore; 15 | }, 16 | }; 17 | 18 | jest.mock('idb-keyval', () => mockIDB); 19 | 20 | const context = new Context({ request: { url: 'test' } }); 21 | 22 | context.updateExternalMeta = jest.fn(context.updateExternalMeta.bind(context)); 23 | const getCacheKey = jest.fn((req) => req.url); 24 | const next = jest.fn(); 25 | 26 | describe('plugins/cache/persistent', () => { 27 | let indexedDB; 28 | let plugin; 29 | 30 | beforeAll(() => { 31 | indexedDB = window.indexedDB; 32 | }); 33 | 34 | afterAll(() => { 35 | (window as any).indexedDB = indexedDB; 36 | }); 37 | 38 | beforeEach(() => { 39 | mockIDB.get.mockClear(); 40 | mockIDB.set.mockClear(); 41 | next.mockClear(); 42 | (context.updateExternalMeta as jest.Mock).mockClear(); 43 | (window as any).indexedDB = {}; 44 | plugin = persistent({ getCacheKey, shouldExecute: true }); 45 | }); 46 | 47 | it('only for browsers', () => { 48 | (window as any).indexedDB = undefined; 49 | 50 | expect(persistent({ shouldExecute: true })).toEqual({}); 51 | 52 | (window as any).indexedDB = {}; 53 | 54 | expect(persistent({ shouldExecute: true })).toEqual({ 55 | shouldExecute: expect.any(Function), 56 | init: expect.any(Function), 57 | complete: expect.any(Function), 58 | }); 59 | }); 60 | 61 | it('init no value in indexedDB', () => { 62 | mockIDB.get.mockImplementation(() => Promise.resolve()); 63 | 64 | plugin.init(context, next, null); 65 | 66 | expect(mockIDB.get).toHaveBeenCalledWith('test', mockStore); 67 | return Promise.resolve().then(() => { 68 | expect(context.updateExternalMeta).not.toHaveBeenCalledWith(metaTypes.CACHE, { 69 | fromPersistCache: true, 70 | }); 71 | expect(next).toHaveBeenCalledWith(); 72 | }); 73 | }); 74 | 75 | it('init, value in indexedDB', () => { 76 | const response = { a: 2 }; 77 | 78 | mockIDB.get.mockImplementation(() => Promise.resolve(response)); 79 | 80 | plugin.init(context, next, null); 81 | 82 | return Promise.resolve().then(() => { 83 | expect(context.updateExternalMeta).toHaveBeenCalledWith(metaTypes.CACHE, { 84 | persistentCache: true, 85 | }); 86 | expect(next).toHaveBeenCalledWith({ 87 | response, 88 | status: Status.COMPLETE, 89 | }); 90 | }); 91 | }); 92 | 93 | it('on complete saves to cache', () => { 94 | const response = { a: 3 }; 95 | 96 | context.setState({ response }); 97 | 98 | plugin.complete(context, next, null); 99 | 100 | expect(next).toHaveBeenCalled(); 101 | expect(mockIDB.set).toHaveBeenCalledWith('test', response, mockStore); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /packages/plugin-cache-persistent/src/persistent.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from '@tinkoff/request-core'; 2 | import { Status } from '@tinkoff/request-core'; 3 | import { shouldCacheExecute, getCacheKey as getCacheKeyUtil, metaTypes } from '@tinkoff/request-cache-utils'; 4 | 5 | declare module '@tinkoff/request-core/lib/types.h' { 6 | export interface Request { 7 | persistentCache?: boolean; 8 | persistentCacheForce?: boolean; 9 | } 10 | } 11 | 12 | /** 13 | * Caches requests result into IndexedDB. 14 | * Uses library `idb-keyval` as wrapper to IndexedDB. 15 | * Works only in browsers with support of IndexedDB, otherwise does nothing. 16 | * 17 | * requestParams: 18 | * persistentCache {boolean} - disable this plugin at all 19 | * persistentCacheForce {boolean} - plugin will only be executed on complete phase 20 | * 21 | * metaInfo: 22 | * persistentCache {boolean} - is current request was returned from this cache 23 | * 24 | * @param {boolean} [shouldExecute = true] is plugin activated by default 25 | * @param {function} getCacheKey function used for generate cache key 26 | */ 27 | export default ({ shouldExecute = true, getCacheKey = undefined } = {}): Plugin => { 28 | if (typeof window !== 'undefined' && window.indexedDB) { 29 | const { get, set, Store } = require('idb-keyval'); 30 | const store = new Store('tinkoff-cache', 'persistent'); 31 | 32 | return { 33 | shouldExecute: shouldCacheExecute('persistent', shouldExecute), 34 | init: (context, next) => { 35 | const cacheKey = getCacheKeyUtil(context, getCacheKey); 36 | 37 | get(cacheKey, store) 38 | .then((value) => { 39 | if (value) { 40 | context.updateExternalMeta(metaTypes.CACHE, { 41 | persistentCache: true, 42 | }); 43 | return next({ 44 | status: Status.COMPLETE, 45 | response: value, 46 | }); 47 | } 48 | 49 | next(); 50 | }) 51 | .catch((err) => { 52 | next(); 53 | }); 54 | }, 55 | complete: (context, next) => { 56 | const cacheKey = getCacheKeyUtil(context, getCacheKey); 57 | 58 | set(cacheKey, context.getResponse(), store); 59 | 60 | next(); 61 | }, 62 | }; 63 | } 64 | 65 | return {}; 66 | }; 67 | -------------------------------------------------------------------------------- /packages/plugin-cache-persistent/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib" 5 | }, 6 | "include": [ 7 | "./src" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /packages/plugin-circuit-breaker/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | src/ 3 | -------------------------------------------------------------------------------- /packages/plugin-circuit-breaker/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [0.3.3](https://github.com/Tinkoff/tinkoff-request/compare/@tinkoff/request-plugin-circuit-breaker@0.3.2...@tinkoff/request-plugin-circuit-breaker@0.3.3) (2023-08-02) 7 | 8 | **Note:** Version bump only for package @tinkoff/request-plugin-circuit-breaker 9 | 10 | 11 | 12 | 13 | 14 | ## [0.3.2](https://github.com/Tinkoff/tinkoff-request/compare/@tinkoff/request-plugin-circuit-breaker@0.3.2...@tinkoff/request-plugin-circuit-breaker@0.3.2) (2023-07-14) 15 | 16 | 17 | ### Features 18 | 19 | * **circuit-breaker:** add ability to specify error check for circuit breaker fail request status ([aacb719](https://github.com/Tinkoff/tinkoff-request/commit/aacb719ff17f76df51317698cf1c2e56c607b731)) 20 | * split context.meta into context.internalMeta and context.externalMeta ([31f00e0](https://github.com/Tinkoff/tinkoff-request/commit/31f00e0ae14767f213a67eb2df349c9f75adcfe7)) 21 | * use @tramvai/build as builder to provide modern es version ([3a26224](https://github.com/Tinkoff/tinkoff-request/commit/3a26224221d4fc073938cf32c2f147515620c28e)) 22 | -------------------------------------------------------------------------------- /packages/plugin-circuit-breaker/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tinkoff/request-plugin-circuit-breaker", 3 | "version": "0.3.3", 4 | "description": "Plugin implementing `Circuit breaker` design pattern", 5 | "main": "lib/circuit-breaker.js", 6 | "typings": "lib/circuit-breaker.d.ts", 7 | "browser": "lib/noop.js", 8 | "sideEffects": false, 9 | "scripts": { 10 | "prepack": "tramvai-build --for-publish", 11 | "build": "tramvai-build", 12 | "tsc": "tsc", 13 | "watch": "tsc -w", 14 | "test": "echo \"Error: no test specified\" && exit 1" 15 | }, 16 | "keywords": [ 17 | "@tinkoff/request", 18 | "request", 19 | "circuit breaker" 20 | ], 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/Tinkoff/tinkoff-request/" 24 | }, 25 | "author": "Oleg Sorokotyaga (http://tinkoff.ru)", 26 | "publishConfig": { 27 | "access": "public" 28 | }, 29 | "license": "ISC", 30 | "dependencies": { 31 | "@tinkoff/utils": "^2.0.0", 32 | "tslib": "^2.1.3" 33 | }, 34 | "devDependencies": { 35 | "@tinkoff/request-core": "^0.9.3", 36 | "@types/node": "^10.12.0" 37 | }, 38 | "peerDependencies": { 39 | "@tinkoff/request-core": "0.x" 40 | }, 41 | "module": "lib/circuit-breaker.es.js" 42 | } 43 | -------------------------------------------------------------------------------- /packages/plugin-circuit-breaker/src/CircuitBreaker.spec.ts: -------------------------------------------------------------------------------- 1 | import each from '@tinkoff/utils/array/each'; 2 | import { CircuitBreaker } from './CircuitBreaker'; 3 | 4 | describe('Circuit Breaker class', () => { 5 | let circuitBreaker: CircuitBreaker; 6 | let currentTime = 1487077708000; 7 | let mockDate; 8 | 9 | const runActions = (n: number, fail = false) => { 10 | each((v, i) => { 11 | circuitBreaker.shouldThrow(); 12 | if (fail) { 13 | circuitBreaker.failure(new Error(`error ${i}`)); 14 | } else { 15 | circuitBreaker.success(); 16 | } 17 | }, Array(n)); 18 | }; 19 | 20 | beforeAll(() => { 21 | mockDate = jest.spyOn(Date, 'now').mockImplementation(() => currentTime); 22 | }); 23 | 24 | afterAll(() => { 25 | mockDate.mockRestore(); 26 | }); 27 | 28 | beforeEach(() => { 29 | circuitBreaker = new CircuitBreaker({ 30 | failureTimeout: 25, 31 | failureThreshold: 50, 32 | openTimeout: 2000, 33 | halfOpenThreshold: 10, 34 | minimumFailureCount: 4, 35 | }); 36 | }); 37 | 38 | it('should not throw after first `failureThreshold` percent of errors', () => { 39 | expect(circuitBreaker.isClosed()).toBe(true); 40 | expect(circuitBreaker.shouldThrow()).toBe(false); 41 | 42 | runActions(5); 43 | runActions(5, true); 44 | 45 | expect(circuitBreaker.shouldThrow()).toBe(false); 46 | expect(circuitBreaker.getError()).toBeUndefined(); 47 | 48 | runActions(3, true); 49 | expect(circuitBreaker.shouldThrow()).toBe(true); 50 | expect(circuitBreaker.getError()).toEqual(new Error('error 2')); 51 | expect(circuitBreaker.isOpen()).toBe(true); 52 | }); 53 | 54 | it('should not throw when number of failures is less than `minimumFailureCount` in `failureTimeout` period', () => { 55 | expect(circuitBreaker.shouldThrow()).toBe(false); 56 | 57 | runActions(4, true); 58 | expect(circuitBreaker.shouldThrow()).toBe(false); 59 | 60 | currentTime += 30; 61 | runActions(3, true); 62 | expect(circuitBreaker.shouldThrow()).toBe(false); 63 | 64 | currentTime += 26; 65 | runActions(4, true); 66 | expect(circuitBreaker.shouldThrow()).toBe(false); 67 | 68 | currentTime += 5; 69 | runActions(1, true); 70 | expect(circuitBreaker.shouldThrow()).toBe(true); 71 | }); 72 | 73 | it('should change state to half-open after `openTimeout`', () => { 74 | runActions(10, true); 75 | 76 | expect(circuitBreaker.shouldThrow()).toBe(true); 77 | currentTime += 2500; 78 | 79 | expect(circuitBreaker.shouldThrow()).toBe(false); 80 | expect(circuitBreaker.isHalfOpen()).toBe(true); 81 | }); 82 | 83 | it('should allow to execute no more than `halfOpenThreshold` actions if in half-open state', () => { 84 | runActions(20); 85 | runActions(21, true); 86 | expect(circuitBreaker.shouldThrow()).toBe(true); 87 | 88 | currentTime += 2500; 89 | 90 | expect(circuitBreaker.shouldThrow()).toBe(false); 91 | expect(circuitBreaker.shouldThrow()).toBe(false); 92 | expect(circuitBreaker.shouldThrow()).toBe(false); 93 | expect(circuitBreaker.shouldThrow()).toBe(false); 94 | expect(circuitBreaker.shouldThrow()).toBe(false); 95 | expect(circuitBreaker.shouldThrow()).toBe(true); 96 | }); 97 | 98 | it('should return to closed state when actions are success', () => { 99 | runActions(10, true); 100 | currentTime += 2500; 101 | 102 | expect(circuitBreaker.shouldThrow()).toBe(false); 103 | expect(circuitBreaker.isHalfOpen()).toBe(true); 104 | 105 | runActions(2); 106 | 107 | expect(circuitBreaker.isClosed()).toBe(true); 108 | expect(circuitBreaker.shouldThrow()).toBe(false); 109 | }); 110 | 111 | it('should return to open state if any action is failed while in half-open state', () => { 112 | const error = new Error('test'); 113 | 114 | runActions(50); 115 | runActions(60, true); 116 | currentTime += 2500; 117 | 118 | circuitBreaker.shouldThrow(); 119 | circuitBreaker.shouldThrow(); 120 | 121 | circuitBreaker.success(); 122 | circuitBreaker.failure(error); 123 | 124 | expect(circuitBreaker.isOpen()).toBe(true); 125 | expect(circuitBreaker.shouldThrow()).toBe(true); 126 | expect(circuitBreaker.getError()).toBe(error); 127 | }); 128 | }); 129 | -------------------------------------------------------------------------------- /packages/plugin-circuit-breaker/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const CIRCUIT_BREAKER_META = 'CIRCUIT_BREAKER'; 2 | -------------------------------------------------------------------------------- /packages/plugin-circuit-breaker/src/errors.ts: -------------------------------------------------------------------------------- 1 | import { RequestError, RequestErrorCode } from '@tinkoff/request-core'; 2 | 3 | export class CircuitBreakerError extends Error implements RequestError { 4 | code: RequestErrorCode['ERR_CIRCUIT_BREAKER_OPEN'] = 'ERR_CIRCUIT_BREAKER_OPEN'; 5 | 6 | constructor(public lastError: Error) { 7 | super('Circuit Breaker has blocked request'); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/plugin-circuit-breaker/src/noop.ts: -------------------------------------------------------------------------------- 1 | import always from '@tinkoff/utils/function/always'; 2 | import circuitBreaker from './circuit-breaker'; 3 | 4 | export default always({}) as typeof circuitBreaker; 5 | -------------------------------------------------------------------------------- /packages/plugin-circuit-breaker/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib" 5 | }, 6 | "include": [ 7 | "./src" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /packages/plugin-log/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | src/ 3 | -------------------------------------------------------------------------------- /packages/plugin-log/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [0.9.3](https://github.com/Tinkoff/tinkoff-request/compare/@tinkoff/request-plugin-log@0.9.2...@tinkoff/request-plugin-log@0.9.3) (2023-08-02) 7 | 8 | **Note:** Version bump only for package @tinkoff/request-plugin-log 9 | 10 | 11 | 12 | 13 | 14 | ## [0.9.2](https://github.com/Tinkoff/tinkoff-request/compare/@tinkoff/request-plugin-log@0.9.2...@tinkoff/request-plugin-log@0.9.2) (2023-07-14) 15 | 16 | 17 | ### Bug Fixes 18 | 19 | * extend typings ([1fcc2cb](https://github.com/Tinkoff/tinkoff-request/commit/1fcc2cb32597b10d788de36303507e385042fc96)) 20 | * **log:** move logging to debug level instead of info ([#45](https://github.com/Tinkoff/tinkoff-request/issues/45)) ([340b83a](https://github.com/Tinkoff/tinkoff-request/commit/340b83a64306e4949d9624cc1f37d48c81e18c52)) 21 | * **log:** refactor log format to simplify usage after json conversion ([#32](https://github.com/Tinkoff/tinkoff-request/issues/32)) ([e674ecc](https://github.com/Tinkoff/tinkoff-request/commit/e674ecc3cdb02b446655fd735dbacc1fa1548b58)) 22 | 23 | 24 | ### Features 25 | 26 | * **log:** mask query and payload values by default ([#39](https://github.com/Tinkoff/tinkoff-request/issues/39)) ([3ecbd21](https://github.com/Tinkoff/tinkoff-request/commit/3ecbd21a4ceda981e504dda05fbadcc0e5e310d4)) 27 | * split context.meta into context.internalMeta and context.externalMeta ([31f00e0](https://github.com/Tinkoff/tinkoff-request/commit/31f00e0ae14767f213a67eb2df349c9f75adcfe7)) 28 | * use @tramvai/build as builder to provide modern es version ([3a26224](https://github.com/Tinkoff/tinkoff-request/commit/3a26224221d4fc073938cf32c2f147515620c28e)) 29 | -------------------------------------------------------------------------------- /packages/plugin-log/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tinkoff/request-plugin-log", 3 | "version": "0.9.3", 4 | "description": "Log debug info abour running requests", 5 | "main": "lib/log.js", 6 | "typings": "lib/log.d.ts", 7 | "sideEffects": false, 8 | "scripts": { 9 | "prepack": "tramvai-build --for-publish", 10 | "build": "tramvai-build", 11 | "tsc": "tsc", 12 | "watch": "tsc -w", 13 | "test": "echo \"Error: no test specified\" && exit 1" 14 | }, 15 | "keywords": [ 16 | "request" 17 | ], 18 | "author": "Oleg Sorokotyaga (http://tinkoff.ru)", 19 | "publishConfig": { 20 | "access": "public" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/Tinkoff/tinkoff-request/" 25 | }, 26 | "license": "ISC", 27 | "dependencies": { 28 | "@tinkoff/utils": "^2.0.0", 29 | "tslib": "^2.1.3" 30 | }, 31 | "devDependencies": { 32 | "@tinkoff/request-core": "^0.9.3" 33 | }, 34 | "peerDependencies": { 35 | "@tinkoff/request-core": "0.x" 36 | }, 37 | "module": "lib/log.es.js" 38 | } 39 | -------------------------------------------------------------------------------- /packages/plugin-log/src/constants/metaTypes.ts: -------------------------------------------------------------------------------- 1 | export const LOG = 'log'; 2 | -------------------------------------------------------------------------------- /packages/plugin-log/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib" 5 | }, 6 | "include": [ 7 | "./src" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /packages/plugin-prom-red-metrics/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | src/ 3 | -------------------------------------------------------------------------------- /packages/plugin-prom-red-metrics/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [0.2.9](https://github.com/Tinkoff/tinkoff-request/compare/@tinkoff/request-plugin-prom-red-metrics@0.2.8...@tinkoff/request-plugin-prom-red-metrics@0.2.9) (2023-08-02) 7 | 8 | **Note:** Version bump only for package @tinkoff/request-plugin-prom-red-metrics 9 | 10 | 11 | 12 | 13 | 14 | ## [0.2.8](https://github.com/Tinkoff/tinkoff-request/compare/@tinkoff/request-plugin-prom-red-metrics@0.2.8...@tinkoff/request-plugin-prom-red-metrics@0.2.8) (2023-07-14) 15 | 16 | 17 | ### Bug Fixes 18 | 19 | * extend typings ([1fcc2cb](https://github.com/Tinkoff/tinkoff-request/commit/1fcc2cb32597b10d788de36303507e385042fc96)) 20 | * **prom-red-metrics:** Added reexport for http variables ([#36](https://github.com/Tinkoff/tinkoff-request/issues/36)) ([20375d6](https://github.com/Tinkoff/tinkoff-request/commit/20375d6055d916406b299c612ba4cf1022ba46fa)) 21 | 22 | 23 | ### Features 24 | 25 | * **prom-red-metrics:** Prometheus metrics plugin added ([#34](https://github.com/Tinkoff/tinkoff-request/issues/34)) ([e40e950](https://github.com/Tinkoff/tinkoff-request/commit/e40e95010bcb05cb9dfbff0158f9ae185cb089cb)) 26 | * use @tramvai/build as builder to provide modern es version ([3a26224](https://github.com/Tinkoff/tinkoff-request/commit/3a26224221d4fc073938cf32c2f147515620c28e)) 27 | -------------------------------------------------------------------------------- /packages/plugin-prom-red-metrics/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tinkoff/request-plugin-prom-red-metrics", 3 | "version": "0.2.9", 4 | "description": "Collect RED metrics for prometheus", 5 | "main": "lib/index.js", 6 | "typings": "lib/index.d.ts", 7 | "sideEffects": false, 8 | "scripts": { 9 | "prepack": "tramvai-build --for-publish", 10 | "build": "tramvai-build", 11 | "tsc": "tsc", 12 | "watch": "tsc -w" 13 | }, 14 | "keywords": [ 15 | "request", 16 | "prometheus" 17 | ], 18 | "author": "Boris Chernysh ", 19 | "publishConfig": { 20 | "access": "public" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/Tinkoff/tinkoff-request/" 25 | }, 26 | "license": "ISC", 27 | "dependencies": { 28 | "tslib": "^2.1.3" 29 | }, 30 | "devDependencies": { 31 | "@tinkoff/request-core": "^0.9.3", 32 | "@tinkoff/request-plugin-protocol-http": "^0.11.9" 33 | }, 34 | "peerDependencies": { 35 | "@tinkoff/request-core": "0.x", 36 | "@tinkoff/request-plugin-protocol-http": "^0.7.0" 37 | }, 38 | "module": "lib/index.es.js" 39 | } 40 | -------------------------------------------------------------------------------- /packages/plugin-prom-red-metrics/src/constants/metaTypes.ts: -------------------------------------------------------------------------------- 1 | export const TIMER_DONE = 'metrics_timerDone'; 2 | -------------------------------------------------------------------------------- /packages/plugin-prom-red-metrics/src/httpMetrics.spec.ts: -------------------------------------------------------------------------------- 1 | import { Context } from '@tinkoff/request-core'; 2 | import metrics from './httpMetrics'; 3 | 4 | describe('plugins/metrics/httpMetrics', () => { 5 | it('Pass http labelNames to metrics fabrics', () => { 6 | const counterFabric = jest.fn(); 7 | const histogramFabric = jest.fn(); 8 | const labelNames = ['method', 'url', 'status']; 9 | 10 | metrics({ 11 | metrics: { counter: counterFabric, histogram: histogramFabric }, 12 | }); 13 | 14 | counterFabric.mock.calls.forEach(([arg]) => expect(arg.labelNames).toEqual(labelNames)); 15 | histogramFabric.mock.calls.forEach(([arg]) => expect(arg.labelNames).toEqual(labelNames)); 16 | }); 17 | 18 | it('Get http labels values from context', () => { 19 | const counter = { inc: jest.fn() }; 20 | const counterFabric = jest.fn(() => counter); 21 | const timerDone = jest.fn(); 22 | const histogram = { startTimer: () => timerDone }; 23 | const histogramFabric = jest.fn(() => histogram); 24 | 25 | const plugin = metrics({ 26 | metrics: { counter: counterFabric, histogram: histogramFabric }, 27 | }); 28 | 29 | const context = new Context(); 30 | context.setState({ 31 | request: { 32 | url: 'foo', 33 | httpMethod: 'POST', 34 | }, 35 | }); 36 | context.updateInternalMeta('PROTOCOL_HTTP', { 37 | response: { 38 | status: '200', 39 | }, 40 | }); 41 | plugin.init(context, () => {}, null); 42 | plugin.complete(context, () => {}, null); 43 | 44 | const labelsValues = { 45 | url: 'foo', 46 | method: 'POST', 47 | status: '200', 48 | }; 49 | 50 | counter.inc.mock.calls.forEach(([arg]) => expect(arg).toEqual(labelsValues)); 51 | timerDone.mock.calls.forEach(([arg]) => expect(arg).toEqual(labelsValues)); 52 | }); 53 | 54 | it('Return default http labels values', () => { 55 | const counter = { inc: jest.fn() }; 56 | const counterFabric = jest.fn(() => counter); 57 | const timerDone = jest.fn(); 58 | const histogram = { startTimer: () => timerDone }; 59 | const histogramFabric = jest.fn(() => histogram); 60 | 61 | const plugin = metrics({ 62 | metrics: { counter: counterFabric, histogram: histogramFabric }, 63 | }); 64 | 65 | const context = new Context(); 66 | plugin.init(context, () => {}, null); 67 | plugin.complete(context, () => {}, null); 68 | 69 | const labelsValues = { 70 | url: 'unknown', 71 | status: 'unknown', 72 | method: 'GET', 73 | }; 74 | 75 | counter.inc.mock.calls.forEach(([arg]) => expect(arg).toEqual(labelsValues)); 76 | timerDone.mock.calls.forEach(([arg]) => expect(arg).toEqual(labelsValues)); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /packages/plugin-prom-red-metrics/src/httpMetrics.ts: -------------------------------------------------------------------------------- 1 | import { getStatus } from '@tinkoff/request-plugin-protocol-http'; 2 | import metricsPlugin from './metrics'; 3 | 4 | export const getHttpLabelsValues = (context) => { 5 | const { url = 'unknown', httpMethod = 'GET' } = context.getRequest() || {}; 6 | 7 | return { 8 | method: httpMethod.toUpperCase(), 9 | url, 10 | status: getStatus(context as any) || 'unknown', 11 | }; 12 | }; 13 | export const httpLabels = ['method', 'url', 'status']; 14 | 15 | export default (opts) => 16 | metricsPlugin( 17 | Object.assign( 18 | { 19 | labelNames: httpLabels, 20 | getLabelsValuesFromContext: getHttpLabelsValues, 21 | }, 22 | opts 23 | ) 24 | ); 25 | -------------------------------------------------------------------------------- /packages/plugin-prom-red-metrics/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './httpMetrics'; 2 | export { default as httpMetrics } from './httpMetrics'; 3 | export { default } from './metrics'; 4 | -------------------------------------------------------------------------------- /packages/plugin-prom-red-metrics/src/metrics.spec.ts: -------------------------------------------------------------------------------- 1 | import { Context } from '@tinkoff/request-core'; 2 | import { TIMER_DONE } from './constants/metaTypes'; 3 | import metrics from './metrics'; 4 | 5 | describe('plugins/metrics', () => { 6 | it('Returns empty object when metrics object not passed', () => { 7 | expect(metrics({ labelNames: [], getLabelsValuesFromContext: () => {} })).toEqual({}); 8 | }); 9 | 10 | describe('Set labels', () => { 11 | it('Pass labelNames to metrics fabrics', () => { 12 | const counterFabric = jest.fn(); 13 | const histogramFabric = jest.fn(); 14 | const labelNames = ['foo', 'bar', 'baz']; 15 | 16 | metrics({ 17 | metrics: { counter: counterFabric, histogram: histogramFabric }, 18 | labelNames, 19 | getLabelsValuesFromContext: () => {}, 20 | }); 21 | 22 | counterFabric.mock.calls.forEach(([arg]) => expect(arg.labelNames).toBe(labelNames)); 23 | histogramFabric.mock.calls.forEach(([arg]) => expect(arg.labelNames).toBe(labelNames)); 24 | }); 25 | 26 | ['complete', 'error'].forEach((method) => { 27 | it(`Pass labelValues from getLabelsValuesFromContext() to metrics instances, on ${method}`, () => { 28 | const counter = { inc: jest.fn() }; 29 | const counterFabric = () => counter; 30 | const timerDone = jest.fn(); 31 | const histogram = { startTimer: () => timerDone }; 32 | const histogramFabric = () => histogram; 33 | const labelNames = []; 34 | const labelsValues = { foo: 'bar', baz: 'quux' }; 35 | const getLabelsValuesFromContext = jest.fn(() => labelsValues); 36 | 37 | const plugin = metrics({ 38 | metrics: { counter: counterFabric, histogram: histogramFabric }, 39 | labelNames, 40 | getLabelsValuesFromContext, 41 | }); 42 | 43 | const context = new Context(); 44 | plugin.init(context, () => {}, null); 45 | plugin[method](context, () => {}, null); 46 | 47 | expect(getLabelsValuesFromContext).toHaveBeenCalledTimes(1); 48 | counter.inc.mock.calls.forEach(([arg]) => expect(arg).toBe(labelsValues)); 49 | timerDone.mock.calls.forEach(([arg]) => expect(arg).toBe(labelsValues)); 50 | }); 51 | }); 52 | }); 53 | 54 | describe('Lifecycle actions', () => { 55 | let context; 56 | let metricsStub; 57 | let counterInc; 58 | let timerDone; 59 | let startTimer; 60 | let next; 61 | 62 | beforeEach(() => { 63 | context = new Context(); 64 | next = jest.fn(); 65 | 66 | counterInc = jest.fn(); 67 | timerDone = jest.fn(); 68 | startTimer = jest.fn(() => timerDone); 69 | 70 | const counterFabric = () => ({ inc: counterInc }); 71 | const histogramFabric = () => ({ startTimer }); 72 | 73 | metricsStub = { 74 | counter: counterFabric, 75 | histogram: histogramFabric, 76 | }; 77 | }); 78 | 79 | it('Init', () => { 80 | const plugin = metrics({ metrics: metricsStub, labelNames: [], getLabelsValuesFromContext: () => {} }); 81 | 82 | plugin.init(context, next, null); 83 | 84 | expect(startTimer).toHaveBeenCalledTimes(1); 85 | expect(next).toHaveBeenCalledTimes(1); 86 | expect(context.getInternalMeta(TIMER_DONE)).toEqual({ timerDone }); 87 | }); 88 | 89 | it('Complete', () => { 90 | const plugin = metrics({ metrics: metricsStub, labelNames: [], getLabelsValuesFromContext: () => {} }); 91 | 92 | plugin.init(context, () => {}, null); 93 | plugin.complete(context, next, null); 94 | 95 | expect(timerDone).toHaveBeenCalledTimes(1); 96 | expect(counterInc).toHaveBeenCalledTimes(1); 97 | expect(next).toHaveBeenCalledTimes(1); 98 | }); 99 | 100 | it('Error', () => { 101 | const plugin = metrics({ metrics: metricsStub, labelNames: [], getLabelsValuesFromContext: () => {} }); 102 | 103 | plugin.init(context, () => {}, null); 104 | plugin.error(context, next, null); 105 | 106 | expect(timerDone).toHaveBeenCalledTimes(1); 107 | expect(counterInc).toHaveBeenCalledTimes(2); 108 | expect(next).toHaveBeenCalledTimes(1); 109 | }); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /packages/plugin-prom-red-metrics/src/metrics.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin } from '@tinkoff/request-core'; 2 | import { TIMER_DONE } from './constants/metaTypes'; 3 | 4 | export default ({ metrics = null, prefix = '', labelNames, getLabelsValuesFromContext }): Plugin => { 5 | if (!metrics) return {}; 6 | 7 | const addPrefix = (str) => (prefix ? `${prefix}_` : '') + str; 8 | 9 | const requestsCounter = metrics.counter({ 10 | name: addPrefix('sent_requests_total'), 11 | help: 'Number of requests sent', 12 | labelNames, 13 | }); 14 | const errorsCounter = metrics.counter({ 15 | name: addPrefix('sent_requests_errors'), 16 | help: 'Number of requests that failed', 17 | labelNames, 18 | }); 19 | const durationHistogram = metrics.histogram({ 20 | name: addPrefix('sent_requests_execution_time'), 21 | help: 'Execution time of the sent requests', 22 | labelNames, 23 | }); 24 | 25 | return { 26 | init: (context, next) => { 27 | context.updateInternalMeta(TIMER_DONE, { 28 | timerDone: durationHistogram.startTimer(), 29 | }); 30 | next(); 31 | }, 32 | complete: (context, next) => { 33 | const labels = getLabelsValuesFromContext(context); 34 | 35 | requestsCounter.inc(labels); 36 | context.getInternalMeta(TIMER_DONE).timerDone(labels); 37 | next(); 38 | }, 39 | error: (context, next) => { 40 | const labels = getLabelsValuesFromContext(context); 41 | 42 | errorsCounter.inc(labels); 43 | requestsCounter.inc(labels); 44 | context.getInternalMeta(TIMER_DONE).timerDone(labels); 45 | next(); 46 | }, 47 | }; 48 | }; 49 | -------------------------------------------------------------------------------- /packages/plugin-prom-red-metrics/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib" 5 | }, 6 | "include": [ 7 | "./src" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /packages/plugin-protocol-http/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | src/ 3 | -------------------------------------------------------------------------------- /packages/plugin-protocol-http/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tinkoff/request-plugin-protocol-http", 3 | "version": "0.11.9", 4 | "description": "Make requests using http(s) protocol", 5 | "main": "lib/index.js", 6 | "typings": "lib/index.d.ts", 7 | "browser": { 8 | "lib/fetch.js": "./lib/fetch.browser.js", 9 | "./lib/index.es.js": "./lib/index.browser.js" 10 | }, 11 | "sideEffects": false, 12 | "scripts": { 13 | "prepack": "tramvai-build --for-publish", 14 | "build": "tramvai-build", 15 | "tsc": "tsc", 16 | "watch": "tsc -w", 17 | "test": "echo \"Error: no test specified\" && exit 1" 18 | }, 19 | "keywords": [ 20 | "request" 21 | ], 22 | "author": "Oleg Sorokotyaga (http://tinkoff.ru)", 23 | "publishConfig": { 24 | "access": "public" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/Tinkoff/tinkoff-request/" 29 | }, 30 | "license": "ISC", 31 | "dependencies": { 32 | "@tinkoff/request-core": "^0.9.3", 33 | "@tinkoff/request-url-utils": "^0.9.3", 34 | "@tinkoff/utils": "^2.0.0", 35 | "abort-controller": "^3.0.0", 36 | "form-data": "^2.5.0", 37 | "node-fetch": "^2.6.1", 38 | "tslib": "^2.1.3" 39 | }, 40 | "devDependencies": { 41 | "@types/node": "^12.7.1", 42 | "jest-fetch-mock": "^2.1.2" 43 | }, 44 | "peerDependencies": { 45 | "@tinkoff/request-core": "0.x" 46 | }, 47 | "module": "lib/index.es.js" 48 | } 49 | -------------------------------------------------------------------------------- /packages/plugin-protocol-http/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const PROTOCOL_HTTP = 'PROTOCOL_HTTP'; 2 | 3 | export const HttpMethods = { 4 | GET: 'GET', 5 | POST: 'POST', 6 | PUT: 'PUT', 7 | DELETE: 'DELETE', 8 | HEAD: 'HEAD', 9 | PATCH: 'PATCH', 10 | } as const; 11 | 12 | export const REQUEST_TYPES = { 13 | html: 'text/html', 14 | json: 'application/json', 15 | xml: 'text/xml', 16 | urlencoded: 'application/x-www-form-urlencoded', 17 | form: 'application/x-www-form-urlencoded', 18 | 'form-data': 'application/x-www-form-urlencoded', 19 | 'multipart/form-data': '', 20 | }; 21 | -------------------------------------------------------------------------------- /packages/plugin-protocol-http/src/errors.ts: -------------------------------------------------------------------------------- 1 | import { RequestError, RequestErrorCode } from '@tinkoff/request-core'; 2 | 3 | export class TimeoutError extends Error implements RequestError { 4 | code: RequestErrorCode['ERR_HTTP_REQUEST_TIMEOUT'] = 'ERR_HTTP_REQUEST_TIMEOUT'; 5 | 6 | message = 'Request timed out'; 7 | } 8 | 9 | export class HttpRequestError extends Error implements RequestError { 10 | code: RequestErrorCode['ERR_HTTP_ERROR'] = 'ERR_HTTP_ERROR'; 11 | 12 | status: number; 13 | 14 | body: any; 15 | } 16 | 17 | export class AbortError extends Error implements RequestError { 18 | code: RequestErrorCode['ABORT_ERR'] = 'ABORT_ERR'; 19 | } 20 | -------------------------------------------------------------------------------- /packages/plugin-protocol-http/src/fetch.browser.ts: -------------------------------------------------------------------------------- 1 | // ref: https://github.com/tc39/proposal-global 2 | const getGlobal = function () { 3 | // the only reliable means to get the global object is 4 | // `Function('return this')()` 5 | // However, this causes CSP violations in Chrome apps. 6 | if (typeof self !== 'undefined') { 7 | return self; 8 | } 9 | if (typeof window !== 'undefined') { 10 | return window; 11 | } 12 | if (typeof glob !== 'undefined') { 13 | return glob; 14 | } 15 | throw new Error('unable to locate global object'); 16 | }; 17 | 18 | const glob = getGlobal(); 19 | 20 | const fetch = (...args) => { 21 | return glob.fetch(...args); 22 | }; 23 | 24 | const { Headers, Request, Response } = glob; 25 | 26 | export { fetch, Headers, Request, Response }; 27 | -------------------------------------------------------------------------------- /packages/plugin-protocol-http/src/fetch.ts: -------------------------------------------------------------------------------- 1 | import fetch, { Headers, Request, Response } from 'node-fetch'; 2 | 3 | export { fetch, Headers, Response, Request }; 4 | -------------------------------------------------------------------------------- /packages/plugin-protocol-http/src/form.spec.ts: -------------------------------------------------------------------------------- 1 | import createForm from './form'; 2 | 3 | describe('plugins/http/form', () => { 4 | it('should transform payload into multi-part form-encoded view', () => { 5 | const form = createForm({ a: 1, b: [1, 2, 3], c: 'test' }); 6 | const boundary = form.getBoundary(); 7 | 8 | expect(form.getHeaders()).toEqual({ 9 | 'content-type': expect.stringContaining('multipart/form-data'), 10 | }); 11 | 12 | expect(form.getBuffer().toString().replace(/\r\n/g, '\n')).toEqual(`--${boundary} 13 | Content-Disposition: form-data; name="a" 14 | 15 | 1 16 | --${boundary} 17 | Content-Disposition: form-data; name="b" 18 | 19 | 1 20 | --${boundary} 21 | Content-Disposition: form-data; name="b" 22 | 23 | 2 24 | --${boundary} 25 | Content-Disposition: form-data; name="b" 26 | 27 | 3 28 | --${boundary} 29 | Content-Disposition: form-data; name="c" 30 | 31 | test 32 | --${boundary}-- 33 | `); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /packages/plugin-protocol-http/src/form.ts: -------------------------------------------------------------------------------- 1 | import each from '@tinkoff/utils/array/each'; 2 | import eachObj from '@tinkoff/utils/object/each'; 3 | import isArray from '@tinkoff/utils/is/array'; 4 | import isNil from '@tinkoff/utils/is/nil'; 5 | 6 | import FormData from 'form-data'; 7 | 8 | export default (payload, attaches = []) => { 9 | const form = new FormData(); 10 | 11 | const setField = (value, name) => { 12 | if (isArray(value)) { 13 | return each((f) => setField(f, name), value); 14 | } 15 | 16 | if (isNil(value)) { 17 | return; 18 | } 19 | 20 | form.append(name, value); 21 | }; 22 | 23 | eachObj(setField, payload); 24 | 25 | attaches.forEach((file) => { 26 | if (!(file instanceof window.Blob)) { 27 | return; 28 | } 29 | 30 | const fileUploadName = (file as any).uploadName || (file as any).name; 31 | const fileFieldName = (file as any).fieldName || 'file'; 32 | 33 | form.append(fileFieldName, file, encodeURIComponent(fileUploadName)); 34 | }); 35 | 36 | return form; 37 | }; 38 | -------------------------------------------------------------------------------- /packages/plugin-protocol-http/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './utils'; 2 | export * from './validators'; 3 | export { HttpMethods } from './constants'; 4 | export { default } from './http'; 5 | export type { Query, QuerySerializer } from '@tinkoff/request-url-utils'; 6 | -------------------------------------------------------------------------------- /packages/plugin-protocol-http/src/parse.spec.ts: -------------------------------------------------------------------------------- 1 | import { Response } from './fetch'; 2 | import parse from './parse'; 3 | 4 | describe('plugins/http/url', () => { 5 | it('for application/json should call json() on response', async () => { 6 | const res = new Response('{"test": 1}', { 7 | status: 200, 8 | headers: { 9 | 'Content-type': 'application/json', 10 | }, 11 | }); 12 | 13 | const json = jest.spyOn(res, 'json'); 14 | const text = jest.spyOn(res, 'text'); 15 | 16 | expect(await parse(res)).toEqual({ test: 1 }); 17 | 18 | expect(json).toHaveBeenCalled(); 19 | expect(text).not.toHaveBeenCalled(); 20 | }); 21 | 22 | it('otherwise should call text()', async () => { 23 | const res = new Response('{"test": 1}', { 24 | status: 200, 25 | headers: {}, 26 | }); 27 | 28 | const json = jest.spyOn(res, 'json'); 29 | const text = jest.spyOn(res, 'text'); 30 | 31 | expect(await parse(res)).toEqual('{"test": 1}'); 32 | 33 | expect(json).not.toHaveBeenCalled(); 34 | expect(text).toHaveBeenCalled(); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /packages/plugin-protocol-http/src/parse.ts: -------------------------------------------------------------------------------- 1 | import toLower from '@tinkoff/utils/string/toLower'; 2 | import includes from '@tinkoff/utils/array/includes'; 3 | 4 | export default (res: Response) => { 5 | const type = toLower(res.headers.get('content-type') || ''); 6 | 7 | if (includes('application/json', type)) { 8 | return res.json(); 9 | } 10 | 11 | if (includes('application/octet-stream', type)) { 12 | return res.arrayBuffer(); 13 | } 14 | 15 | return res.text(); 16 | }; 17 | -------------------------------------------------------------------------------- /packages/plugin-protocol-http/src/serialize.spec.ts: -------------------------------------------------------------------------------- 1 | import { serialize } from './serialize'; 2 | 3 | describe('plugins/http/serialize', () => { 4 | it('should encode query url if type is form', () => { 5 | const payload = { a: 1, b: 2, c: 'abc' }; 6 | 7 | expect(serialize('form', payload)).toMatchInlineSnapshot(`"a=1&b=2&c=abc"`); 8 | expect(serialize('urlencoded', payload)).toMatchInlineSnapshot(`"a=1&b=2&c=abc"`); 9 | expect(serialize('form-data', payload)).toMatchInlineSnapshot(`"a=1&b=2&c=abc"`); 10 | }); 11 | 12 | it('should converst to json', () => { 13 | const payload = { a: 1, b: 2, c: 'abc' }; 14 | 15 | expect(serialize('json', payload)).toMatchInlineSnapshot(`"{\\"a\\":1,\\"b\\":2,\\"c\\":\\"abc\\"}"`); 16 | }); 17 | 18 | it('should return payload as is otherwise', () => { 19 | const payload = { a: 1, b: 2, c: 'abc' }; 20 | 21 | expect(serialize('multipart/form-data', payload)).toBe(payload); 22 | expect(serialize('unknown', payload)).toBe(payload); 23 | expect(serialize('html', payload)).toBe(payload); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /packages/plugin-protocol-http/src/serialize.ts: -------------------------------------------------------------------------------- 1 | import { serializeQuery } from '@tinkoff/request-url-utils'; 2 | 3 | export const serialize = (type: string, payload, querySerializer = serializeQuery) => { 4 | switch (type) { 5 | case 'form': 6 | case 'urlencoded': 7 | case 'form-data': 8 | return querySerializer(payload); 9 | case 'json': 10 | return JSON.stringify(payload); 11 | default: 12 | return payload; 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /packages/plugin-protocol-http/src/utils.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | import { MakeRequestResult } from '@tinkoff/request-core'; 5 | import { Headers } from './fetch'; 6 | import { PROTOCOL_HTTP } from './constants'; 7 | import { abort, getHeader, getHeaders, getStatus } from './utils'; 8 | 9 | const headers = new Headers({ 10 | a: 'aaa', 11 | b: 'bbb', 12 | }); 13 | 14 | headers.append('set-cookie', 'cookie1'); 15 | headers.append('set-cookie', 'cookie2'); 16 | 17 | describe('plugins/http/utils', () => { 18 | const requestAbort = jest.fn(); 19 | const response = { 20 | headers, 21 | status: 202, 22 | }; 23 | let result: MakeRequestResult; 24 | 25 | beforeEach(() => { 26 | result = { 27 | getState: jest.fn(), 28 | getInternalMeta: jest.fn(() => { 29 | return { 30 | requestAbort, 31 | response, 32 | }; 33 | }), 34 | } as any; 35 | }); 36 | 37 | it('get headers', () => { 38 | expect(getHeaders(result)).toEqual({ a: 'aaa', b: 'bbb', 'set-cookie': ['cookie1', 'cookie2'] }); 39 | expect(result.getInternalMeta).toHaveBeenCalledWith(PROTOCOL_HTTP); 40 | }); 41 | 42 | it('get header', () => { 43 | expect(getHeader(result, 'a')).toBe('aaa'); 44 | expect(getHeader(result, 'b')).toBe('bbb'); 45 | expect(getHeader(result, 'c')).toBeNull(); 46 | expect(getHeader(result, 'set-cookie')).toEqual(['cookie1', 'cookie2']); 47 | }); 48 | 49 | it('get status', () => { 50 | expect(getStatus(result)).toEqual(202); 51 | }); 52 | 53 | it('abort request', () => { 54 | expect(requestAbort).not.toHaveBeenCalled(); 55 | abort(result); 56 | expect(requestAbort).toHaveBeenCalled(); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /packages/plugin-protocol-http/src/utils.ts: -------------------------------------------------------------------------------- 1 | import { MakeRequestResult } from '@tinkoff/request-core'; 2 | import prop from '@tinkoff/utils/object/prop'; 3 | import { PROTOCOL_HTTP } from './constants'; 4 | 5 | const getSetCookieHeader = (headers) => { 6 | if (typeof window === 'undefined') { 7 | return headers.raw()['set-cookie']; // node-fetch specific api, see https://github.com/bitinn/node-fetch#extract-set-cookie-header 8 | } 9 | 10 | return []; // browser doesn't provide set-cookie header, just return empty array for compatibility 11 | }; 12 | 13 | // TODO: when some plugins (for example cache) break flow, plugin-http won't be called and meta will be empty 14 | export const _getResponse = (request: MakeRequestResult): Response => { 15 | const meta = request.getInternalMeta(PROTOCOL_HTTP); 16 | 17 | return meta && meta.response; 18 | }; 19 | 20 | const _getHeaders = (request: MakeRequestResult) => { 21 | return prop('headers', _getResponse(request)); 22 | }; 23 | 24 | export const getHeaders = (request: MakeRequestResult) => { 25 | const headers = _getHeaders(request); 26 | const result = {}; 27 | 28 | if (headers) { 29 | headers.forEach((v, k) => { 30 | if (k === 'set-cookie') { 31 | result[k] = getSetCookieHeader(headers); 32 | } else { 33 | result[k] = v; 34 | } 35 | }); 36 | } 37 | 38 | return result; 39 | }; 40 | 41 | export const getHeader = (request: MakeRequestResult, header: string) => { 42 | const headers = _getHeaders(request); 43 | 44 | if (headers) { 45 | if (header === 'set-cookie') { 46 | return getSetCookieHeader(headers); 47 | } 48 | 49 | return headers.get(header); 50 | } 51 | }; 52 | 53 | export const getStatus = (request: MakeRequestResult) => { 54 | return prop('status', _getResponse(request)); 55 | }; 56 | 57 | export const abort = (request: MakeRequestResult) => { 58 | const meta = request.getInternalMeta(PROTOCOL_HTTP); 59 | 60 | return meta && meta.requestAbort(); 61 | }; 62 | -------------------------------------------------------------------------------- /packages/plugin-protocol-http/src/validators.ts: -------------------------------------------------------------------------------- 1 | import { RequestError } from '@tinkoff/request-core'; 2 | import { HttpRequestError, TimeoutError, AbortError } from './errors'; 3 | 4 | export const isHttpError = (error: RequestError): error is HttpRequestError => { 5 | return error.code === 'ERR_HTTP_ERROR'; 6 | }; 7 | 8 | export const isTimeoutError = (error: RequestError): error is TimeoutError => { 9 | return error.code === 'ERR_HTTP_REQUEST_TIMEOUT'; 10 | }; 11 | 12 | export const isAbortError = (error: RequestError): error is AbortError => { 13 | return error.code === 'ABORT_ERR'; 14 | }; 15 | 16 | export const isNetworkFail = (error: RequestError) => { 17 | return isTimeoutError(error) || (isHttpError(error) && !error.status); 18 | }; 19 | 20 | export const isServerError = (error: RequestError) => { 21 | return isHttpError(error) && error.status >= 500; 22 | }; 23 | -------------------------------------------------------------------------------- /packages/plugin-protocol-http/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib" 5 | }, 6 | "include": [ 7 | "./src" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /packages/plugin-protocol-jsonp/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | src/ 3 | -------------------------------------------------------------------------------- /packages/plugin-protocol-jsonp/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [0.2.5](https://github.com/Tinkoff/tinkoff-request/compare/@tinkoff/request-plugin-protocol-jsonp@0.2.4...@tinkoff/request-plugin-protocol-jsonp@0.2.5) (2023-08-02) 7 | 8 | **Note:** Version bump only for package @tinkoff/request-plugin-protocol-jsonp 9 | 10 | 11 | 12 | 13 | 14 | ## [0.2.4](https://github.com/Tinkoff/tinkoff-request/compare/@tinkoff/request-plugin-protocol-jsonp@0.2.4...@tinkoff/request-plugin-protocol-jsonp@0.2.4) (2023-07-14) 15 | 16 | 17 | ### Bug Fixes 18 | 19 | * extend typings ([1fcc2cb](https://github.com/Tinkoff/tinkoff-request/commit/1fcc2cb32597b10d788de36303507e385042fc96)) 20 | 21 | 22 | ### Features 23 | 24 | * **protocol-jsonp:** add plugin for making jsonp requests ([ba18859](https://github.com/Tinkoff/tinkoff-request/commit/ba188599377436ba4814d16cf3f0d47c1cf0eaac)) 25 | * **protocol-jsonp:** add possibility to specify custom querySerializer ([5c1d92a](https://github.com/Tinkoff/tinkoff-request/commit/5c1d92a439d28969713d537fb04edaf6318334e4)) 26 | * **url-utils:** move url utils to separate package ([1ab2397](https://github.com/Tinkoff/tinkoff-request/commit/1ab239709142460ac5cdacfb93714ad5a0e7d277)) 27 | * use @tramvai/build as builder to provide modern es version ([3a26224](https://github.com/Tinkoff/tinkoff-request/commit/3a26224221d4fc073938cf32c2f147515620c28e)) 28 | -------------------------------------------------------------------------------- /packages/plugin-protocol-jsonp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tinkoff/request-plugin-protocol-jsonp", 3 | "version": "0.2.5", 4 | "description": "Make a jsonp request", 5 | "main": "lib/noop.js", 6 | "browser": "lib/jsonp.js", 7 | "typings": "lib/jsonp.d.ts", 8 | "sideEffects": false, 9 | "scripts": { 10 | "prepack": "tramvai-build --for-publish", 11 | "build": "tramvai-build", 12 | "tsc": "tsc", 13 | "watch": "tsc -w", 14 | "test": "echo \"Error: no test specified\" && exit 1" 15 | }, 16 | "keywords": [ 17 | "request" 18 | ], 19 | "author": "Kirill Meleshko (http://tinkoff.ru)", 20 | "publishConfig": { 21 | "access": "public" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/Tinkoff/tinkoff-request/" 26 | }, 27 | "license": "ISC", 28 | "dependencies": { 29 | "@tinkoff/request-core": "^0.9.3", 30 | "@tinkoff/request-url-utils": "^0.9.3", 31 | "@tinkoff/utils": "^2.0.0", 32 | "fetch-jsonp": "^1.1.3", 33 | "tslib": "^2.1.3" 34 | }, 35 | "devDependencies": { 36 | "@types/node": "^12.7.1" 37 | }, 38 | "peerDependencies": { 39 | "@tinkoff/request-core": "0.x" 40 | }, 41 | "module": "lib/noop.es.js" 42 | } 43 | -------------------------------------------------------------------------------- /packages/plugin-protocol-jsonp/src/jsonp.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | import { Context } from '@tinkoff/request-core'; 5 | import jsonp from './jsonp'; 6 | 7 | const mockJsonp = jest.fn((...args) => Promise.resolve()); 8 | jest.mock( 9 | 'fetch-jsonp', 10 | () => 11 | (...args) => 12 | mockJsonp(...args) 13 | ); 14 | 15 | const plugin = jsonp(); 16 | const next = jest.fn(); 17 | 18 | describe('plugins/jsonp', () => { 19 | beforeEach(() => { 20 | mockJsonp.mockClear(); 21 | next.mockClear(); 22 | }); 23 | 24 | it('test jsonp option', async () => { 25 | const jsonpObject = {}; 26 | 27 | plugin.init!( 28 | new Context({ 29 | request: { 30 | url: 'test', 31 | jsonp: jsonpObject, 32 | }, 33 | }), 34 | next, 35 | null as any 36 | ); 37 | 38 | await new Promise((res) => { 39 | next.mockImplementation(res); 40 | }); 41 | 42 | expect(mockJsonp).toBeCalledWith('test', jsonpObject); 43 | }); 44 | 45 | it('test with custom querySerializer', async () => { 46 | const mockQuerySerializer = jest.fn(() => 'query-string'); 47 | 48 | const plugin = jsonp({}, { querySerializer: mockQuerySerializer }); 49 | 50 | plugin.init!( 51 | new Context({ request: { url: 'http://test.com/api?test=123', query: { a: '1' } } }), 52 | next, 53 | null as any 54 | ); 55 | 56 | await new Promise((res) => { 57 | next.mockImplementation(res); 58 | }); 59 | 60 | expect(mockQuerySerializer).toHaveBeenCalledWith({ a: '1' }, 'test=123'); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /packages/plugin-protocol-jsonp/src/jsonp.ts: -------------------------------------------------------------------------------- 1 | import fetchJsonp from 'fetch-jsonp'; 2 | 3 | import type { Plugin } from '@tinkoff/request-core'; 4 | import { Status } from '@tinkoff/request-core'; 5 | import type { Query, QuerySerializer } from '@tinkoff/request-url-utils'; 6 | import { addQuery } from '@tinkoff/request-url-utils'; 7 | 8 | declare module '@tinkoff/request-core/lib/types.h' { 9 | export interface Request { 10 | url?: string; 11 | query?: Query; 12 | queryNoCache?: Query; 13 | jsonp?: fetchJsonp.Options; 14 | } 15 | } 16 | 17 | let isPageUnloaded = false; 18 | 19 | window.addEventListener('beforeunload', () => { 20 | isPageUnloaded = true; 21 | }); 22 | 23 | /** 24 | * Makes jsonp request. 25 | * Uses `fetch-jsonp` library. 26 | * 27 | * requestParams: 28 | * url {string} 29 | * query {object} 30 | * queryNoCache {object} - query which wont be used in generating cache key 31 | * jsonp {object} - configuration for `fetch-jsonp` 32 | * 33 | * @param {object} jsonOptions configuration for `fetch-jsonp` 34 | * @param {QuerySerializer} querySerializer function that will be used instead of default value to serialize query strings in url 35 | * @return {{init: init}} 36 | */ 37 | export default ( 38 | jsonpOptions: fetchJsonp.Options = {}, 39 | { 40 | querySerializer, 41 | }: { 42 | querySerializer?: QuerySerializer; 43 | } = {} 44 | ): Plugin => { 45 | return { 46 | init: (context, next) => { 47 | const { url, query, queryNoCache, jsonp } = context.getRequest(); 48 | let ended = false; 49 | 50 | fetchJsonp( 51 | addQuery( 52 | url, 53 | { 54 | ...queryNoCache, 55 | ...query, 56 | }, 57 | querySerializer 58 | ), 59 | { 60 | ...jsonpOptions, 61 | ...jsonp, 62 | } 63 | ) 64 | .then((res) => res.json()) 65 | .then((response) => { 66 | if (ended) { 67 | return; 68 | } 69 | 70 | next({ 71 | status: Status.COMPLETE, 72 | response: response, 73 | }); 74 | }) 75 | .catch((err) => { 76 | if (ended || (err && isPageUnloaded)) { 77 | return; 78 | } 79 | 80 | next({ 81 | status: Status.ERROR, 82 | error: err, 83 | }); 84 | }) 85 | .then(() => { 86 | ended = true; 87 | }); 88 | }, 89 | }; 90 | }; 91 | -------------------------------------------------------------------------------- /packages/plugin-protocol-jsonp/src/noop.ts: -------------------------------------------------------------------------------- 1 | import always from '@tinkoff/utils/function/always'; 2 | import jsonp from './jsonp'; 3 | 4 | export default always({}) as typeof jsonp; 5 | -------------------------------------------------------------------------------- /packages/plugin-protocol-jsonp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib" 5 | }, 6 | "include": [ 7 | "./src" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /packages/plugin-retry/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | src/ 3 | -------------------------------------------------------------------------------- /packages/plugin-retry/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [0.2.4](https://github.com/Tinkoff/tinkoff-request/compare/@tinkoff/request-plugin-retry@0.2.3...@tinkoff/request-plugin-retry@0.2.4) (2023-08-02) 7 | 8 | **Note:** Version bump only for package @tinkoff/request-plugin-retry 9 | 10 | 11 | 12 | 13 | 14 | ## [0.2.3](https://github.com/Tinkoff/tinkoff-request/compare/@tinkoff/request-plugin-retry@0.2.3...@tinkoff/request-plugin-retry@0.2.3) (2023-07-14) 15 | 16 | 17 | ### Bug Fixes 18 | 19 | * extend typings ([1fcc2cb](https://github.com/Tinkoff/tinkoff-request/commit/1fcc2cb32597b10d788de36303507e385042fc96)) 20 | * **retry:** prevent retry requests from duplicating caching ([8db5a52](https://github.com/Tinkoff/tinkoff-request/commit/8db5a5223ccfd97c74e70df0c1a523e7dfd42b45)) 21 | 22 | 23 | ### Features 24 | 25 | * **retry:** add plugin for retry failed requests ([#42](https://github.com/Tinkoff/tinkoff-request/issues/42)) ([ff0e04e](https://github.com/Tinkoff/tinkoff-request/commit/ff0e04e933b46a8bdad488bf8882f0c0d19b6cb9)) 26 | * use @tramvai/build as builder to provide modern es version ([3a26224](https://github.com/Tinkoff/tinkoff-request/commit/3a26224221d4fc073938cf32c2f147515620c28e)) 27 | -------------------------------------------------------------------------------- /packages/plugin-retry/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tinkoff/request-plugin-retry", 3 | "version": "0.2.4", 4 | "description": "Retries failed requests", 5 | "main": "lib/retry.js", 6 | "typings": "lib/retry.d.ts", 7 | "sideEffects": false, 8 | "scripts": { 9 | "prepack": "tramvai-build --for-publish", 10 | "build": "tramvai-build", 11 | "tsc": "tsc", 12 | "watch": "tsc -w", 13 | "test": "echo \"Error: no test specified\" && exit 1" 14 | }, 15 | "keywords": [ 16 | "request" 17 | ], 18 | "author": "Kirill Meleshko ", 19 | "publishConfig": { 20 | "access": "public" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/Tinkoff/tinkoff-request/" 25 | }, 26 | "license": "ISC", 27 | "dependencies": { 28 | "@tinkoff/utils": "^2.0.0", 29 | "tslib": "^2.1.3" 30 | }, 31 | "devDependencies": { 32 | "@tinkoff/request-core": "^0.9.3" 33 | }, 34 | "peerDependencies": { 35 | "@tinkoff/request-core": "0.x" 36 | }, 37 | "module": "lib/retry.es.js" 38 | } 39 | -------------------------------------------------------------------------------- /packages/plugin-retry/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const RETRY_META = 'retry'; 2 | -------------------------------------------------------------------------------- /packages/plugin-retry/src/retry.ts: -------------------------------------------------------------------------------- 1 | import applyOrReturn from '@tinkoff/utils/function/applyOrReturn'; 2 | import type { Plugin } from '@tinkoff/request-core'; 3 | import { Status } from '@tinkoff/request-core'; 4 | import { RETRY_META } from './constants'; 5 | 6 | declare module '@tinkoff/request-core/lib/types.h' { 7 | export interface Request { 8 | retry?: number; 9 | retryDelay?: number | ((attempt: number) => number); 10 | } 11 | } 12 | 13 | /** 14 | * Retries failed requests 15 | * 16 | * requestParams: 17 | * retry {number} - number of attempts to execute failed request 18 | * retryDelay {number | Function} - time in ms to wait before execute new attempt 19 | * 20 | * metaInfo: 21 | * retry.attempts {number} - number of executed attempts 22 | * 23 | * @param {number} [retry = 0] - number of attempts to execute failed request 24 | * @param {number | Function} [retryDelay = 100] - time in ms to wait before execute new attempt 25 | * @param {number} [maxTimeout = 60000] - final timeout for complete request including retry attempts 26 | * @return {{init: init, error: error}} 27 | */ 28 | export default ({ 29 | retry = 0, 30 | retryDelay = 100, 31 | maxTimeout = 60000, 32 | }: { 33 | retry?: number; 34 | retryDelay?: number | ((attempt: number) => number); 35 | maxTimeout?: number; 36 | } = {}): Plugin => { 37 | return { 38 | init: (context, next) => { 39 | context.updateInternalMeta(RETRY_META, { 40 | startTime: Date.now(), 41 | }); 42 | 43 | next(); 44 | }, 45 | 46 | error: (context, next, makeRequest) => { 47 | const request = context.getRequest(); 48 | const timeout = request.timeout || maxTimeout; 49 | const attemptsMax = request.retry ?? retry; 50 | const delay = request.retryDelay ?? retryDelay; 51 | const { startTime } = context.getInternalMeta(RETRY_META); 52 | let attempt = 0; 53 | 54 | const doRequest = () => { 55 | if (attempt < attemptsMax) { 56 | setTimeout(() => { 57 | const diffTime = Date.now() - startTime; 58 | 59 | if (diffTime >= timeout) { 60 | context.updateExternalMeta(RETRY_META, { 61 | attempts: attempt, 62 | timeout: true, 63 | }); 64 | 65 | return next(); 66 | } 67 | 68 | makeRequest({ 69 | ...request, 70 | retry: 0, 71 | timeout: timeout - diffTime, 72 | silent: true, 73 | retryDelay: undefined, 74 | // to prevent deduplicating new request with the initial one 75 | // all of the caching will still be happen for initial request on complete 76 | cache: false, 77 | }).then( 78 | (response) => { 79 | context.updateExternalMeta(RETRY_META, { 80 | attempts: attempt + 1, 81 | }); 82 | 83 | return next({ 84 | status: Status.COMPLETE, 85 | response, 86 | }); 87 | }, 88 | () => { 89 | attempt++; 90 | doRequest(); 91 | } 92 | ); 93 | }, applyOrReturn([attempt], delay) as number); 94 | } else { 95 | if (attemptsMax > 0) { 96 | context.updateExternalMeta(RETRY_META, { 97 | attempts: attempt, 98 | timeout: false, 99 | }); 100 | } 101 | 102 | next(); 103 | } 104 | }; 105 | 106 | doRequest(); 107 | }, 108 | }; 109 | }; 110 | -------------------------------------------------------------------------------- /packages/plugin-retry/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib" 5 | }, 6 | "include": [ 7 | "./src" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /packages/plugin-transform-url/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | src/ 3 | -------------------------------------------------------------------------------- /packages/plugin-transform-url/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [0.9.3](https://github.com/Tinkoff/tinkoff-request/compare/@tinkoff/request-plugin-transform-url@0.9.2...@tinkoff/request-plugin-transform-url@0.9.3) (2023-08-02) 7 | 8 | **Note:** Version bump only for package @tinkoff/request-plugin-transform-url 9 | 10 | 11 | 12 | 13 | 14 | ## [0.9.2](https://github.com/Tinkoff/tinkoff-request/compare/@tinkoff/request-plugin-transform-url@0.9.2...@tinkoff/request-plugin-transform-url@0.9.2) (2023-07-14) 15 | 16 | 17 | ### Bug Fixes 18 | 19 | * extend typings ([1fcc2cb](https://github.com/Tinkoff/tinkoff-request/commit/1fcc2cb32597b10d788de36303507e385042fc96)) 20 | 21 | 22 | ### Features 23 | 24 | * use @tramvai/build as builder to provide modern es version ([3a26224](https://github.com/Tinkoff/tinkoff-request/commit/3a26224221d4fc073938cf32c2f147515620c28e)) 25 | -------------------------------------------------------------------------------- /packages/plugin-transform-url/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tinkoff/request-plugin-transform-url", 3 | "version": "0.9.3", 4 | "description": "Transforms requested url", 5 | "main": "lib/url.js", 6 | "typings": "lib/url.d.ts", 7 | "sideEffects": false, 8 | "scripts": { 9 | "prepack": "tramvai-build --for-publish", 10 | "build": "tramvai-build", 11 | "tsc": "tsc", 12 | "watch": "tsc -w", 13 | "test": "echo \"Error: no test specified\" && exit 1" 14 | }, 15 | "keywords": [ 16 | "request" 17 | ], 18 | "author": "Oleg Sorokotyaga (http://tinkoff.ru)", 19 | "publishConfig": { 20 | "access": "public" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/Tinkoff/tinkoff-request/" 25 | }, 26 | "license": "ISC", 27 | "devDependencies": { 28 | "@tinkoff/request-core": "^0.9.3" 29 | }, 30 | "peerDependencies": { 31 | "@tinkoff/request-core": "0.x" 32 | }, 33 | "dependencies": { 34 | "tslib": "^2.1.3" 35 | }, 36 | "module": "lib/url.es.js" 37 | } 38 | -------------------------------------------------------------------------------- /packages/plugin-transform-url/src/url.spec.ts: -------------------------------------------------------------------------------- 1 | import { Context } from '@tinkoff/request-core'; 2 | import url from './url'; 3 | 4 | const baseUrl = 'global/'; 5 | const transform = jest.fn(({ baseUrl, url }) => baseUrl + url); 6 | const plugin = url({ baseUrl, transform }); 7 | const next = jest.fn(); 8 | 9 | describe('plugins/transform/url', () => { 10 | beforeEach(() => { 11 | next.mockClear(); 12 | }); 13 | 14 | it('baseUrl is not passed', () => { 15 | const request = { url: 'test12313123/weawe' }; 16 | const context = new Context({ request }); 17 | 18 | plugin.init(context, next, null); 19 | 20 | expect(transform).toHaveBeenCalledWith({ 21 | ...request, 22 | baseUrl, 23 | }); 24 | expect(next).toHaveBeenCalledWith({ 25 | request: { 26 | ...request, 27 | url: baseUrl + request.url, 28 | }, 29 | }); 30 | }); 31 | 32 | it('baseUrl passed', () => { 33 | const request = { baseUrl: 'hahah.cm/', url: 'test12313123/weawe' }; 34 | const context = new Context({ request }); 35 | 36 | plugin.init(context, next, null); 37 | 38 | expect(transform).toHaveBeenCalledWith(request); 39 | expect(next).toHaveBeenCalledWith({ 40 | request: { 41 | ...request, 42 | url: request.baseUrl + request.url, 43 | }, 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /packages/plugin-transform-url/src/url.ts: -------------------------------------------------------------------------------- 1 | import type { Plugin, Request } from '@tinkoff/request-core'; 2 | 3 | interface Transform { 4 | (params: Request & { baseUrl: string }): string; 5 | } 6 | 7 | interface Params { 8 | baseUrl?: string; 9 | transform?: Transform; 10 | } 11 | 12 | declare module '@tinkoff/request-core/lib/types.h' { 13 | export interface Request { 14 | baseUrl?: string; 15 | } 16 | } 17 | 18 | /** 19 | * Transforms request url using passed function. 20 | * 21 | * @param baseUrl {string} 22 | * @param transform {Function} 23 | */ 24 | export default ({ baseUrl = '', transform = ({ baseUrl, url }) => `${baseUrl}${url}` }: Params = {}): Plugin => { 25 | return { 26 | init: (context, next) => { 27 | const request = context.getRequest(); 28 | 29 | next({ 30 | request: { 31 | ...request, 32 | url: transform({ ...request, baseUrl: request.baseUrl || baseUrl }), 33 | }, 34 | }); 35 | }, 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /packages/plugin-transform-url/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib" 5 | }, 6 | "include": [ 7 | "./src" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /packages/plugin-validate/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | src/ 3 | -------------------------------------------------------------------------------- /packages/plugin-validate/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [0.9.3](https://github.com/Tinkoff/tinkoff-request/compare/@tinkoff/request-plugin-validate@0.9.2...@tinkoff/request-plugin-validate@0.9.3) (2023-08-02) 7 | 8 | **Note:** Version bump only for package @tinkoff/request-plugin-validate 9 | 10 | 11 | 12 | 13 | 14 | ## [0.9.2](https://github.com/Tinkoff/tinkoff-request/compare/@tinkoff/request-plugin-validate@0.9.2...@tinkoff/request-plugin-validate@0.9.2) (2023-07-14) 15 | 16 | 17 | ### Features 18 | 19 | * **plugin-validate:** add errorValidator option ([#6](https://github.com/Tinkoff/tinkoff-request/issues/6)) ([e01772b](https://github.com/Tinkoff/tinkoff-request/commit/e01772bdc31cc5a3f6b9b5c7d90a4a16d257a6cf)) 20 | * split context.meta into context.internalMeta and context.externalMeta ([31f00e0](https://github.com/Tinkoff/tinkoff-request/commit/31f00e0ae14767f213a67eb2df349c9f75adcfe7)) 21 | * use @tramvai/build as builder to provide modern es version ([3a26224](https://github.com/Tinkoff/tinkoff-request/commit/3a26224221d4fc073938cf32c2f147515620c28e)) 22 | -------------------------------------------------------------------------------- /packages/plugin-validate/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tinkoff/request-plugin-validate", 3 | "version": "0.9.3", 4 | "description": "Validate your responses", 5 | "main": "lib/validate.js", 6 | "typings": "lib/validate.d.ts", 7 | "sideEffects": false, 8 | "scripts": { 9 | "prepack": "tramvai-build --for-publish", 10 | "build": "tramvai-build", 11 | "tsc": "tsc", 12 | "watch": "tsc -w", 13 | "test": "echo \"Error: no test specified\" && exit 1" 14 | }, 15 | "keywords": [ 16 | "request" 17 | ], 18 | "author": "Oleg Sorokotyaga (http://tinkoff.ru)", 19 | "publishConfig": { 20 | "access": "public" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/Tinkoff/tinkoff-request/" 25 | }, 26 | "license": "ISC", 27 | "dependencies": { 28 | "@tinkoff/utils": "^2.0.0", 29 | "tslib": "^2.1.3" 30 | }, 31 | "devDependencies": { 32 | "@tinkoff/request-core": "^0.9.3" 33 | }, 34 | "peerDependencies": { 35 | "@tinkoff/request-core": "0.x" 36 | }, 37 | "module": "lib/validate.es.js" 38 | } 39 | -------------------------------------------------------------------------------- /packages/plugin-validate/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const VALIDATE = 'validate'; 2 | -------------------------------------------------------------------------------- /packages/plugin-validate/src/validate.spec.ts: -------------------------------------------------------------------------------- 1 | import { Context, Status } from '@tinkoff/request-core'; 2 | import { VALIDATE } from './constants'; 3 | import validate from './validate'; 4 | 5 | const validator = ({ response }) => { 6 | if (response.error) { 7 | return new Error(response.error); 8 | } 9 | }; 10 | 11 | const errorValidator = ({ error }) => { 12 | return !!error.valid; 13 | }; 14 | 15 | const plugin = validate({ validator, errorValidator }); 16 | const context = new Context(); 17 | 18 | context.setState = jest.fn(context.setState.bind(context)); 19 | context.updateExternalMeta = jest.fn(context.updateExternalMeta.bind(context)); 20 | 21 | describe('plugins/validate/validate', () => { 22 | beforeEach(() => { 23 | // @ts-ignore 24 | context.setState.mockClear(); 25 | // @ts-ignore 26 | context.updateExternalMeta.mockClear(); 27 | }); 28 | 29 | it('if validator returns undefined plugin should not return any state or call next callback', () => { 30 | context.setState({ response: { a: 1 } }); 31 | (context.setState as jest.Mock).mockClear(); 32 | const next = jest.fn(); 33 | 34 | expect(plugin.complete(context, next, null)).toBeUndefined(); 35 | expect(context.setState).not.toHaveBeenCalled(); 36 | expect(context.updateExternalMeta).toHaveBeenCalledWith(VALIDATE, { validated: true }); 37 | expect(next).toHaveBeenCalledWith(); 38 | }); 39 | 40 | it('on error validate, change status to error', () => { 41 | const request = { a: 1, url: '' }; 42 | const next = jest.fn(); 43 | 44 | context.setState({ request, response: { error: '123' } }); 45 | 46 | plugin.complete(context, next, null); 47 | expect(next).toHaveBeenCalledWith({ 48 | request, 49 | status: Status.ERROR, 50 | error: new Error('123'), 51 | }); 52 | }); 53 | 54 | it('on error validate, change status to error, fallback disabled', () => { 55 | const request = { a: 1, url: '' }; 56 | const next = jest.fn(); 57 | const plugin = validate({ validator, allowFallback: false }); 58 | 59 | context.setState({ request, response: { error: '123' } }); 60 | 61 | plugin.complete(context, next, null); 62 | expect(next).toHaveBeenCalledWith({ 63 | request: { ...request, fallbackCache: false }, 64 | status: Status.ERROR, 65 | error: new Error('123'), 66 | }); 67 | }); 68 | 69 | it('on errored request, errorValidator may change request to complete', () => { 70 | const response = { test: 'abc' }; 71 | const error = Object.assign(new Error(), { valid: true }); 72 | const next = jest.fn(); 73 | 74 | context.setState({ error, response }); 75 | 76 | plugin.error(context, next, null); 77 | expect(next).toHaveBeenCalledWith({ 78 | status: Status.COMPLETE, 79 | error: null, 80 | }); 81 | expect(context.updateExternalMeta).toHaveBeenCalledWith(VALIDATE, { 82 | errorValidated: true, 83 | }); 84 | expect(context.updateExternalMeta).toHaveBeenCalledWith(VALIDATE, { 85 | error, 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /packages/plugin-validate/src/validate.ts: -------------------------------------------------------------------------------- 1 | import nothing from '@tinkoff/utils/function/nothing'; 2 | import type { Plugin, ContextState } from '@tinkoff/request-core'; 3 | import { Status } from '@tinkoff/request-core'; 4 | import { VALIDATE } from './constants'; 5 | 6 | interface Validator { 7 | (state: ContextState): any; 8 | } 9 | 10 | interface Options { 11 | allowFallback?: boolean; 12 | validator?: Validator; 13 | errorValidator?: Validator; 14 | } 15 | 16 | /** 17 | * Plugin to validate response. If `validator` returns falsy value plugin does nothing, 18 | * otherwise return value used as a error and requests goes to the error phase. If `errorValidator` returns truthy 19 | * for request in error phase then plugin switch phase to complete. 20 | * 21 | * @param {function} validator - function to validate success request, accepts single parameter: the state of current request, 22 | * should return error if request should be treated as errored request 23 | * @param {function} errorValidator - function to validate errored request, accepts single parameter: the state of current request, 24 | * should return `truthy` value if request should be treated as success request 25 | * @param {boolean} [allowFallback = true] - if false adds `fallbackCache` option to request to prevent activating fallback cache 26 | * @return {{complete: complete}} 27 | */ 28 | export default ({ validator = nothing, allowFallback = true, errorValidator = nothing }: Options = {}): Plugin => ({ 29 | complete: (context, next) => { 30 | const state = context.getState(); 31 | const { request } = state; 32 | const error = validator(state); 33 | 34 | context.updateExternalMeta(VALIDATE, { 35 | validated: !error, 36 | }); 37 | 38 | if (error) { 39 | return next({ 40 | error, 41 | request: allowFallback 42 | ? request 43 | : { 44 | ...request, 45 | fallbackCache: false, 46 | }, 47 | status: Status.ERROR, 48 | }); 49 | } 50 | 51 | return next(); 52 | }, 53 | error: (context, next) => { 54 | const state = context.getState(); 55 | const isSuccess = errorValidator(state); 56 | 57 | context.updateExternalMeta(VALIDATE, { 58 | errorValidated: !!isSuccess, 59 | }); 60 | 61 | if (isSuccess) { 62 | context.updateExternalMeta(VALIDATE, { 63 | error: state.error, 64 | }); 65 | 66 | return next({ 67 | error: null, 68 | status: Status.COMPLETE, 69 | }); 70 | } 71 | 72 | return next(); 73 | }, 74 | }); 75 | -------------------------------------------------------------------------------- /packages/plugin-validate/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib" 5 | }, 6 | "include": [ 7 | "./src" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /packages/url-utils/.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | src/ 3 | -------------------------------------------------------------------------------- /packages/url-utils/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## [0.9.3](https://github.com/Tinkoff/tinkoff-request/compare/@tinkoff/request-url-utils@0.9.2...@tinkoff/request-url-utils@0.9.3) (2023-08-02) 7 | 8 | **Note:** Version bump only for package @tinkoff/request-url-utils 9 | 10 | 11 | 12 | 13 | 14 | ## [0.9.2](https://github.com/Tinkoff/tinkoff-request/compare/@tinkoff/request-url-utils@0.9.2...@tinkoff/request-url-utils@0.9.2) (2023-07-14) 15 | 16 | 17 | ### Features 18 | 19 | * **url-utils:** add possibility to use custom serializer to addQuery ([e0f1839](https://github.com/Tinkoff/tinkoff-request/commit/e0f1839b52862efc457c53d408ae0e9a2148ff16)) 20 | * **url-utils:** move url utils to separate package ([1ab2397](https://github.com/Tinkoff/tinkoff-request/commit/1ab239709142460ac5cdacfb93714ad5a0e7d277)) 21 | -------------------------------------------------------------------------------- /packages/url-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tinkoff/request-url-utils", 3 | "version": "0.9.3", 4 | "description": "Support library for @tinkoff/request", 5 | "main": "lib/index.js", 6 | "typings": "lib/index.d.ts", 7 | "sideEffects": false, 8 | "scripts": { 9 | "prepack": "tramvai-build --for-publish", 10 | "build": "tramvai-build", 11 | "tsc": "tsc", 12 | "watch": "tsc -w", 13 | "test": "echo \"Error: no test specified\" && exit 1" 14 | }, 15 | "keywords": [ 16 | "request" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/Tinkoff/tinkoff-request/" 21 | }, 22 | "author": "Kirill Meleshko ", 23 | "publishConfig": { 24 | "access": "public" 25 | }, 26 | "license": "ISC", 27 | "dependencies": { 28 | "@tinkoff/utils": "^2.0.0", 29 | "tslib": "^2.1.3" 30 | }, 31 | "module": "lib/index.es.js" 32 | } 33 | -------------------------------------------------------------------------------- /packages/url-utils/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export * from './url'; 3 | -------------------------------------------------------------------------------- /packages/url-utils/src/types.ts: -------------------------------------------------------------------------------- 1 | export type Query = { [key: string]: string | string[] | Query | undefined | null }; 2 | 3 | export type QuerySerializer = (query: Query, initialSearchString?: string) => string; 4 | -------------------------------------------------------------------------------- /packages/url-utils/src/url.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | 5 | import { addQuery, normalizeUrl, serializeQuery } from './url'; 6 | 7 | describe('plugins/http/url', () => { 8 | it('should add query to url', () => { 9 | expect(addQuery('/api', {})).toBe('/api'); 10 | expect(addQuery('/api', { a: '1', b: '2' })).toBe('/api?a=1&b=2'); 11 | expect(addQuery('domain.abv/api', { q: 'test', c: 'test2' })).toBe('domain.abv/api?q=test&c=test2'); 12 | expect(addQuery('https://domain.abv/api/v2/?a=1', { b: '2', c: '3' })).toBe( 13 | 'https://domain.abv/api/v2/?a=1&b=2&c=3' 14 | ); 15 | expect(addQuery('/api/some/?a=1', { a: '2', b: undefined })).toBe('/api/some/?a=2'); 16 | }); 17 | 18 | it('should use customSerializer', () => { 19 | const serializer = jest.fn(() => 'query=string'); 20 | 21 | expect(addQuery('/test?initial=str', { a: '1', b: '2' }, serializer)).toBe('/test?query=string'); 22 | expect(serializer).toHaveBeenCalledWith( 23 | { 24 | a: '1', 25 | b: '2', 26 | }, 27 | 'initial=str' 28 | ); 29 | }); 30 | 31 | it('should serialize query', () => { 32 | expect(serializeQuery({ a: '1', b: '2', c: 'test' })).toBe('a=1&b=2&c=test'); 33 | expect(serializeQuery({ a: '', b: undefined, c: null, d: '0' })).toBe('a=&d=0'); 34 | expect(serializeQuery({ arr: ['1', '2', '3'] })).toBe('arr%5B0%5D=1&arr%5B1%5D=2&arr%5B2%5D=3'); 35 | expect(serializeQuery({ a: '1', b: { c: '2', d: null, e: { f: '3' } }, g: 'test' })).toBe( 36 | 'a=1&b%5Bc%5D=2&b%5Be%5D%5Bf%5D=3&g=test' 37 | ); 38 | }); 39 | 40 | it('should add default http protocol for requests on server', () => { 41 | expect(normalizeUrl('static.com/test.js')).toEqual('http://static.com/test.js'); 42 | expect(normalizeUrl('https://static.com/test.js')).toBe('https://static.com/test.js'); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /packages/url-utils/src/url.ts: -------------------------------------------------------------------------------- 1 | import eachObj from '@tinkoff/utils/object/each'; 2 | import isNil from '@tinkoff/utils/is/nil'; 3 | import isObject from '@tinkoff/utils/is/object'; 4 | import reduceArr from '@tinkoff/utils/array/reduce'; 5 | import type { Query, QuerySerializer } from './types'; 6 | 7 | export const serializeQuery: QuerySerializer = (obj: Query, init = '') => { 8 | const searchParams = new URLSearchParams(init); 9 | 10 | const setParams = (params: object, keys: string[] = []) => { 11 | eachObj((v, k) => { 12 | if (isNil(v)) return; 13 | 14 | const arr = keys.length ? [...keys, k] : [k]; 15 | 16 | if (isObject(v)) { 17 | setParams(v, arr); 18 | } else { 19 | searchParams.set( 20 | reduceArr((acc, curr, i) => (i ? `${acc}[${curr}]` : curr), '', arr), 21 | v 22 | ); 23 | } 24 | }, params); 25 | }; 26 | 27 | setParams(obj); 28 | 29 | return searchParams.toString(); 30 | }; 31 | 32 | export const addQuery = (url: string, query: Query, querySerializer: QuerySerializer = serializeQuery) => { 33 | const [path, search] = url.split('?', 2); 34 | const serialized = querySerializer(query, search); 35 | 36 | if (!serialized) { 37 | return path; 38 | } 39 | 40 | return `${path}?${serialized}`; 41 | }; 42 | 43 | export const normalizeUrl = (url: string) => { 44 | if (typeof window === 'undefined' && !/^https?:\/\//.test(url)) { 45 | return `http://${url}`; 46 | } 47 | 48 | return url; 49 | }; 50 | -------------------------------------------------------------------------------- /packages/url-utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./lib" 5 | }, 6 | "include": [ 7 | "./src" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | "node_modules", 4 | "bin", 5 | "lib", 6 | "**/*.spec.ts" 7 | ], 8 | "compilerOptions": { 9 | "allowJs": false, 10 | "module": "commonjs", 11 | "declaration": true, 12 | "esModuleInterop": true, 13 | "importHelpers": true, 14 | "noUnusedLocals": true, 15 | "lib": [ 16 | "es2015", 17 | "es5", 18 | "dom" 19 | ], 20 | "target": "ES2015" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /website/core/Footer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2017-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | const React = require('react'); 9 | const pt = require('prop-types'); 10 | 11 | class Footer extends React.Component { 12 | static propTypes = { 13 | config: pt.object 14 | }; 15 | 16 | docUrl(doc, language) { 17 | const baseUrl = this.props.config.baseUrl; 18 | 19 | return `${baseUrl}docs/${language ? `${language}/` : ''}${doc}`; 20 | } 21 | 22 | pageUrl(doc, language) { 23 | const baseUrl = this.props.config.baseUrl; 24 | 25 | return baseUrl + (language ? `${language}/` : '') + doc; 26 | } 27 | 28 | render() { 29 | return ( 30 |
31 |
32 | {this.props.config.copyright} 33 |
34 |
35 | ); 36 | } 37 | } 38 | 39 | module.exports = Footer; 40 | -------------------------------------------------------------------------------- /website/i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "_comment": "This file is auto-generated by write-translations.js", 3 | "localized-strings": { 4 | "next": "Next", 5 | "previous": "Previous", 6 | "tagline": "Construct your own request library", 7 | "docs": { 8 | "core/context": { 9 | "title": "@tinkoff/request-core - Context", 10 | "sidebar_label": "Context" 11 | }, 12 | "core/execution": { 13 | "title": "@tinkoff/request-core - Request Execution flow", 14 | "sidebar_label": "Request execution" 15 | }, 16 | "core/index": { 17 | "title": "@tinkoff/request-core", 18 | "sidebar_label": "Core" 19 | }, 20 | "core/plugin": { 21 | "title": "Plugin definition", 22 | "sidebar_label": "Plugin" 23 | }, 24 | "how-to/index": { 25 | "title": "How to", 26 | "sidebar_label": "How to" 27 | }, 28 | "plugins/batch": { 29 | "title": "Batch Plugin", 30 | "sidebar_label": "Batch" 31 | }, 32 | "plugins/cache-deduplicate": { 33 | "title": "Cache Plugin - Deduplicate", 34 | "sidebar_label": "Cache - Deduplicate" 35 | }, 36 | "plugins/cache-etag": { 37 | "title": "Cache Plugin - Etag", 38 | "sidebar_label": "Cache - Etag" 39 | }, 40 | "plugins/cache-fallback": { 41 | "title": "Cache Plugin - Fallback", 42 | "sidebar_label": "Cache - Fallback" 43 | }, 44 | "plugins/cache-memory": { 45 | "title": "Cache Plugin - Memory", 46 | "sidebar_label": "Cache - Memory" 47 | }, 48 | "plugins/cache-persistent": { 49 | "title": "Cache Plugin - Persistent", 50 | "sidebar_label": "Cache - Persistent" 51 | }, 52 | "plugins/circuit-breaker": { 53 | "title": "Circuit Breaker Plugin", 54 | "sidebar_label": "Circuit Breaker" 55 | }, 56 | "plugins/index": { 57 | "title": "Plugins", 58 | "sidebar_label": "Plugins" 59 | }, 60 | "plugins/log": { 61 | "title": "Log Plugin", 62 | "sidebar_label": "Log" 63 | }, 64 | "plugins/prom-red-metrics": { 65 | "title": "Prom RED metrics", 66 | "sidebar_label": "Prom RED metrics" 67 | }, 68 | "plugins/protocol-http": { 69 | "title": "Protocol Plugin - Http", 70 | "sidebar_label": "Protocol - Http" 71 | }, 72 | "plugins/protocol-jsonp": { 73 | "title": "Protocol Plugin - Jsonp", 74 | "sidebar_label": "Protocol - Jsonp" 75 | }, 76 | "plugins/retry": { 77 | "title": "Retry Plugin", 78 | "sidebar_label": "Retry" 79 | }, 80 | "plugins/transform-url": { 81 | "title": "Transform Plugin - Url", 82 | "sidebar_label": "Transform - Url" 83 | }, 84 | "plugins/validate": { 85 | "title": "Validate Plugin", 86 | "sidebar_label": "Validate" 87 | } 88 | }, 89 | "links": { 90 | "How to": "How to", 91 | "Plugins": "Plugins", 92 | "Internals": "Internals", 93 | "GitHub": "GitHub" 94 | }, 95 | "categories": { 96 | "Core": "Core", 97 | "Plugins": "Plugins", 98 | "How to": "How to" 99 | } 100 | }, 101 | "pages-strings": { 102 | "Help Translate|recruit community translators for your project": "Help Translate", 103 | "Edit this Doc|recruitment message asking to edit the doc source": "Edit", 104 | "Translate this Doc|recruitment message asking to translate the docs": "Translate" 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tinkoff/request-docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "examples": "docusaurus-examples", 7 | "start": "docusaurus-start", 8 | "build": "docusaurus-build", 9 | "publish-gh-pages": "docusaurus-publish", 10 | "write-translations": "docusaurus-write-translations", 11 | "version": "docusaurus-version", 12 | "rename-version": "docusaurus-rename-version" 13 | }, 14 | "devDependencies": { 15 | "docusaurus": "^1.14.2" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /website/sidebars.json: -------------------------------------------------------------------------------- 1 | { 2 | "docs": { 3 | "Core": [ 4 | "core/index", 5 | "core/context", 6 | "core/execution", 7 | "core/plugin" 8 | ], 9 | "Plugins": [ 10 | "plugins/index", 11 | "plugins/batch", 12 | "plugins/cache-deduplicate", 13 | "plugins/cache-fallback", 14 | "plugins/cache-memory", 15 | "plugins/cache-etag", 16 | "plugins/cache-persistent", 17 | "plugins/log", 18 | "plugins/protocol-http", 19 | "plugins/protocol-jsonp", 20 | "plugins/transform-url", 21 | "plugins/validate", 22 | "plugins/circuit-breaker", 23 | "plugins/prom-red-metrics", 24 | "plugins/retry" 25 | ], 26 | "How to": [ 27 | "how-to/index" 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /website/siteConfig.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2017-present, Facebook, Inc. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | // See https://docusaurus.io/docs/site-config.html for all the possible 9 | // site configuration options. 10 | 11 | /* List of projects/orgs using your project for the users page */ 12 | 13 | const siteConfig = { 14 | title: '@tinkoff/request' /* title for your website */, 15 | tagline: 'Construct your own request library', 16 | baseUrl: '/tinkoff-request/' /* base url for your project */, 17 | // For github.io type URLs, you would set the url and baseUrl like: 18 | // url: 'https://facebook.github.io', 19 | // baseUrl: '/test-site/', 20 | 21 | // Used for publishing and more 22 | projectName: 'tinkoff-request', 23 | organizationName: 'Tinkoff', 24 | // For top-level user or org sites, the organization is still the same. 25 | // e.g., for the https://JoelMarcey.github.io site, it would be set like... 26 | // organizationName: 'JoelMarcey' 27 | 28 | // For no header links in the top nav bar -> headerLinks: [], 29 | headerLinks: [ 30 | { doc: 'how-to/index', label: 'How to' }, 31 | { doc: 'plugins/index', label: 'Plugins' }, 32 | { doc: 'core/index', label: 'Internals' }, 33 | { href: 'https://github.com/Tinkoff/tinkoff-request', label: 'GitHub' } 34 | ], 35 | 36 | // If you have users set above, you add it here: 37 | // users, 38 | 39 | /* path to images for header/footer */ 40 | headerIcon: 'img/logo-tinkoff.svg', 41 | footerIcon: 'img/logo-tinkoff.svg', 42 | favicon: 'img/favicon.png', 43 | 44 | /* colors for website */ 45 | colors: { 46 | primaryColor: '#3B3738', 47 | secondaryColor: '#843131' 48 | }, 49 | 50 | /* custom fonts for website */ 51 | /* fonts: { 52 | myFont: [ 53 | "Times New Roman", 54 | "Serif" 55 | ], 56 | myOtherFont: [ 57 | "-apple-system", 58 | "system-ui" 59 | ] 60 | }, */ 61 | 62 | // This copyright info is used in /core/Footer.js and blog rss/atom feeds. 63 | copyright: 64 | `Copyright © ${ 65 | new Date().getFullYear() 66 | } tinkoff.ru`, 67 | 68 | highlight: { 69 | // Highlight.js theme to use for syntax highlighting in code blocks 70 | theme: 'default' 71 | }, 72 | 73 | // Add custom scripts here that would be placed in