From 60df4c38297fa181757ec5092d42e3a4ce57cc14 Mon Sep 17 00:00:00 2001 From: gretzkiy Date: Wed, 25 Dec 2024 10:38:06 +0300 Subject: [PATCH] fix!: remove once from Function prototype --- CHANGELOG.md | 9 +++- src/core/async/events/index.ts | 4 +- src/core/async/events/interface.ts | 2 +- src/core/functools/CHANGELOG.md | 7 +++ src/core/functools/memoize.ts | 40 +++++++++++++++-- src/core/functools/spec.ts | 43 +++++++++++++++++++ .../prelude/function/memoize/CHANGELOG.md | 6 +++ src/core/prelude/function/memoize/index.ts | 39 ----------------- src/core/prelude/function/memoize/spec.js | 35 --------------- src/core/request/response/index.ts | 22 +++++----- 10 files changed, 114 insertions(+), 93 deletions(-) delete mode 100644 src/core/prelude/function/memoize/spec.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 23d7f0f4e..b0d597f90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,9 +11,16 @@ Changelog _Note: Gaps between patch versions are faulty, broken or test releases._ +## v4.0.0-alpha.54 (2024-12-25) + +#### :boom: Breaking Change + +* Removed `once` from `Function` prototype. Now `once` is a separate wrapper. `core/prelude/function/memoize` +* Renamed exported `once` decorator to `onceDecorator`. `core/functools/memoize` + ## v4.0.0-alpha.53 (2024-12-16) -#### :bug: Bug Fix +#### :bug: Bug Fix * Added handling the rejection of provider in provider request engine `core/request/engines/provider` diff --git a/src/core/async/events/index.ts b/src/core/async/events/index.ts index e88a4fbad..6fe81a4d4 100644 --- a/src/core/async/events/index.ts +++ b/src/core/async/events/index.ts @@ -140,7 +140,7 @@ export default class Async> extends Super { wrapper(cb: AnyFunction): unknown { if (Object.isFunction(originalEmitter)) { // eslint-disable-next-line func-name-matching - emitter = function wrappedEmitter(this: unknown): CanUndef { + emitter = function wrappedEmitter(this: unknown): CanUndef { // eslint-disable-next-line prefer-rest-params const destructor = originalEmitter.apply(this, arguments); @@ -182,7 +182,7 @@ export default class Async> extends Super { }; function handler(this: unknown, ...handlerArgs: unknown[]): unknown { - if (p.single && (hasMultipleEvent || !emitter.once)) { + if (p.single && (hasMultipleEvent || !('once' in emitter))) { if (hasMultipleEvent) { that.clearEventListener(ids); diff --git a/src/core/async/events/interface.ts b/src/core/async/events/interface.ts index b5268eaf2..e89ebb528 100644 --- a/src/core/async/events/interface.ts +++ b/src/core/async/events/interface.ts @@ -49,7 +49,7 @@ export interface EventEmitterLike { /** * Extended type of event emitter */ -export type EventEmitterLikeP = ((event: string, handler: Function) => CanUndef) | EventEmitterLike; +export type EventEmitterLikeP = ((event: string, handler: AnyFunction) => CanUndef) | EventEmitterLike; export interface AsyncOnOptions extends AsyncCbOptionsSingle { /** diff --git a/src/core/functools/CHANGELOG.md b/src/core/functools/CHANGELOG.md index 5455923af..f0bb85c3a 100644 --- a/src/core/functools/CHANGELOG.md +++ b/src/core/functools/CHANGELOG.md @@ -9,6 +9,13 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v4.0.0-alpha.54 (2024-12-25) + +#### :boom: Breaking Change + +* Removed `once` from `Function` prototype. Now `once` is a separate wrapper +* Renamed exported `once` decorator to `onceDecorator` + ## v4.0.0-alpha.29 (2024-04-04) #### :bug: Bug Fix diff --git a/src/core/functools/memoize.ts b/src/core/functools/memoize.ts index 6f6aee71c..b09f8c0ca 100644 --- a/src/core/functools/memoize.ts +++ b/src/core/functools/memoize.ts @@ -7,12 +7,12 @@ */ /** - * Decorator for `Function.prototype.once` + * Decorator for {@link once} * * @decorator - * @see [[Function.once]] + * @see once */ -export function once(target: object, key: string | symbol, descriptor: PropertyDescriptor): void { +export function onceDecorator(target: object, key: string | symbol, descriptor: PropertyDescriptor): void { const method = descriptor.value; @@ -23,9 +23,41 @@ export function once(target: object, key: string | symbol, descriptor: PropertyD descriptor.value = function value(this: object, ...args: unknown[]): unknown { Object.defineProperty(this, key, { configurable: true, - value: method.once() + value: once(method) }); return this[key](...args); }; } + +/** + * Returns a new function that allows to invoke the specified function only once + * @param fn + */ +export function once(fn: T): T { + let + called = false, + result; + + Object.defineProperty(onceWrapper, 'cancelOnce', { + configurable: true, + enumerable: false, + writable: true, + value: () => { + called = true; + result = undefined; + } + }); + + return onceWrapper; + + function onceWrapper(this: unknown, ...args: unknown[]): unknown { + if (!called) { + result = fn.apply(this, args); + called = true; + } + + return result; + } +} + diff --git a/src/core/functools/spec.ts b/src/core/functools/spec.ts index c366b8f27..04c741581 100644 --- a/src/core/functools/spec.ts +++ b/src/core/functools/spec.ts @@ -7,8 +7,51 @@ */ import { debounce } from 'core/functools/lazy'; +import { once } from 'core/functools/memoize'; describe('core/functools', () => { + describe('`once`', () => { + it('should memoize the return result', () => { + const + rand = 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 = 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 = 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 = once(testFn); + + expect(onceFn(1)).toBe(1); + expect(onceFn(2)).toBe(1); + + onceFn.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) => { class Input { diff --git a/src/core/prelude/function/memoize/CHANGELOG.md b/src/core/prelude/function/memoize/CHANGELOG.md index 496ec4aee..030eae2ca 100644 --- a/src/core/prelude/function/memoize/CHANGELOG.md +++ b/src/core/prelude/function/memoize/CHANGELOG.md @@ -9,6 +9,12 @@ Changelog > - :house: [Internal] > - :nail_care: [Polish] +## v4.0.0-alpha.54 (2024-12-25) + +#### :boom: Breaking Change + +* Removed `once` from `Function` prototype. Now `once` is a separate wrapper + ## v3.20.0 (2020-07-05) #### :rocket: New Feature diff --git a/src/core/prelude/function/memoize/index.ts b/src/core/prelude/function/memoize/index.ts index b1918486d..8367c4b21 100644 --- a/src/core/prelude/function/memoize/index.ts +++ b/src/core/prelude/function/memoize/index.ts @@ -8,44 +8,5 @@ import extend from 'core/prelude/extend'; -/** @see [[Function.once]] */ -extend(Function.prototype, 'once', function once(this: AnyFunction): AnyFunction { - const - // eslint-disable-next-line @typescript-eslint/no-this-alias - 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(this: unknown, ...args: unknown[]): unknown { - if (called) { - return res; - } - - res = fn.apply(this, args); - called = true; - return res; - } -}); - /** @see [[Function.cancelOnce]] */ extend(Function.prototype, 'cancelOnce', () => undefined); - -/** @see [[FunctionConstructor.once]] */ -extend(Function, 'once', (fn: AnyFunction) => fn.once()); - -/** @see [[FunctionConstructor.cancelOnce]] */ -extend(Function, 'cancelOnce', (fn: AnyFunction) => fn.cancelOnce()); diff --git a/src/core/prelude/function/memoize/spec.js b/src/core/prelude/function/memoize/spec.js deleted file mode 100644 index da18c5def..000000000 --- a/src/core/prelude/function/memoize/spec.js +++ /dev/null @@ -1,35 +0,0 @@ -/*! - * V4Fire Core - * https://github.com/V4Fire/Core - * - * Released under the MIT license - * https://github.com/V4Fire/Core/blob/master/LICENSE - */ - -describe('core/prelude/function/memoize', () => { - it('`once`', () => { - const - rand = Math.random.once(), - res = rand(); - - expect(Object.isNumber(res)).toBe(true); - expect(rand()).toBe(res); - expect(rand()).toBe(res); - }); - - it('`once` with arguments', () => { - const fn = ((i) => i).once(); - expect(fn(1)).toBe(1); - expect(fn(2)).toBe(1); - }); - - it('`Function.once`', () => { - const - rand = Function.once(Math.random), - res = rand(); - - expect(Object.isNumber(res)).toBe(true); - expect(rand()).toBe(res); - expect(rand()).toBe(res); - }); -}); diff --git a/src/core/request/response/index.ts b/src/core/request/response/index.ts index e03ee365a..519321c7c 100644 --- a/src/core/request/response/index.ts +++ b/src/core/request/response/index.ts @@ -12,7 +12,7 @@ */ import { EventEmitter2 as EventEmitter } from 'eventemitter2'; -import { once, deprecated } from 'core/functools'; +import { onceDecorator, once, deprecated } from 'core/functools'; import { IS_NODE } from 'core/env'; import { convertIfDate } from 'core/json'; @@ -253,7 +253,7 @@ export default class Response< this.headers = Object.freeze(new Headers(p.headers)); if (Object.isFunction(body)) { - this.body = body.once(); + this.body = once(body); this.body[Symbol.asyncIterator] = body[Symbol.asyncIterator].bind(body); } else { @@ -442,7 +442,7 @@ export default class Response< /** * Parses the response body as a JSON object and returns it */ - @once + @onceDecorator json(): AbortablePromise { return this.readBody().then((body) => { if (body == null) { @@ -494,7 +494,7 @@ export default class Response< /** * Parses the response data stream as a JSON tokens and yields them via an asynchronous iterator */ - @once + @onceDecorator jsonStream(): AsyncIterableIterator { const iter = Parser.from(this.textStream()); @@ -511,7 +511,7 @@ export default class Response< /** * Parses the response body as a FormData object and returns it */ - @once + @onceDecorator formData(): AbortablePromise { const that = this; @@ -565,7 +565,7 @@ export default class Response< /** * Parses the response body as a Document instance and returns it */ - @once + @onceDecorator document(): AbortablePromise { return this.readBody().then((body) => { //#if node_js @@ -595,7 +595,7 @@ export default class Response< /** * Parses the response body as a string and returns it */ - @once + @onceDecorator text(): AbortablePromise { return this.readBody().then((body) => this.decodeToString(body)); } @@ -603,7 +603,7 @@ export default class Response< /** * Parses the response data stream as a text chunks and yields them via an asynchronous iterator */ - @once + @onceDecorator textStream(): AsyncIterableIterator { const iter = this.stream(); @@ -628,7 +628,7 @@ export default class Response< /** * Parses the response data stream as an ArrayBuffer chunks and yields them via an asynchronous iterator */ - @once + @onceDecorator stream(): AsyncIterableIterator { const iter = this[Symbol.asyncIterator](); @@ -653,7 +653,7 @@ export default class Response< /** * Parses the response body as a Blob structure and returns it */ - @once + @onceDecorator blob(): AbortablePromise { return this.readBody().then((body) => this.decodeToBlob(body)); } @@ -661,7 +661,7 @@ export default class Response< /** * Parses the response body as an ArrayBuffer and returns it */ - @once + @onceDecorator arrayBuffer(): AbortablePromise { return this.readBody().then((body) => { if (body == null || body === '') {