From 03a261de8a623ee442b9a0cddd5b7bbdc04cb45e Mon Sep 17 00:00:00 2001 From: Seyyed Morteza Moosavi Date: Sun, 6 Feb 2022 16:45:39 +0330 Subject: [PATCH] feat: add id to state wires --- index.d.ts | 32 +++++++++++++- src/index.test.ts | 4 ++ src/index.ts | 1 + .../create-state-selector.test.ts | 23 ++++++++++ src/state-selector/create-state-selector.ts | 34 ++++++++++++++- src/state-wire/create-state-wire.test.ts | 35 +++++++++++++++ src/state-wire/create-state-wire.ts | 17 +++++++- src/state-wire/readonly-state-wire.ts | 5 ++- src/state-wire/state-wire.ts | 3 +- src/types.d.ts | 1 + src/utils/memoize.test.ts | 43 +++++++++++++++++++ src/utils/memoize.ts | 16 +++++++ src/utils/wire-id.test.ts | 41 ++++++++++++++++++ src/utils/wire-id.ts | 31 +++++++++++++ 14 files changed, 279 insertions(+), 7 deletions(-) create mode 100644 src/state-wire/create-state-wire.test.ts create mode 100644 src/utils/memoize.test.ts create mode 100644 src/utils/memoize.ts create mode 100644 src/utils/wire-id.test.ts create mode 100644 src/utils/wire-id.ts diff --git a/index.d.ts b/index.d.ts index 021c62e..25902e9 100644 --- a/index.d.ts +++ b/index.d.ts @@ -39,6 +39,20 @@ declare interface FnsWireGuard { ' fns-wire': StrictMethodsGuard; } +export declare function getLinkIds(wire: undefined): undefined; + +export declare function getLinkIds(wire: WireId): LinkIds; + +export declare function getLinkIds( + wire: WireId | undefined, +): LinkIds | undefined; + +export declare function getWireId(wire: undefined): undefined; + +export declare function getWireId(wire: WireId): string; + +export declare function getWireId(wire: WireId | undefined): string | undefined; + declare type GetWireValue = (wire: ReadonlyStateWire) => V; declare type InitializerOrValue = V | (() => V); @@ -61,6 +75,8 @@ declare type IsNever = [T] extends [never] ? true : false; declare type KeyOfMethods = NonNever>; +export declare type LinkIds = Array; + declare type MethodKeys = { [P in keyof T]: T[P] extends Function ? P : never; }[keyof T]; @@ -74,7 +90,8 @@ export declare interface ReadOnlySelectorOptions { } export declare interface ReadonlyStateWire - extends ReadonlyStateWireGuard { + extends ReadonlyStateWireGuard, + WireId { /** * get current value * @returns current value @@ -97,7 +114,7 @@ export declare type ReadonlyWire = ReadonlyStateWire & declare type SetWireValue = (wire: StateWire, value: Defined) => void; -export declare interface StateWire extends StateWireGuard { +export declare interface StateWire extends StateWireGuard, WireId { /** * get current value * @returns current value @@ -273,6 +290,17 @@ export declare type WireFns> = W extends FnsWire< ? Fns : never; +export declare interface WireId { + /** + * @internal + */ + _getId(): string; + /** + * @internal + */ + _getLinkIds(): LinkIds; +} + export declare type WireState< W extends ReadonlyStateWire > = W extends ReadonlyStateWire ? V : never; diff --git a/src/index.test.ts b/src/index.test.ts index 10039a0..f867aff 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,6 +1,8 @@ import { createSelector, createWire, + getLinkIds, + getWireId, isDefined, useFn, useInterceptor, @@ -23,5 +25,7 @@ describe('index file', () => { expect(useSelector).toBeDefined(); expect(useSubscribe).toBeDefined(); expect(isDefined).toBeDefined(); + expect(getLinkIds).toBeDefined(); + expect(getWireId).toBeDefined(); }); }); diff --git a/src/index.ts b/src/index.ts index 6745c78..452391e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,3 +17,4 @@ export { useSelector } from './selector/use-selector'; export { useSubscribe } from './state-wire/use-subscribe'; export { isDefined } from './utils/type-utils'; +export { getLinkIds, getWireId } from './utils/wire-id'; diff --git a/src/state-selector/create-state-selector.test.ts b/src/state-selector/create-state-selector.test.ts index fc1b129..b965d4f 100644 --- a/src/state-selector/create-state-selector.test.ts +++ b/src/state-selector/create-state-selector.test.ts @@ -1,4 +1,5 @@ import { createStateWire } from '../state-wire/create-state-wire'; +import { getLinkIds, getWireId } from '../utils/wire-id'; import { createStateSelector } from './create-state-selector'; const createDoubleValueSelector = () => { @@ -323,4 +324,26 @@ describe('selector', () => { expect(selector.getValue()).toBe(12); }); }); + describe('ids', function () { + it('should have same id', function () { + const [stateSelector] = createStateSelector({ get: () => 1 }); + const id = getWireId(stateSelector); + expect(getWireId(stateSelector)).toBe(id); + }); + it('should be last item of link ids', function () { + const [stateSelector] = createStateSelector({ get: () => 1 }); + const id = getWireId(stateSelector); + const ids = getLinkIds(stateSelector); + expect(getLinkIds(stateSelector)).toBe(ids); + expect(getLinkIds(stateSelector)).toEqual([id]); + }); + it('should be have up links', function () { + const { selector, wire } = createDoubleValueSelector(); + const wireId = getWireId(wire); + const selectorId = getWireId(selector); + const ids = getLinkIds(selector); + expect(getLinkIds(selector)).toBe(ids); + expect(getLinkIds(selector)).toEqual([[wireId], selectorId]); + }); + }); }); diff --git a/src/state-selector/create-state-selector.ts b/src/state-selector/create-state-selector.ts index 05d9668..695cfa5 100644 --- a/src/state-selector/create-state-selector.ts +++ b/src/state-selector/create-state-selector.ts @@ -5,7 +5,9 @@ import { } from '../state-wire/readonly-state-wire'; import { createStateWireGuard } from '../state-wire/state-wire'; import { StateWire } from '../types'; +import { memoize } from '../utils/memoize'; import { Defined } from '../utils/type-utils'; +import { createId } from '../utils/wire-id'; type ReconnectFunction = (options?: O) => void; type ConnectFunction = () => ReconnectFunction; @@ -146,9 +148,37 @@ export function createStateSelector( }; }; + const id = createId('s-'); + const _getId = () => id; + + // it should memoize for each selector instance, do not move to top level + const memoizedGetLinkIds = memoize((wires: UnsubscribeMap) => { + return [ + ...Array.from(wires.keys()).map((wire) => wire._getLinkIds()), + _getId(), + ]; + }); + + const _getLinkIds = () => { + return memoizedGetLinkIds(activeWires.current); + }; + const selectorContext: ReadonlyStateWire | StateWire = setOption - ? { getValue, subscribe, setValue, ...createStateWireGuard() } - : { getValue, subscribe, ...createReadonlyStateWireGuard() }; + ? { + getValue, + subscribe, + setValue, + ...createStateWireGuard(), + _getId, + _getLinkIds, + } + : { + getValue, + subscribe, + ...createReadonlyStateWireGuard(), + _getId, + _getLinkIds, + }; return [selectorContext, connect]; } diff --git a/src/state-wire/create-state-wire.test.ts b/src/state-wire/create-state-wire.test.ts new file mode 100644 index 0000000..560f76e --- /dev/null +++ b/src/state-wire/create-state-wire.test.ts @@ -0,0 +1,35 @@ +import { getLinkIds, getWireId } from '../utils/wire-id'; +import { createStateWire } from './create-state-wire'; + +describe('create state wire', function () { + it('should create state wire', function () { + const stateWire = createStateWire({}, 1); + expect(stateWire).toBeDefined(); + }); + it('should have same id', function () { + const [stateWire] = createStateWire({}, 1); + const id = getWireId(stateWire); + expect(getWireId(stateWire)).toBe(id); + }); + it('should be last item of link ids', function () { + const [stateWire] = createStateWire({}, 1); + const id = getWireId(stateWire); + expect(getLinkIds(stateWire)).toEqual([id]); + }); + it('should have uplink id in link ids', function () { + const [upLink] = createStateWire({}, 1); + const [stateWire] = createStateWire(upLink, 1); + const uplinkId = getWireId(upLink); + const id = getWireId(stateWire); + expect(getLinkIds(stateWire)).toEqual([uplinkId, id]); + }); + it('should have uplink id in link ids', function () { + const [upLink] = createStateWire({}, 1); + const [stateWire] = createStateWire(upLink, 1); + const uplinkId = getWireId(upLink); + const id = getWireId(stateWire); + const ids = getLinkIds(stateWire); + expect(getLinkIds(stateWire)).toBe(ids); + expect(getLinkIds(stateWire)).toEqual([uplinkId, id]); + }); +}); diff --git a/src/state-wire/create-state-wire.ts b/src/state-wire/create-state-wire.ts index 24bfcc5..a825e14 100644 --- a/src/state-wire/create-state-wire.ts +++ b/src/state-wire/create-state-wire.ts @@ -1,5 +1,7 @@ import mitt, { Emitter } from 'mitt'; +import { memoize } from '../utils/memoize'; import { Defined, isDefined } from '../utils/type-utils'; +import { createId } from '../utils/wire-id'; import { createStateWireGuard, isWritableStateWire, @@ -79,5 +81,18 @@ export function createStateWire( subscribe, }; - return [{ ...stateContext, ...createStateWireGuard() }, connect]; + const id = createId('w-'); + const _getId = () => id; + // it should memoize for each wire instance, do not move to top level + const memoizedGetLinkIds = memoize( + (upLink: StateWire | null) => { + return upLink ? [...upLink._getLinkIds(), _getId()] : [_getId()]; + }, + ); + const _getLinkIds = () => memoizedGetLinkIds(upLink); + + return [ + { ...stateContext, ...createStateWireGuard(), _getId, _getLinkIds }, + connect, + ]; } diff --git a/src/state-wire/readonly-state-wire.ts b/src/state-wire/readonly-state-wire.ts index f0d7089..85031d1 100644 --- a/src/state-wire/readonly-state-wire.ts +++ b/src/state-wire/readonly-state-wire.ts @@ -1,10 +1,13 @@ import { CovarianceGuard, Defined } from '../utils/type-utils'; +import { WireId } from '../utils/wire-id'; export interface ReadonlyStateWireGuard { ' state-wire': CovarianceGuard; } -export interface ReadonlyStateWire extends ReadonlyStateWireGuard { +export interface ReadonlyStateWire + extends ReadonlyStateWireGuard, + WireId { /** * get current value * @returns current value diff --git a/src/state-wire/state-wire.ts b/src/state-wire/state-wire.ts index fc5ee33..87e4bf6 100644 --- a/src/state-wire/state-wire.ts +++ b/src/state-wire/state-wire.ts @@ -1,10 +1,11 @@ import { Defined, StrictGuard } from '../utils/type-utils'; +import { WireId } from '../utils/wire-id'; export interface StateWireGuard { ' state-wire': StrictGuard; } -export interface StateWire extends StateWireGuard { +export interface StateWire extends StateWireGuard, WireId { /** * get current value * @returns current value diff --git a/src/types.d.ts b/src/types.d.ts index fd940b3..365e588 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -9,3 +9,4 @@ export { } from './state-selector/create-state-selector'; export type { Defined } from './utils/type-utils'; +export type { WireId, LinkIds } from './utils/wire-id'; diff --git a/src/utils/memoize.test.ts b/src/utils/memoize.test.ts new file mode 100644 index 0000000..b6e5e8d --- /dev/null +++ b/src/utils/memoize.test.ts @@ -0,0 +1,43 @@ +import { memoize } from './memoize'; + +describe('memoize', function () { + it('should return a function', function () { + expect(typeof memoize(() => {})).toBe('function'); + }); + it('should remember last call', function () { + const fn = jest.fn(); + const memoized = memoize(fn); + memoized(); + memoized(); + expect(fn).toHaveBeenCalledTimes(1); + }); + it('should remember last call with arguments', function () { + const fn = jest.fn(); + const memoized = memoize(fn); + memoized(1, 2, 3); + memoized(1, 2, 3); + expect(fn).toHaveBeenCalledTimes(1); + }); + it('should remember last call with different arguments', function () { + const fn = jest.fn(); + const memoized = memoize(fn); + memoized(1, 2, 3); + memoized(4, 5, 6); + expect(fn).toHaveBeenCalledTimes(2); + }); + it('should forget previous calls with different arguments', function () { + const fn = jest.fn(); + const memoized = memoize(fn); + memoized(1, 2, 3); + memoized(4, 5, 6); + memoized(1, 2, 3); + expect(fn).toHaveBeenCalledTimes(3); + }); + it('should returns memoized value', function () { + const fn = jest.fn(() => []); + const memoized = memoize(fn); + const result1 = memoized(); + const result2 = memoized(); + expect(result1).toBe(result2); + }); +}); diff --git a/src/utils/memoize.ts b/src/utils/memoize.ts new file mode 100644 index 0000000..39d24a6 --- /dev/null +++ b/src/utils/memoize.ts @@ -0,0 +1,16 @@ +export function memoize(fn: F): F { + let lastArgs: any[] | undefined = undefined; + let lastResult: unknown = undefined; + return function (...args: any[]): any { + if ( + lastArgs !== undefined && + lastArgs.length === args.length && + lastArgs.every((arg, i) => arg === args[i]) + ) { + return lastResult; + } + lastArgs = args; + lastResult = fn(...args); + return lastResult; + } as any; +} diff --git a/src/utils/wire-id.test.ts b/src/utils/wire-id.test.ts new file mode 100644 index 0000000..476eb56 --- /dev/null +++ b/src/utils/wire-id.test.ts @@ -0,0 +1,41 @@ +/*eslint @typescript-eslint/no-unused-vars: ["warn", { "varsIgnorePattern": "^_" }]*/ +import { createId, getLinkIds, getWireId, LinkIds, WireId } from './wire-id'; + +describe('create id', function () { + it('should create a valid id', function () { + const id = createId(); + expect(id).toBeDefined(); + expect(id.length).toBeGreaterThan(0); + }); + it('should accepts prefix', function () { + const id = createId('prefix'); + expect(id).toBeDefined(); + expect(id.length).toBeGreaterThan(0); + expect(id.startsWith('prefix')).toBeTruthy(); + }); +}); + +describe('get wire id', function () { + it('should returns undefined for undefined', function () { + expect(getWireId(undefined)).toBeUndefined(); + }); + it('have correct type', function () { + function _f0(w0: undefined, w1: WireId, w2: WireId | undefined) { + let _r0: undefined = getWireId(w0); + let _r1: string = getWireId(w1); + let _r2: string | undefined = getWireId(w2); + } + }); +}); +describe('get link ids', function () { + it('should returns undefined for undefined', function () { + expect(getLinkIds(undefined)).toBeUndefined(); + }); + it('have correct type', function () { + function _f0(w0: undefined, w1: WireId, w2: WireId | undefined) { + let _r0: undefined = getLinkIds(w0); + let _r1: LinkIds = getLinkIds(w1); + let _r2: LinkIds | undefined = getLinkIds(w2); + } + }); +}); diff --git a/src/utils/wire-id.ts b/src/utils/wire-id.ts new file mode 100644 index 0000000..b6c0873 --- /dev/null +++ b/src/utils/wire-id.ts @@ -0,0 +1,31 @@ +export type LinkIds = Array; +export interface WireId { + /** + * @internal + */ + _getId(): string; + /** + * @internal + */ + _getLinkIds(): LinkIds; +} + +let lastId = 0; + +export function createId(prefix: string = ''): string { + const id = lastId++; + return `${prefix}${id}`; +} + +export function getWireId(wire: undefined): undefined; +export function getWireId(wire: WireId): string; +export function getWireId(wire: WireId | undefined): string | undefined; +export function getWireId(wire: WireId | undefined): string | undefined { + return wire?._getId() ?? undefined; +} +export function getLinkIds(wire: undefined): undefined; +export function getLinkIds(wire: WireId): LinkIds; +export function getLinkIds(wire: WireId | undefined): LinkIds | undefined; +export function getLinkIds(wire: WireId | undefined): LinkIds | undefined { + return wire?._getLinkIds() ?? undefined; +}