From fe3c7eab433db44d639436722582719b31e33ece Mon Sep 17 00:00:00 2001 From: gretzkiy Date: Tue, 24 Dec 2024 17:39:12 +0300 Subject: [PATCH] chore(build): update lib build --- lib/core/async/events/index.js | 2 +- lib/core/functools/memoize.d.ts | 5 +- lib/core/functools/memoize.js | 26 ++- lib/core/functools/spec.js | 32 ++++ lib/core/log/index.d.ts | 2 +- lib/core/perf/timer/engines/index.d.ts | 2 +- lib/core/prelude/function/memoize/index.js | 27 +-- lib/core/prelude/global/index.js | 6 +- lib/core/prelude/i18n/const.d.ts | 9 - lib/core/prelude/i18n/const.js | 11 +- lib/core/prelude/i18n/helpers.d.ts | 53 ++++-- lib/core/prelude/i18n/helpers.js | 66 +++++-- lib/core/prelude/i18n/interface.d.ts | 11 +- lib/core/prelude/i18n/spec.d.ts | 8 + lib/core/prelude/i18n/spec.js | 176 ++++++++++++++++++ lib/core/promise/abortable/index.d.ts | 24 ++- lib/core/request/engines/composition/index.js | 51 ++++- .../engines/composition/interface.d.ts | 13 ++ lib/core/request/engines/fetch/index.js | 5 + lib/core/request/engines/provider/index.js | 12 +- lib/core/request/interface.d.ts | 1 + lib/core/request/response/index.js | 6 +- lib/core/request/response/test/main.spec.d.ts | 1 + lib/core/request/response/test/main.spec.js | 16 ++ lib/lang/interface.d.ts | 9 +- ts-definitions/prelude/function/proto.d.ts | 8 +- ts-definitions/prelude/function/static.d.ts | 10 +- 27 files changed, 479 insertions(+), 113 deletions(-) create mode 100644 lib/core/prelude/i18n/spec.d.ts create mode 100644 lib/core/prelude/i18n/spec.js create mode 100644 lib/core/request/response/test/main.spec.d.ts create mode 100644 lib/core/request/response/test/main.spec.js diff --git a/lib/core/async/events/index.js b/lib/core/async/events/index.js index e0125b040..e8365b050 100644 --- a/lib/core/async/events/index.js +++ b/lib/core/async/events/index.js @@ -114,7 +114,7 @@ class Async extends _timers.default { args }; function handler(...handlerArgs) { - if (p.single && (hasMultipleEvent || !emitter.once)) { + if (p.single && (hasMultipleEvent || !('once' in emitter))) { if (hasMultipleEvent) { that.clearEventListener(ids); } else { diff --git a/lib/core/functools/memoize.d.ts b/lib/core/functools/memoize.d.ts index fcabe75b4..02569a7ca 100644 --- a/lib/core/functools/memoize.d.ts +++ b/lib/core/functools/memoize.d.ts @@ -9,6 +9,7 @@ * Decorator for `Function.prototype.once` * * @decorator - * @see [[Function.once]] + * @see once */ -export declare function once(target: object, key: string | symbol, descriptor: PropertyDescriptor): void; +export declare function onceDecorator(target: object, key: string | symbol, descriptor: PropertyDescriptor): void; +export declare function once(fn: AnyFunction): AnyFunction; diff --git a/lib/core/functools/memoize.js b/lib/core/functools/memoize.js index 94c9c67a6..bfc135177 100644 --- a/lib/core/functools/memoize.js +++ b/lib/core/functools/memoize.js @@ -4,7 +4,8 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.once = once; -function once(target, key, descriptor) { +exports.onceDecorator = onceDecorator; +function onceDecorator(target, key, descriptor) { const method = descriptor.value; if (!Object.isFunction(method)) { throw new TypeError(`descriptor.value is not a function: ${method}`); @@ -12,8 +13,29 @@ function once(target, key, descriptor) { descriptor.value = function value(...args) { Object.defineProperty(this, key, { configurable: true, - value: method.once() + value: once(method) }); return this[key](...args); }; +} +function once(fn) { + let called = false, + result = undefined; + Object.defineProperty(onceWrapper, 'cancelOnce', { + configurable: true, + enumerable: false, + writable: true, + value: () => { + called = true; + result = undefined; + } + }); + return onceWrapper; + function onceWrapper(...args) { + if (!called) { + result = fn.apply(this, args); + called = true; + } + return result; + } } \ No newline at end of file diff --git a/lib/core/functools/spec.js b/lib/core/functools/spec.js index 66fbeb7dc..7e6bdfe60 100644 --- a/lib/core/functools/spec.js +++ b/lib/core/functools/spec.js @@ -3,7 +3,39 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); var _applyDecoratedDescriptor2 = _interopRequireDefault(require("@babel/runtime/helpers/applyDecoratedDescriptor")); var _lazy = require("../../core/functools/lazy"); +var _memoize = require("../../core/functools/memoize"); describe('core/functools', () => { + describe('`once`', () => { + it('should memoize the return result', () => { + const rand = (0, _memoize.once)(Math.random), + res = rand(); + expect(Object.isNumber(res)).toBe(true); + expect(rand()).toBe(res); + expect(rand()).toBe(res); + }); + it('should memoize the return result with different arguments', () => { + const testFn = (0, _memoize.once)(i => i); + expect(testFn(1)).toBe(1); + expect(testFn(2)).toBe(1); + }); + it('should not be called multiple times', () => { + const testFn = jest.fn(i => i); + const onceFn = (0, _memoize.once)(testFn); + onceFn(1); + onceFn(2); + onceFn(3); + expect(testFn).toHaveBeenCalledTimes(1); + }); + it('`cancelOnce` should reset return value', () => { + const testFn = jest.fn(i => i); + const onceFn = (0, _memoize.once)(testFn); + expect(onceFn(1)).toBe(1); + expect(onceFn(2)).toBe(1); + testFn.cancelOnce(); + expect(onceFn(1)).toBe(undefined); + expect(testFn).toBeCalledTimes(1); + }); + }); describe('`@debounce`', () => { it('should decorate a method so it runs delayed by a specified number of ms.', done => { var _dec, _class; diff --git a/lib/core/log/index.d.ts b/lib/core/log/index.d.ts index b81e68f1b..d70a16380 100644 --- a/lib/core/log/index.d.ts +++ b/lib/core/log/index.d.ts @@ -11,5 +11,5 @@ export * from '../../core/log/config'; * API for logging * @defaultExport */ -declare const logger: import("./interface").ExtendedLogger; +declare const logger: import("../../core/log/interface").ExtendedLogger; export default logger; diff --git a/lib/core/perf/timer/engines/index.d.ts b/lib/core/perf/timer/engines/index.d.ts index 16d57836d..a5e4a6f06 100644 --- a/lib/core/perf/timer/engines/index.d.ts +++ b/lib/core/perf/timer/engines/index.d.ts @@ -7,6 +7,6 @@ */ export * from '../../../../core/perf/timer/engines/interface'; declare const engines: { - console: import("./interface").PerfTimerEngine; + console: import("../../../../core/perf/timer/engines/interface").PerfTimerEngine; }; export default engines; diff --git a/lib/core/prelude/function/memoize/index.js b/lib/core/prelude/function/memoize/index.js index 453d7f203..d190f7c84 100644 --- a/lib/core/prelude/function/memoize/index.js +++ b/lib/core/prelude/function/memoize/index.js @@ -2,29 +2,4 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); var _extend = _interopRequireDefault(require("../../../../core/prelude/extend")); -(0, _extend.default)(Function.prototype, 'once', function once() { - const fn = this; - let called = false, - res; - Object.defineProperty(wrapper, 'cancelOnce', { - configurable: true, - enumerable: false, - writable: true, - value: () => { - called = true; - res = undefined; - } - }); - return wrapper; - function wrapper(...args) { - if (called) { - return res; - } - res = fn.apply(this, args); - called = true; - return res; - } -}); -(0, _extend.default)(Function.prototype, 'cancelOnce', () => undefined); -(0, _extend.default)(Function, 'once', fn => fn.once()); -(0, _extend.default)(Function, 'cancelOnce', fn => fn.cancelOnce()); \ No newline at end of file +(0, _extend.default)(Function.prototype, 'cancelOnce', () => undefined); \ No newline at end of file diff --git a/lib/core/prelude/global/index.js b/lib/core/prelude/global/index.js index 0ad962986..8973e1d84 100644 --- a/lib/core/prelude/global/index.js +++ b/lib/core/prelude/global/index.js @@ -5,12 +5,12 @@ var _log = _interopRequireDefault(require("../../../core/log")); var _extend = _interopRequireDefault(require("../../../core/prelude/extend")); var _const = require("../../../core/prelude/global/const"); (0, _extend.default)(globalThis, 'Any', obj => obj); -(0, _extend.default)(globalThis, 'stderr', err => { +(0, _extend.default)(globalThis, 'stderr', (err, ...details) => { if (err instanceof Object) { if (_const.errorsToIgnore[err.type] === true) { - _log.default.info('stderr', err); + _log.default.info('stderr', err, ...details); return; } - _log.default.error('stderr', err); + _log.default.error('stderr', err, ...details); } }); \ No newline at end of file diff --git a/lib/core/prelude/i18n/const.d.ts b/lib/core/prelude/i18n/const.d.ts index eab406cdd..946b1beb8 100644 --- a/lib/core/prelude/i18n/const.d.ts +++ b/lib/core/prelude/i18n/const.d.ts @@ -24,12 +24,3 @@ export declare const locale: Locale; * The default application region */ export declare const region: RegionStore; -/** - * A dictionary to map literal pluralization forms to numbers - */ -export declare const pluralizeMap: Pick<{ - none: number; - one: number; - some: number; - many: number; -}, "some" | "none" | "one" | "many">; diff --git a/lib/core/prelude/i18n/const.js b/lib/core/prelude/i18n/const.js index a86e5f5f5..305adc181 100644 --- a/lib/core/prelude/i18n/const.js +++ b/lib/core/prelude/i18n/const.js @@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); -exports.region = exports.pluralizeMap = exports.locale = exports.event = exports.emitter = void 0; +exports.region = exports.locale = exports.event = exports.emitter = void 0; var _eventemitter = require("eventemitter2"); const emitter = new _eventemitter.EventEmitter2({ maxListeners: 100, @@ -21,11 +21,4 @@ const region = { value: undefined, isDefault: false }; -exports.region = region; -const pluralizeMap = Object.createDict({ - none: 0, - one: 1, - some: 2, - many: 5 -}); -exports.pluralizeMap = pluralizeMap; \ No newline at end of file +exports.region = region; \ No newline at end of file diff --git a/lib/core/prelude/i18n/helpers.d.ts b/lib/core/prelude/i18n/helpers.d.ts index ce2cb5f86..ff93bffc6 100644 --- a/lib/core/prelude/i18n/helpers.d.ts +++ b/lib/core/prelude/i18n/helpers.d.ts @@ -6,7 +6,7 @@ * https://github.com/V4Fire/Core/blob/master/LICENSE */ import { Translation, PluralTranslation } from '../../../lang'; -import type { PluralizationCount } from '../../../core/prelude/i18n/interface'; +import type { I18nOpts, PluralizationCount } from '../../../core/prelude/i18n/interface'; /** * Creates a function to internationalize strings in an application based on the given locale and keyset. * Keyset allows you to share the same keys in different contexts. @@ -26,6 +26,7 @@ export declare function i18nFactory(keysetNameOrNames: string | string[], custom * * @param value - a string for the default case, or an array of strings for the plural case * @param params - a dictionary with parameters for internationalization + * @params [opts] - additional options for current translation * * @example * ```typescript @@ -33,33 +34,55 @@ export declare function i18nFactory(keysetNameOrNames: string | string[], custom * * console.log(example); // 'My name is John, I live in Denver' * - * const examplePluralize = resolveTemplate([ - * {count} product, // One - * {count} products, // Some - * {count} products, // Many - * {count} products, // None - * ], {count: 5}); + * const examplePluralize = resolveTemplate({ + * one: {count} product, + * few: {count} products, + * many: {count} products, + * zero: {count} products, + * }, {count: 5}); * * console.log(examplePluralize); // '5 products' * ``` */ -export declare function resolveTemplate(value: Translation, params?: I18nParams): string; +export declare function resolveTemplate(value: Translation, params?: I18nParams, opts?: I18nOpts): string; /** * Returns the correct plural form to translate based on the given count * * @param pluralTranslation - list of translation variants * @param count - the value on the basis of which the form of pluralization will be selected + * @params [opts] - additional options for current translation * * @example * ```typescript - * const result = pluralizeText([ - * {count} product, // One - * {count} products, // Some - * {count} products, // Many - * {count} products, // None - * ], 5); + * const result = pluralizeText({ + * one: {count} product, + * few: {count} products, + * many: {count} products, + * zero: {count} products, + * other: {count} products, + * }, 5, {pluralRules: new Intl.PluralRulse('en')}); * * console.log(result); // '{count} products' * ``` */ -export declare function pluralizeText(pluralTranslation: PluralTranslation, count: CanUndef): string; +export declare function pluralizeText(pluralTranslation: PluralTranslation, count: CanUndef, opts?: I18nOpts): string; +/** + * Returns the plural form name for a given number `n` based on the specified pluralization rules. + * Otherwise will be used default set of rules. + * + * If a `rules` object implementing `Intl.PluralRules` is provided, it will use that to determine the plural form. + * Otherwise, it will fall back to a custom rule set: + * - Returns 'zero' for `n === 0`. + * - Returns 'one' for `n === 1`. + * - Returns 'few' for `n > 1 && n < 5`. + * - Returns 'many' for all other values of `n`. + * + * @param n - The number to evaluate for pluralization. + * @param rules - Plural rules object. If undefined, a default rule set is used. + */ +export declare function getPluralFormName(n: number, rules?: CanUndef): keyof Required; +/** + * Returns an instance of `Intl.PluralRules` for a given locale, if supported. + * @param locale - The locale for which to generate plural rules. + */ +export declare function getPluralRules(locale: Language): CanUndef; diff --git a/lib/core/prelude/i18n/helpers.js b/lib/core/prelude/i18n/helpers.js index d628cbea0..0fa47af62 100644 --- a/lib/core/prelude/i18n/helpers.js +++ b/lib/core/prelude/i18n/helpers.js @@ -4,6 +4,8 @@ var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefau Object.defineProperty(exports, "__esModule", { value: true }); +exports.getPluralFormName = getPluralFormName; +exports.getPluralRules = getPluralRules; exports.i18nFactory = i18nFactory; exports.pluralizeText = pluralizeText; exports.resolveTemplate = resolveTemplate; @@ -17,22 +19,34 @@ function i18nFactory(keysetNameOrNames, customLocale) { if (resolvedLocale == null) { throw new ReferenceError('The locale for internationalization is not defined'); } + const pluralRules = getPluralRules(resolvedLocale); return function i18n(value, params) { if (Object.isArray(value) && value.length !== 1) { throw new SyntaxError('Using i18n with template literals is allowed only without variables'); } const key = Object.isString(value) ? value : value[0], correctKeyset = keysetNames.find(keysetName => _lang.default[resolvedLocale]?.[keysetName]?.[key]), - translateValue = _lang.default[resolvedLocale]?.[correctKeyset ?? '']?.[key]; + translateValue = _lang.default[resolvedLocale]?.[correctKeyset ?? '']?.[key], + meta = { + language: resolvedLocale, + keyset: correctKeyset, + key + }; if (translateValue != null && translateValue !== '') { - return resolveTemplate(translateValue, params); + return resolveTemplate(translateValue, params, { + pluralRules, + meta + }); } logger.error('Translation for the given key is not found', `Key: ${key}, KeysetNames: ${keysetNames.join(', ')}, LocaleName: ${resolvedLocale}, available locales: ${Object.keys(_lang.default).join(', ')}`); - return resolveTemplate(key, params); + return resolveTemplate(key, params, { + pluralRules, + meta + }); }; } -function resolveTemplate(value, params) { - const template = Object.isArray(value) ? pluralizeText(value, params?.count) : value; +function resolveTemplate(value, params, opts = {}) { + const template = Object.isPlainObject(value) ? pluralizeText(value, params?.count, opts) : value; return template.replace(/{([^}]+)}/g, (_, key) => { if (params?.[key] == null) { logger.error('Undeclared variable', `Name: "${key}", Template: "${template}"`); @@ -41,28 +55,50 @@ function resolveTemplate(value, params) { return params[key]; }); } -function pluralizeText(pluralTranslation, count) { +function pluralizeText(pluralTranslation, count, opts = {}) { + const { + pluralRules, + meta + } = opts; let normalizedCount; if (Object.isNumber(count)) { normalizedCount = count; } else if (Object.isString(count)) { - if (count in _const.pluralizeMap) { - normalizedCount = _const.pluralizeMap[count]; + const translation = pluralTranslation[count]; + if (translation != null) { + return translation; } } if (normalizedCount == null) { - logger.error('Invalid value of the `count` parameter for string pluralization', `String: ${pluralTranslation[0]}`); + logger.error('Invalid value of the `count` parameter for string pluralization', `Count: ${count}, Key: ${meta?.key}, Language: ${meta?.language}, Keyset: ${meta?.keyset}`); normalizedCount = 1; } - switch (normalizedCount) { + const pluralFormName = getPluralFormName(normalizedCount, pluralRules), + translation = pluralTranslation[pluralFormName]; + if (translation == null) { + logger.error(`Plural form ${pluralFormName} doesn't exist.`, `Key: ${meta?.key}, Language: ${meta?.language}, Keyset: ${meta?.keyset}`); + return pluralTranslation.one; + } + return translation; +} +function getPluralFormName(n, rules) { + if (rules != null) { + return rules.select(n); + } + switch (n) { case 0: - return pluralTranslation[3]; + return 'zero'; case 1: - return pluralTranslation[0]; + return 'one'; default: - if (normalizedCount > 1 && normalizedCount < 5) { - return pluralTranslation[1]; + if (n > 1 && n < 5) { + return 'few'; } - return pluralTranslation[2]; + return 'many'; + } +} +function getPluralRules(locale) { + if ('PluralRules' in globalThis['Intl']) { + return new globalThis['Intl'].PluralRules(locale); } } \ No newline at end of file diff --git a/lib/core/prelude/i18n/interface.d.ts b/lib/core/prelude/i18n/interface.d.ts index 3de348214..4b6be9c69 100644 --- a/lib/core/prelude/i18n/interface.d.ts +++ b/lib/core/prelude/i18n/interface.d.ts @@ -33,4 +33,13 @@ export interface LocaleKVStorage { */ set?: SyncStorage['set']; } -export declare type PluralizationCount = StringPluralizationForms | string | number; +export declare type PluralizationCount = StringPluralizationForms | number; +export interface I18nMeta { + language: string; + key: string; + keyset?: string; +} +export interface I18nOpts { + pluralRules?: Intl.PluralRules; + meta?: I18nMeta; +} diff --git a/lib/core/prelude/i18n/spec.d.ts b/lib/core/prelude/i18n/spec.d.ts new file mode 100644 index 000000000..3c73d6848 --- /dev/null +++ b/lib/core/prelude/i18n/spec.d.ts @@ -0,0 +1,8 @@ +/*! + * V4Fire Core + * https://github.com/V4Fire/Core + * + * Released under the MIT license + * https://github.com/V4Fire/Core/blob/master/LICENSE + */ +export {}; diff --git a/lib/core/prelude/i18n/spec.js b/lib/core/prelude/i18n/spec.js new file mode 100644 index 000000000..ae88a625b --- /dev/null +++ b/lib/core/prelude/i18n/spec.js @@ -0,0 +1,176 @@ +"use strict"; + +var _i18n = require("../../../core/prelude/i18n"); +var _helpers = require("../../../core/prelude/i18n/helpers"); +describe('core/prelude/i18n', () => { + const rules = new Intl.PluralRules('en'); + const forms = { + one: 'first form', + two: 'second form', + few: 'third form', + many: 'fifth form', + zero: 'zeroth form', + other: 'others form' + }; + const formNames = Object.keys(forms); + describe('pluralization forms detection', () => { + it('detecting plural form without Intl rules', () => { + expect((0, _helpers.getPluralFormName)(0)).toBe('zero'); + expect((0, _helpers.getPluralFormName)(1)).toBe('one'); + expect((0, _helpers.getPluralFormName)(2)).toBe('few'); + expect((0, _helpers.getPluralFormName)(5)).toBe('many'); + }); + it('detecting plural form using Intl rules', () => { + expect((0, _helpers.getPluralFormName)(0, rules)).toBe('other'); + expect((0, _helpers.getPluralFormName)(1, rules)).toBe('one'); + expect((0, _helpers.getPluralFormName)(2, rules)).toBe('other'); + expect((0, _helpers.getPluralFormName)(5, rules)).toBe('other'); + }); + }); + describe('text pluralization', () => { + it('using pluralization constants to choose the right form', () => { + formNames.forEach(form => { + expect((0, _i18n.pluralizeText)(forms, form, { + pluralRules: rules + })).toBe(forms[form]); + }); + }); + it('using a number to choose the right form of pluralization', () => { + const input = { + forms, + count: [1, 2, 100, 0] + }; + [forms.one, forms.other, forms.other, forms.other].forEach((form, index) => { + expect((0, _i18n.pluralizeText)(input.forms, input.count[index], { + pluralRules: rules + })).toBe(form); + }); + }); + it('returns "one" form when required plural form is missing', () => { + const input = { + forms, + count: [1, 2, 100, 0] + }; + [forms.one, forms.one, forms.one, forms.one].forEach((form, index) => { + expect((0, _i18n.pluralizeText)({ + one: input.forms.one + }, input.count[index], { + pluralRules: rules + })).toBe(form); + }); + }); + it('returns "one" form when count is invalid', () => { + const input = { + forms + }; + [forms.one, forms.one, forms.one, forms.one].forEach(form => { + expect((0, _i18n.pluralizeText)({ + one: input.forms.one + }, undefined, { + pluralRules: rules + })).toBe(form); + }); + }); + }); + describe('substitution of variables and pluralization forms in a template', () => { + it('template resolving without additional parameters', () => { + expect((0, _i18n.resolveTemplate)('foo bar baz')).toBe('foo bar baz'); + }); + it('passing variables for template resolving', () => { + const tpl = 'foo {macros} {macros2}'; + expect((0, _i18n.resolveTemplate)(tpl, { + macros: 'bar', + macros2: 'baz' + })).toBe('foo bar baz'); + }); + it('if the variable is not set, then it should be displayed as text', () => { + const tpl = 'foo {macros} {macros2}'; + expect((0, _i18n.resolveTemplate)(tpl, { + macros: 'bar' + })).toBe('foo bar macros2'); + }); + it('passing the `count` parameter for template resolving', () => { + const res1 = (0, _i18n.resolveTemplate)({ + one: 'one {count}', + few: 'few {count}', + many: 'many {count}', + other: 'other {count}' + }, { + count: 5 + }, { + pluralRules: rules + }); + const res2 = (0, _i18n.resolveTemplate)({ + one: 'one {count}', + few: 'few {count}', + many: 'many {count}', + other: 'other {count}' + }, { + count: 1 + }, { + pluralRules: rules + }); + expect(res1).toBe('other 5'); + expect(res2).toBe('one 1'); + }); + }); + describe('pluralization for cyrillic language', () => { + it('russian language with Intl', () => { + const cyrillicRules = new Intl.PluralRules('ru'), + forms = { + one: '{count} яблоко', + few: '{count} яблока', + many: '{count} яблок', + zero: '{count} яблок' + }; + expect((0, _i18n.resolveTemplate)(forms, { + count: 1 + }, { + pluralRules: cyrillicRules + })).toBe('1 яблоко'); + expect((0, _i18n.resolveTemplate)(forms, { + count: 2 + }, { + pluralRules: cyrillicRules + })).toBe('2 яблока'); + expect((0, _i18n.resolveTemplate)(forms, { + count: 0 + }, { + pluralRules: cyrillicRules + })).toBe('0 яблок'); + expect((0, _i18n.resolveTemplate)(forms, { + count: 12 + }, { + pluralRules: cyrillicRules + })).toBe('12 яблок'); + expect((0, _i18n.resolveTemplate)(forms, { + count: 22 + }, { + pluralRules: cyrillicRules + })).toBe('22 яблока'); + }); + it('russian language without Intl', () => { + const forms = { + one: '{count} яблоко', + few: '{count} яблока', + many: '{count} яблок', + zero: '{count} яблок' + }; + expect((0, _i18n.resolveTemplate)(forms, { + count: 1 + })).toBe('1 яблоко'); + expect((0, _i18n.resolveTemplate)(forms, { + count: 2 + })).toBe('2 яблока'); + expect((0, _i18n.resolveTemplate)(forms, { + count: 0 + })).toBe('0 яблок'); + expect((0, _i18n.resolveTemplate)(forms, { + count: 12 + })).toBe('12 яблок'); + expect((0, _i18n.resolveTemplate)(forms, { + count: 22 + })).toBe('22 яблок'); + }); + }); +}); \ No newline at end of file diff --git a/lib/core/promise/abortable/index.d.ts b/lib/core/promise/abortable/index.d.ts index 4abce7bf6..4bfb56f6c 100644 --- a/lib/core/promise/abortable/index.d.ts +++ b/lib/core/promise/abortable/index.d.ts @@ -185,8 +185,28 @@ export default class AbortablePromise implements Promise { */ finally(cb?: Nullable): AbortablePromise; /** - * Aborts the current promise (the promise will be rejected) - * @param [reason] - abort reason + * Aborts the current promise. + * The promise will be rejected only if it doesn't have any active consumers. + * You can follow the link to see how to get around this behavior. + * @see https://github.com/V4Fire/Core/blob/a0635b1ed2600409b5c14b5f85f0281a4f48ee8c/src/core/promise/abortable/README.md#tied-promises + * + * @param [reason] + * + * @example + * ```js + * const promise1 = new AbortablePromise(...); + * const promise2 = new AbortablePromise(...); + * + * promise1.then((res) => doSomething(res)); + * promise2.then((res) => doSomething(res)); + * promise2.then((res) => doSomethingElse(res)); + * + * // It will be aborted, because it has only 1 consumer + * promise1.abort(); + * + * // It won't be aborted, because it has 2 consumers + * promise2.abort(); + * ``` */ abort(reason?: unknown): boolean; /** diff --git a/lib/core/request/engines/composition/index.js b/lib/core/request/engines/composition/index.js index fedaf7315..e035b4542 100644 --- a/lib/core/request/engines/composition/index.js +++ b/lib/core/request/engines/composition/index.js @@ -66,21 +66,40 @@ function compositionEngine(compositionRequests, engineOptions) { } const promises = compositionRequests.map(r => _structures.SyncPromise.resolve(r.requestFilter?.(options)).then(filterValue => { if (filterValue === false) { - return; + return {}; } - return r.request(options).then(boundRequest.bind(null, async)).then(request => isRequestResponseObject(request) ? request.data : request).catch(err => { + return r.request(options).then(boundRequest.bind(null, async)).then(async request => { + if (isRequestResponseObject(request)) { + return { + data: await request.data, + headers: request.response.headers, + status: request.response.status + }; + } + return { + data: await request, + headers: {}, + status: _statusCodes.default.OK + }; + }).catch(err => { if (r.failCompositionOnError) { throw err; } + return {}; }); })); - gatherDataFromRequests(promises, options).then(data => { + gatherDataFromRequests(promises, options).then(({ + data, + status, + headers + }) => { resolve(new _request.Response(data, { parent: requestOptions.parent, important: requestOptions.important, responseType: 'object', okStatuses: requestOptions.okStatuses, - status: _statusCodes.default.OK, + status: status ?? _statusCodes.default.OK, + headers: headers ?? {}, decoder: requestOptions.decoders, noContentStatuses: requestOptions.noContentStatuses })); @@ -114,7 +133,9 @@ function boundRequest(async, requestObject) { return requestObject; } async function gatherDataFromRequests(promises, options) { - const accumulator = {}; + const accumulator = { + data: {} + }; if (options.engineOptions?.aggregateErrors) { await Promise.allSettled(promises).then(results => { const errors = []; @@ -139,14 +160,24 @@ async function gatherDataFromRequests(promises, options) { } return accumulator; } -function accumulateData(accumulator, data, compositionRequest) { +function accumulateData(accumulator, newData, compositionRequest) { const { - as - } = compositionRequest; + as + } = compositionRequest, + { + status, + headers, + data + } = newData; + accumulator.data ??= {}; if (as === _const.compositionEngineSpreadResult) { - Object.assign(accumulator, data); + Object.assign(accumulator.data, data); } else { - Object.set(accumulator, as, data); + Object.set(accumulator.data, as, data); + } + if (compositionRequest.propagateStatusAndHeaders === true) { + accumulator.status = status; + accumulator.headers = headers; } return accumulator; } diff --git a/lib/core/request/engines/composition/interface.d.ts b/lib/core/request/engines/composition/interface.d.ts index f3fddcf79..8514b149f 100644 --- a/lib/core/request/engines/composition/interface.d.ts +++ b/lib/core/request/engines/composition/interface.d.ts @@ -8,6 +8,8 @@ import type Provider from '../../../../core/data'; import type { ProviderOptions } from '../../../../core/data'; import type { RequestOptions, RequestResponseObject, MiddlewareParams, RequestPromise, RequestEngine } from '../../../../core/request'; +import type { RawHeaders } from '../../../../core/request/headers'; +import type { StatusCodes } from '../../../../core/status-codes'; export interface CompositionEngineOpts { /** * If true, the engine will change its behavior and will now wait for the completion @@ -124,6 +126,12 @@ export interface CompositionRequest { * If false / undefined, request errors will be ignored. */ failCompositionOnError?: boolean; + /** + * If true, status code and reponse headers will be propagated from this request to the whole + * composition. Note that if there are more than one request with this option set to true, + * only last request's data will be propagated. + */ + propagateStatusAndHeaders?: boolean; } export interface CompositionRequestOptions { /** @@ -152,3 +160,8 @@ export interface CompositionRequestEngine extends RequestEngine { dropCache: NonNullable; destroy: NonNullable; } +export interface GatheredRequestsData { + data?: Dictionary; + headers?: RawHeaders; + status?: StatusCodes; +} diff --git a/lib/core/request/engines/fetch/index.js b/lib/core/request/engines/fetch/index.js index f7306836d..1e338f63f 100644 --- a/lib/core/request/engines/fetch/index.js +++ b/lib/core/request/engines/fetch/index.js @@ -31,10 +31,15 @@ const request = params => { } else if (p.credentials) { credentials = 'include'; } + let redirect = 'follow'; + if (Object.isString(p.redirect)) { + redirect = p.redirect; + } const fetchOpts = { body, headers, credentials, + redirect, method: p.method, signal: abortController.signal }; diff --git a/lib/core/request/engines/provider/index.js b/lib/core/request/engines/provider/index.js index 1790e03a9..478be1bdc 100644 --- a/lib/core/request/engines/provider/index.js +++ b/lib/core/request/engines/provider/index.js @@ -124,9 +124,15 @@ function createProviderEngine(src, methodsMapping = {}) { req.emitter.on(event, e => params.emitter.emit(event, e)); }); params.emitter.emit('drainListeners'); - const providerResObj = await req, - providerResponse = providerResObj.response; - const getResponse = () => providerResObj.data; + let providerResObj; + try { + providerResObj = await req; + } catch (err) { + reject(err); + return; + } + const providerResponse = providerResObj.response, + getResponse = () => providerResObj.data; getResponse[Symbol.asyncIterator] = () => { const type = providerResponse.sourceResponseType; if (!(`${type}Stream` in providerResponse)) { diff --git a/lib/core/request/interface.d.ts b/lib/core/request/interface.d.ts index ff7347f26..78722117b 100644 --- a/lib/core/request/interface.d.ts +++ b/lib/core/request/interface.d.ts @@ -590,6 +590,7 @@ export interface RequestOptions { readonly body?: RequestBody; readonly important?: boolean; readonly credentials?: boolean | RequestCredentials; + readonly redirect?: RequestRedirect; } /** * Request engine diff --git a/lib/core/request/response/index.js b/lib/core/request/response/index.js index 506970ad5..db5f7b759 100644 --- a/lib/core/request/response/index.js +++ b/lib/core/request/response/index.js @@ -120,7 +120,7 @@ let Response = (_dec = (0, _functools.deprecated)({ this.hasNoContent = (0, _helpers.statusesContainStatus)(noContent, this.status); this.headers = Object.freeze(new _headers.default(p.headers)); if (Object.isFunction(body)) { - this.body = body.once(); + this.body = (0, _functools.once)(body); this.body[Symbol.asyncIterator] = body[Symbol.asyncIterator].bind(body); } else { this.body = body; @@ -369,7 +369,7 @@ let Response = (_dec = (0, _functools.deprecated)({ } arrayBuffer() { return this.readBody().then(body => { - if (body == null) { + if (body == null || body === '') { return new ArrayBuffer(0); } if (body instanceof ArrayBuffer) { @@ -553,7 +553,7 @@ let Response = (_dec = (0, _functools.deprecated)({ }, this.parent); }); } -}, ((0, _applyDecoratedDescriptor2.default)(_class.prototype, "getHeader", [_dec], Object.getOwnPropertyDescriptor(_class.prototype, "getHeader"), _class.prototype), (0, _applyDecoratedDescriptor2.default)(_class.prototype, "json", [_functools.once], Object.getOwnPropertyDescriptor(_class.prototype, "json"), _class.prototype), (0, _applyDecoratedDescriptor2.default)(_class.prototype, "jsonStream", [_functools.once], Object.getOwnPropertyDescriptor(_class.prototype, "jsonStream"), _class.prototype), (0, _applyDecoratedDescriptor2.default)(_class.prototype, "formData", [_functools.once], Object.getOwnPropertyDescriptor(_class.prototype, "formData"), _class.prototype), (0, _applyDecoratedDescriptor2.default)(_class.prototype, "document", [_functools.once], Object.getOwnPropertyDescriptor(_class.prototype, "document"), _class.prototype), (0, _applyDecoratedDescriptor2.default)(_class.prototype, "text", [_functools.once], Object.getOwnPropertyDescriptor(_class.prototype, "text"), _class.prototype), (0, _applyDecoratedDescriptor2.default)(_class.prototype, "textStream", [_functools.once], Object.getOwnPropertyDescriptor(_class.prototype, "textStream"), _class.prototype), (0, _applyDecoratedDescriptor2.default)(_class.prototype, "stream", [_functools.once], Object.getOwnPropertyDescriptor(_class.prototype, "stream"), _class.prototype), (0, _applyDecoratedDescriptor2.default)(_class.prototype, "blob", [_functools.once], Object.getOwnPropertyDescriptor(_class.prototype, "blob"), _class.prototype), (0, _applyDecoratedDescriptor2.default)(_class.prototype, "arrayBuffer", [_functools.once], Object.getOwnPropertyDescriptor(_class.prototype, "arrayBuffer"), _class.prototype)), _class)); +}, ((0, _applyDecoratedDescriptor2.default)(_class.prototype, "getHeader", [_dec], Object.getOwnPropertyDescriptor(_class.prototype, "getHeader"), _class.prototype), (0, _applyDecoratedDescriptor2.default)(_class.prototype, "json", [_functools.onceDecorator], Object.getOwnPropertyDescriptor(_class.prototype, "json"), _class.prototype), (0, _applyDecoratedDescriptor2.default)(_class.prototype, "jsonStream", [_functools.onceDecorator], Object.getOwnPropertyDescriptor(_class.prototype, "jsonStream"), _class.prototype), (0, _applyDecoratedDescriptor2.default)(_class.prototype, "formData", [_functools.onceDecorator], Object.getOwnPropertyDescriptor(_class.prototype, "formData"), _class.prototype), (0, _applyDecoratedDescriptor2.default)(_class.prototype, "document", [_functools.onceDecorator], Object.getOwnPropertyDescriptor(_class.prototype, "document"), _class.prototype), (0, _applyDecoratedDescriptor2.default)(_class.prototype, "text", [_functools.onceDecorator], Object.getOwnPropertyDescriptor(_class.prototype, "text"), _class.prototype), (0, _applyDecoratedDescriptor2.default)(_class.prototype, "textStream", [_functools.onceDecorator], Object.getOwnPropertyDescriptor(_class.prototype, "textStream"), _class.prototype), (0, _applyDecoratedDescriptor2.default)(_class.prototype, "stream", [_functools.onceDecorator], Object.getOwnPropertyDescriptor(_class.prototype, "stream"), _class.prototype), (0, _applyDecoratedDescriptor2.default)(_class.prototype, "blob", [_functools.onceDecorator], Object.getOwnPropertyDescriptor(_class.prototype, "blob"), _class.prototype), (0, _applyDecoratedDescriptor2.default)(_class.prototype, "arrayBuffer", [_functools.onceDecorator], Object.getOwnPropertyDescriptor(_class.prototype, "arrayBuffer"), _class.prototype)), _class)); exports.default = Response; function fastClone(data) { return () => Object.fastClone(data, { diff --git a/lib/core/request/response/test/main.spec.d.ts b/lib/core/request/response/test/main.spec.d.ts new file mode 100644 index 000000000..cb0ff5c3b --- /dev/null +++ b/lib/core/request/response/test/main.spec.d.ts @@ -0,0 +1 @@ +export {}; diff --git a/lib/core/request/response/test/main.spec.js b/lib/core/request/response/test/main.spec.js new file mode 100644 index 000000000..6faa025b1 --- /dev/null +++ b/lib/core/request/response/test/main.spec.js @@ -0,0 +1,16 @@ +"use strict"; + +var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); +var _request = require("../../../../core/request"); +var _headers = _interopRequireDefault(require("../../../../core/request/headers")); +describe('core/request/response', () => { + test(['should successfully handle a request with the Content-Type: application/octet-stream header', 'and an empty response body'].join(' '), async () => { + const response = new _request.Response(Promise.resolve(''), { + url: 'url/url', + headers: new _headers.default({ + 'Content-Type': 'application/octet-stream' + }) + }); + await expect(response.decode()).resolves.toBeInstanceOf(ArrayBuffer); + }); +}); \ No newline at end of file diff --git a/lib/lang/interface.d.ts b/lib/lang/interface.d.ts index 421a793d8..1581c846d 100644 --- a/lib/lang/interface.d.ts +++ b/lib/lang/interface.d.ts @@ -9,6 +9,13 @@ export declare type LangPacs = { [key in Language]?: KeysetTranslations; }; export declare type Translation = string | PluralTranslation; -export declare type PluralTranslation = [one: string, some: string, many: string, none: string]; +export interface PluralTranslation { + one: string; + two?: string; + few?: string; + many?: string; + zero?: string; + other?: string; +} export declare type Translations = Dictionary; export declare type KeysetTranslations = Dictionary; diff --git a/ts-definitions/prelude/function/proto.d.ts b/ts-definitions/prelude/function/proto.d.ts index 34c25a6fe..fc280a32b 100644 --- a/ts-definitions/prelude/function/proto.d.ts +++ b/ts-definitions/prelude/function/proto.d.ts @@ -9,10 +9,10 @@ */ interface Function { - /** - * Returns a new function that allows to invoke the target function only once - */ - once(this: T): T; + // /** + // * Returns a new function that allows to invoke the target function only once + // */ + // once(this: T): T; /** * Cancels the memoization of the function result diff --git a/ts-definitions/prelude/function/static.d.ts b/ts-definitions/prelude/function/static.d.ts index 95270afdb..ef64a52d4 100644 --- a/ts-definitions/prelude/function/static.d.ts +++ b/ts-definitions/prelude/function/static.d.ts @@ -23,11 +23,11 @@ interface FunctionConstructor { */ __: TB.__; - /** - * Returns a new function that allows to invoke the specified function only once - * @param fn - */ - once(fn: T): T; + // /** + // * Returns a new function that allows to invoke the specified function only once + // * @param fn + // */ + // once(fn: T): T; /** * Cancels the memoization of the function result