From 24e32d0c050037c3894e05b27b9d8ca114114f5f Mon Sep 17 00:00:00 2001 From: Ivan Koryakovtsev Date: Fri, 18 Aug 2023 12:10:41 +0100 Subject: [PATCH] chore: update eslint, prettier configs, apply them --- .eslintrc | 2 +- .prettierignore | 8 + .prettierrc | 10 - .prettierrc.js | 1 + package.json | 123 +- packages/atom/src/atom/atom.ts | 314 ++-- packages/atom/src/atom/base.ts | 898 +++++----- packages/atom/src/atom/index.test.ts | 1482 ++++++++-------- .../atom/src/lenses/by-index/index.test.ts | 36 +- packages/atom/src/lenses/by-index/index.ts | 16 +- .../src/lenses/by-key-immutable/index.test.ts | 38 +- .../atom/src/lenses/by-key-immutable/index.ts | 16 +- packages/atom/src/lenses/by-key/index.test.ts | 38 +- packages/atom/src/lenses/by-key/index.ts | 26 +- packages/atom/src/lenses/equity/index.test.ts | 24 +- packages/atom/src/lenses/equity/index.ts | 14 +- packages/cache/src/domain.ts | 70 +- packages/cache/src/key-memo/index.test.ts | 225 +-- packages/cache/src/key-memo/index.ts | 107 +- packages/cache/src/memo/index.test.ts | 285 +-- packages/cache/src/memo/index.ts | 202 +-- packages/cache/src/utils/batcher.ts | 26 +- packages/cache/src/utils/errors.ts | 18 +- .../run-promise-with-cache/index.test.ts | 65 +- .../src/utils/run-promise-with-cache/index.ts | 27 +- packages/cache/src/utils/save/index.test.ts | 81 +- packages/cache/src/utils/save/index.ts | 8 +- packages/cache/src/utils/to-cache.ts | 44 +- .../src/utils/to-list-loader/index.test.ts | 46 +- .../cache/src/utils/to-list-loader/index.ts | 48 +- packages/form-store/src/domain.ts | 18 +- packages/form-store/src/index.spec.ts | 343 ++-- packages/form-store/src/index.ts | 70 +- packages/form-store/src/is-submit-disabled.ts | 11 +- .../src/utils/create-validation-result.ts | 56 +- .../form-store/src/utils/validate-joi.spec.ts | 226 +-- packages/form-store/src/utils/validate-joi.ts | 58 +- packages/lens/src/base.ts | 292 +-- packages/lens/src/equals.ts | 432 ++--- packages/lens/src/json.ts | 169 +- packages/lens/src/lens.test.ts | 324 ++-- packages/lens/src/simple-cache.ts | 24 +- packages/lens/src/utils.ts | 32 +- packages/list-react/src/domain.ts | 10 +- .../list-react/src/grid/index.stories.tsx | 80 +- packages/list-react/src/grid/index.tsx | 316 ++-- packages/list-react/src/index.ts | 12 +- packages/list-react/src/rx.tsx | 83 +- packages/list-react/src/utils.ts | 2 +- .../list-react/src/vertical/index.stories.tsx | 85 +- packages/list-react/src/vertical/index.tsx | 232 +-- packages/list/src/domain.ts | 38 +- packages/list/src/index.ts | 16 +- packages/list/src/infinite-list.test.ts | 214 +-- packages/list/src/infinite-list.ts | 256 ++- packages/list/src/reactive-list.ts | 61 +- packages/list/src/utils.ts | 63 +- packages/list/test/utils/range.ts | 2 +- packages/react/src/base.ts | 302 ++-- packages/react/src/lift.test.tsx | 92 +- packages/react/src/lift.tsx | 40 +- packages/react/src/rx-html.test.tsx | 109 +- packages/react/src/rx-html.tsx | 292 +-- packages/react/src/rx-if.test.tsx | 128 +- packages/react/src/rx-if.tsx | 38 +- packages/react/src/rx-wrapper.test.tsx | 251 +-- packages/react/src/rx-wrapper.ts | 32 +- packages/react/src/rx.test.tsx | 452 ++--- packages/react/src/rx.tsx | 97 +- packages/react/src/use-rx.test.tsx | 103 +- packages/react/src/use-rx.ts | 85 +- packages/react/src/use-subscription.ts | 31 +- packages/wrapped/src/domain.ts | 31 +- packages/wrapped/src/operators/index.test.ts | 641 +++---- packages/wrapped/src/operators/index.ts | 232 +-- packages/wrapped/src/ow/index.test.ts | 187 +- packages/wrapped/src/ow/index.ts | 65 +- packages/wrapped/src/rx-object/index.test.ts | 50 +- packages/wrapped/src/rx-object/index.ts | 40 +- packages/wrapped/src/utils.ts | 11 +- yarn.lock | 1575 ++++++++--------- 81 files changed, 6367 insertions(+), 6340 deletions(-) create mode 100644 .prettierignore delete mode 100644 .prettierrc create mode 100644 .prettierrc.js diff --git a/.eslintrc b/.eslintrc index f101930..f394c3a 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,4 +1,4 @@ { "root": true, - "extends": ["@roborox/default", "prettier", "prettier/@typescript-eslint"] + "extends": ["@rarible/eslint-config-ts", "prettier"] } diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..31ab05d --- /dev/null +++ b/.prettierignore @@ -0,0 +1,8 @@ +node_modules +.husky +.vscode +.yarn +.idea +build +*.md +*.d.ts \ No newline at end of file diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 0fae9f5..0000000 --- a/.prettierrc +++ /dev/null @@ -1,10 +0,0 @@ -{ - "trailingComma": "es5", - "tabWidth": 2, - "useTabs": true, - "semi": false, - "singleQuote": false, - "printWidth": 120, - "arrowParens": "avoid", - "jsxBracketSameLine": false -} diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000..a981210 --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1 @@ +module.exports = require("@rarible/prettier"); diff --git a/package.json b/package.json index 31849c6..d07fa15 100644 --- a/package.json +++ b/package.json @@ -1,61 +1,66 @@ { - "name": "root", - "private": true, - "workspaces": [ - "packages/*" - ], - "scripts": { - "bootstrap": "lerna bootstrap && lerna link", - "build": "sh build-all.sh", - "build:atom": "yarn workspace @rixio/atom run build", - "build:cache": "yarn workspace @rixio/cache run build", - "build:form-store": "yarn workspace @rixio/form-store run build", - "build:lens": "yarn workspace @rixio/lens run build", - "build:list": "yarn workspace @rixio/list run build", - "build:list-react": "yarn workspace @rixio/list-react run build", - "build:react": "yarn workspace @rixio/react run build", - "build:wrapped": "yarn workspace @rixio/wrapped run build", - "clean": "yarn workspaces run clean", - "link": "lerna link", - "lint": "eslint --ext .js,.jsx,.ts,.tsx ./", - "prettify": "run-s prettify:*", - "prettify:code": "prettier --write **/src/**/*", - "prettify:packages": "lerna exec -- sort-package-json && sort-package-json", - "release": "lerna publish from-package", - "test": "yarn workspaces run test", - "verify": "yarn workspaces run verify", - "version": "lerna version" - }, - "lint-staged": { - "**/src/package.json": [ - "yarn run prettify:packages" - ], - "package.json": [ - "yarn run prettify:packages" - ] - }, - "devDependencies": { - "@roborox/eslint-config-default": "^2.2.0", - "@testing-library/jest-dom": "^5.16.5", - "@testing-library/react": "^12.1.5", - "@types/jest": "^29.5.0", - "@types/react": "^16.9.37", - "@types/react-dom": "^16.9.8", - "@types/react-virtualized": "9.21.10", - "eslint": "^7.10.0", - "eslint-config-prettier": "^7.2.0", - "husky": "^8.0.3", - "jest": "^29.5.0", - "jest-environment-jsdom": "^29.5.0", - "joi": "^17.9.1", - "lerna": "^6.6.1", - "lint-staged": "^10.4.0", - "prettier": "^2.1.2", - "react": "^16.13.1", - "react-dom": "16.13.1", - "rxjs": "^6.6.7", - "sort-package-json": "^1.50.0", - "ts-jest": "^29.1.0", - "typescript": "^5.0.3" - } + "name": "root", + "private": true, + "workspaces": [ + "packages/*" + ], + "scripts": { + "bootstrap": "lerna bootstrap && lerna link", + "build": "sh build-all.sh", + "build:atom": "yarn workspace @rixio/atom run build", + "build:cache": "yarn workspace @rixio/cache run build", + "build:form-store": "yarn workspace @rixio/form-store run build", + "build:lens": "yarn workspace @rixio/lens run build", + "build:list": "yarn workspace @rixio/list run build", + "build:list-react": "yarn workspace @rixio/list-react run build", + "build:react": "yarn workspace @rixio/react run build", + "build:wrapped": "yarn workspace @rixio/wrapped run build", + "clean": "yarn workspaces run clean", + "link": "lerna link", + "lint": "eslint --ext .js,.jsx,.ts,.tsx ./", + "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx ./ --fix", + "prepare": "husky install", + "prettify": "run-s prettify:*", + "prettify:code": "prettier --write **/src/**/*", + "prettify:packages": "lerna exec -- sort-package-json && sort-package-json", + "release": "lerna publish from-package", + "test": "yarn workspaces run test", + "verify": "yarn workspaces run verify", + "version": "lerna version" + }, + "lint-staged": { + "**/src/package.json": [ + "yarn run prettify:packages" + ], + "package.json": [ + "yarn run prettify:packages" + ] + }, + "devDependencies": { + "@rarible/eslint-config-ts": "~0.10.0-alpha.10", + "@rarible/prettier": "^0.10.0-alpha.10", + "@testing-library/jest-dom": "^5.16.5", + "@testing-library/react": "^12.1.5", + "@types/jest": "^29.5.0", + "@types/react": "^16.9.37", + "@types/react-dom": "^16.9.8", + "@types/react-virtualized": "9.21.10", + "eslint": "^8.47.0", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-prettier": "^4.2.1", + "husky": "^8.0.3", + "jest": "^29.5.0", + "jest-environment-jsdom": "^29.5.0", + "joi": "^17.9.1", + "lerna": "^6.6.1", + "lint-staged": "^10.4.0", + "prettier": "^2.4.1", + "react": "^16.13.1", + "react-dom": "16.13.1", + "rxjs": "^6.6.7", + "sort-package-json": "^1.50.0", + "ts-jest": "^29.1.0", + "typescript": "^5.0.3", + "webpack": "~5.72.0" + } } diff --git a/packages/atom/src/atom/atom.ts b/packages/atom/src/atom/atom.ts index 1f317d6..bf15123 100644 --- a/packages/atom/src/atom/atom.ts +++ b/packages/atom/src/atom/atom.ts @@ -8,161 +8,161 @@ import { JsonAtom, CombinedAtomViewImpl } from "./base" export type Atom = _Atom export namespace Atom { - /** - * Create an atom with given initial value. - * - * @export - * @template T type of atom values - * @param initialValue initial value for this atom - * @returns fresh atom - */ - export function create(initialValue: T): Atom { - return new JsonAtom(initialValue) - } - - export function log(atom: Atom, name?: string): Atom - export function log(atom: ReadOnlyAtom, name?: string): ReadOnlyAtom - export function log(atom: Atom, logger?: (prevState: T, next: T) => void): Atom - export function log(atom: ReadOnlyAtom, logger?: (prevState: T, next: T) => void): ReadOnlyAtom - - export function log( - atom: Atom | ReadOnlyAtom, - logger?: string | ((prevState: T, next: T) => void) - ): Atom | ReadOnlyAtom { - const logState = (msg: string, color: string, x: T) => { - console.log("%c" + msg, `color: ${color}; font-weight: bold`, x) - } - let prevState = atom.get() - - atom.subscribe(next => { - if (typeof logger === "function") { - logger(prevState, next) - } else { - console.group(`UPDATE ${logger ? `TYPE: ${logger} ` : ""}@ ${new Date().toTimeString()}`) - logState("prev state", "#9E9E9E", prevState) - logState("next state", "#4CAF50", next) - console.groupEnd() - } - prevState = next - }) - - return atom - } - - export function combine( - source1: ReadOnlyAtom, - source2: ReadOnlyAtom, - combineFn: (x1: T1, x2: T2) => TResult - ): ReadOnlyAtom - - export function combine( - source1: ReadOnlyAtom, - source2: ReadOnlyAtom, - source3: ReadOnlyAtom, - combineFn: (x1: T1, x2: T2, x3: T3) => TResult - ): ReadOnlyAtom - - export function combine( - source1: ReadOnlyAtom, - source2: ReadOnlyAtom, - source3: ReadOnlyAtom, - source4: ReadOnlyAtom, - combineFn: (x1: T1, x2: T2, x3: T3, x4: T4) => TResult - ): ReadOnlyAtom - - export function combine( - source1: ReadOnlyAtom, - source2: ReadOnlyAtom, - source3: ReadOnlyAtom, - source4: ReadOnlyAtom, - source5: ReadOnlyAtom, - combineFn: (x1: T1, x2: T2, x3: T3, x4: T4, x5: T5) => TResult - ): ReadOnlyAtom - - export function combine( - source1: ReadOnlyAtom, - source2: ReadOnlyAtom, - source3: ReadOnlyAtom, - source4: ReadOnlyAtom, - source5: ReadOnlyAtom, - source6: ReadOnlyAtom, - combineFn: (x1: T1, x2: T2, x3: T3, x4: T4, x5: T5, x6: T6) => TResult - ): ReadOnlyAtom - - export function combine( - source1: ReadOnlyAtom, - source2: ReadOnlyAtom, - source3: ReadOnlyAtom, - source4: ReadOnlyAtom, - source5: ReadOnlyAtom, - source6: ReadOnlyAtom, - source7: ReadOnlyAtom, - combineFn: (x1: T1, x2: T2, x3: T3, x4: T4, x5: T5, x6: T6, x7: T7) => TResult - ): ReadOnlyAtom - - export function combine(...args: (ReadOnlyAtom | ((...xs: any[]) => TResult))[]) { - return new CombinedAtomViewImpl(args.slice(undefined, -1) as ReadOnlyAtom[], xs => - (args[args.length - 1] as (...xs: any[]) => TResult)(...xs) - ) - } - - /** - * Converts an observable to a read-only atom. - * - * The returned atom is wrapped into an observable, which will only emit a single value. - * The source observable will only be subscribed to for as long as there is at least one - * subscription to the returned observable. - * - * The returned observable never completes and controls the lifecycle of the emitted atom: - * as long as it's subscribed to, the returned atom will have its value updated from the - * source observable. - * - * @export - * @template T type of atom values - * @param source$ the source observable - * @returns an observable that emits a read-only atom - */ - export function fromObservable(source$: Observable) { - const subject$ = new BehaviorSubject | null>(null) - - const initAndUpdateAtom = source$.pipe( - tap(x => { - const atom = subject$.value - - if (atom === null) { - subject$.next(Atom.create(x)) - } else { - atom.set(x) - } - }), - // prevent updating atom multiple times to the same value - share() - ) - - return new Observable>(o => { - const sub = new Subscription() - - sub.add(subject$.pipe(filter((x): x is Atom => !!x)).subscribe(o)) - sub.add( - // It's necessary to use internal subscribe - initAndUpdateAtom.subscribe({ - error: e => o.error(e), - complete: () => o.complete(), - }) - ) - - return sub - }) - } - - export function set(atom$: Atom, source$: Observable): Observable { - return new Observable(s => { - const sub = source$.subscribe({ - next: next => atom$.set(next), - error: e => s.next(e), - complete: () => s.complete(), - }) - s.add(sub) - }) - } + /** + * Create an atom with given initial value. + * + * @export + * @template T type of atom values + * @param initialValue initial value for this atom + * @returns fresh atom + */ + export function create(initialValue: T): Atom { + return new JsonAtom(initialValue) + } + + export function log(atom: Atom, name?: string): Atom + export function log(atom: ReadOnlyAtom, name?: string): ReadOnlyAtom + export function log(atom: Atom, logger?: (prevState: T, next: T) => void): Atom + export function log(atom: ReadOnlyAtom, logger?: (prevState: T, next: T) => void): ReadOnlyAtom + + export function log( + atom: Atom | ReadOnlyAtom, + logger?: string | ((prevState: T, next: T) => void), + ): Atom | ReadOnlyAtom { + const logState = (msg: string, color: string, x: T) => { + console.log("%c" + msg, `color: ${color}; font-weight: bold`, x) + } + let prevState = atom.get() + + atom.subscribe(next => { + if (typeof logger === "function") { + logger(prevState, next) + } else { + console.group(`UPDATE ${logger ? `TYPE: ${logger} ` : ""}@ ${new Date().toTimeString()}`) + logState("prev state", "#9E9E9E", prevState) + logState("next state", "#4CAF50", next) + console.groupEnd() + } + prevState = next + }) + + return atom + } + + export function combine( + source1: ReadOnlyAtom, + source2: ReadOnlyAtom, + combineFn: (x1: T1, x2: T2) => TResult, + ): ReadOnlyAtom + + export function combine( + source1: ReadOnlyAtom, + source2: ReadOnlyAtom, + source3: ReadOnlyAtom, + combineFn: (x1: T1, x2: T2, x3: T3) => TResult, + ): ReadOnlyAtom + + export function combine( + source1: ReadOnlyAtom, + source2: ReadOnlyAtom, + source3: ReadOnlyAtom, + source4: ReadOnlyAtom, + combineFn: (x1: T1, x2: T2, x3: T3, x4: T4) => TResult, + ): ReadOnlyAtom + + export function combine( + source1: ReadOnlyAtom, + source2: ReadOnlyAtom, + source3: ReadOnlyAtom, + source4: ReadOnlyAtom, + source5: ReadOnlyAtom, + combineFn: (x1: T1, x2: T2, x3: T3, x4: T4, x5: T5) => TResult, + ): ReadOnlyAtom + + export function combine( + source1: ReadOnlyAtom, + source2: ReadOnlyAtom, + source3: ReadOnlyAtom, + source4: ReadOnlyAtom, + source5: ReadOnlyAtom, + source6: ReadOnlyAtom, + combineFn: (x1: T1, x2: T2, x3: T3, x4: T4, x5: T5, x6: T6) => TResult, + ): ReadOnlyAtom + + export function combine( + source1: ReadOnlyAtom, + source2: ReadOnlyAtom, + source3: ReadOnlyAtom, + source4: ReadOnlyAtom, + source5: ReadOnlyAtom, + source6: ReadOnlyAtom, + source7: ReadOnlyAtom, + combineFn: (x1: T1, x2: T2, x3: T3, x4: T4, x5: T5, x6: T6, x7: T7) => TResult, + ): ReadOnlyAtom + + export function combine(...args: (ReadOnlyAtom | ((...xs: any[]) => TResult))[]) { + return new CombinedAtomViewImpl(args.slice(undefined, -1) as ReadOnlyAtom[], xs => + (args[args.length - 1] as (...xs: any[]) => TResult)(...xs), + ) + } + + /** + * Converts an observable to a read-only atom. + * + * The returned atom is wrapped into an observable, which will only emit a single value. + * The source observable will only be subscribed to for as long as there is at least one + * subscription to the returned observable. + * + * The returned observable never completes and controls the lifecycle of the emitted atom: + * as long as it's subscribed to, the returned atom will have its value updated from the + * source observable. + * + * @export + * @template T type of atom values + * @param source$ the source observable + * @returns an observable that emits a read-only atom + */ + export function fromObservable(source$: Observable) { + const subject$ = new BehaviorSubject | null>(null) + + const initAndUpdateAtom = source$.pipe( + tap(x => { + const atom = subject$.value + + if (atom === null) { + subject$.next(Atom.create(x)) + } else { + atom.set(x) + } + }), + // prevent updating atom multiple times to the same value + share(), + ) + + return new Observable>(o => { + const sub = new Subscription() + + sub.add(subject$.pipe(filter((x): x is Atom => !!x)).subscribe(o)) + sub.add( + // It's necessary to use internal subscribe + initAndUpdateAtom.subscribe({ + error: e => o.error(e), + complete: () => o.complete(), + }), + ) + + return sub + }) + } + + export function set(atom$: Atom, source$: Observable): Observable { + return new Observable(s => { + const sub = source$.subscribe({ + next: next => atom$.set(next), + error: e => s.next(e), + complete: () => s.complete(), + }) + s.add(sub) + }) + } } diff --git a/packages/atom/src/atom/base.ts b/packages/atom/src/atom/base.ts index ba370a0..0d3a596 100644 --- a/packages/atom/src/atom/base.ts +++ b/packages/atom/src/atom/base.ts @@ -8,131 +8,131 @@ import { Subscription, BehaviorSubject, combineLatest } from "rxjs" * @template T type of atom values */ export interface ReadOnlyAtom extends Observable { - /** - * Get the current atom value. - * - * @example - * import { Atom } from '@rixio/atom' - * - * const a = Atom.create(5) - * a.get() - * // => 5 - * - * a.set(6) - * a.get() - * // => 6 - * @returns current value - */ - get(): T - - /** - * View this atom as is. - * Doesn't seem to make sense, but it is needed to be used from - * inheriting atom classes to conveniently go from read/write to - * read-only atom. - * - * @example - * import { Atom } from '@rixio/atom' - * - * const source = Atom.create(5) - * const view = source.view() - * - * view.get() - * // => 5 - * - * source.set(6) - * view.get() - * // => 6 - * - * view.set(7) // compilation error - * @returns this atom - */ - view(): ReadOnlyAtom - - /** - * View this atom through a given mapping. - * - * @example - * import { Atom } from '@rixio/atom' - * - * const a = Atom.create(5) - * const b = a.view(x => x * 2) - * - * a.get() - * // => 5 - * b.get() - * // => 10 - * - * a.set(10) - * - * a.get() - * // => 10 - * b.get() - * // => 20 - * @param getter getter function that defines the view - * @returns atom viewed through the given transformation - */ - view(getter: (x: T) => U): ReadOnlyAtom - - /** - * View this atom through a given lens. - * @param lens lens that defines the view - * @returns atom viewed through the given transformation - */ - view(lens: Lens): ReadOnlyAtom - - /** - * View this atom through a given prism. - * @param prism prism that defines the view - * @returns atom viewed through the given transformation - */ - view(prism: Prism): ReadOnlyAtom> - - /** - * View this atom at a property of given name. - */ - view(k: K): ReadOnlyAtom - - /** - * View this atom at a give property path. - */ - view(k1: K1, k2: K2): ReadOnlyAtom - - /** - * View this atom at a give property path. - */ - view( - k1: K1, - k2: K2, - k3: K3 - ): ReadOnlyAtom - - /** - * View this atom at a give property path. - */ - view( - k1: K1, - k2: K2, - k3: K3, - k4: K4 - ): ReadOnlyAtom - - /** - * View this atom at a give property path. - */ - view< - K1 extends keyof T, - K2 extends keyof T[K1], - K3 extends keyof T[K1][K2], - K4 extends keyof T[K1][K2][K3], - K5 extends keyof T[K1][K2][K3][K4] - >( - k1: K1, - k2: K2, - k3: K3, - k4: K4, - k5: K5 - ): ReadOnlyAtom + /** + * Get the current atom value. + * + * @example + * import { Atom } from '@rixio/atom' + * + * const a = Atom.create(5) + * a.get() + * // => 5 + * + * a.set(6) + * a.get() + * // => 6 + * @returns current value + */ + get(): T + + /** + * View this atom as is. + * Doesn't seem to make sense, but it is needed to be used from + * inheriting atom classes to conveniently go from read/write to + * read-only atom. + * + * @example + * import { Atom } from '@rixio/atom' + * + * const source = Atom.create(5) + * const view = source.view() + * + * view.get() + * // => 5 + * + * source.set(6) + * view.get() + * // => 6 + * + * view.set(7) // compilation error + * @returns this atom + */ + view(): ReadOnlyAtom + + /** + * View this atom through a given mapping. + * + * @example + * import { Atom } from '@rixio/atom' + * + * const a = Atom.create(5) + * const b = a.view(x => x * 2) + * + * a.get() + * // => 5 + * b.get() + * // => 10 + * + * a.set(10) + * + * a.get() + * // => 10 + * b.get() + * // => 20 + * @param getter getter function that defines the view + * @returns atom viewed through the given transformation + */ + view(getter: (x: T) => U): ReadOnlyAtom + + /** + * View this atom through a given lens. + * @param lens lens that defines the view + * @returns atom viewed through the given transformation + */ + view(lens: Lens): ReadOnlyAtom + + /** + * View this atom through a given prism. + * @param prism prism that defines the view + * @returns atom viewed through the given transformation + */ + view(prism: Prism): ReadOnlyAtom> + + /** + * View this atom at a property of given name. + */ + view(k: K): ReadOnlyAtom + + /** + * View this atom at a give property path. + */ + view(k1: K1, k2: K2): ReadOnlyAtom + + /** + * View this atom at a give property path. + */ + view( + k1: K1, + k2: K2, + k3: K3, + ): ReadOnlyAtom + + /** + * View this atom at a give property path. + */ + view( + k1: K1, + k2: K2, + k3: K3, + k4: K4, + ): ReadOnlyAtom + + /** + * View this atom at a give property path. + */ + view< + K1 extends keyof T, + K2 extends keyof T[K1], + K3 extends keyof T[K1][K2], + K4 extends keyof T[K1][K2][K3], + K5 extends keyof T[K1][K2][K3][K4], + >( + k1: K1, + k2: K2, + k3: K3, + k4: K4, + k5: K5, + ): ReadOnlyAtom } /** @@ -140,350 +140,350 @@ export interface ReadOnlyAtom extends Observable { * @template T type of atom values */ export interface Atom extends ReadOnlyAtom { - /** - * Modify atom value. - * - * The update function should be: - * - referentially transparent: return same result for same arguments - * - side-effect free: don't perform any mutations (including calling - * Atom.set/Atom.modify) and side effects - * - * @param updateFn value update function - */ - modify(updateFn: (current: T) => T): void - - /** - * Set new atom value. - * - * @param next new value - */ - set(next: T): void - - /** - * Create a lensed atom by supplying a lens. - * @template U destination value type - * @param lens a lens - * @returns a lensed atom - */ - lens(lens: Lens): Atom - - /** - * Create a lensed atom that's focused on a property of given name. - */ - lens(k: K): Atom - - /** - * Create a lensed atom that's focused on a given property path. - */ - lens(k1: K1, k2: K2): Atom - - /** - * Create a lensed atom that's focused on a given property path. - */ - lens( - k1: K1, - k2: K2, - k3: K3 - ): Atom - - /** - * Create a lensed atom that's focused on a given property path. - */ - lens( - k1: K1, - k2: K2, - k3: K3, - k4: K4 - ): Atom - - /** - * Create a lensed atom that's focused on a given property path. - */ - lens< - K1 extends keyof T, - K2 extends keyof T[K1], - K3 extends keyof T[K1][K2], - K4 extends keyof T[K1][K2][K3], - K5 extends keyof T[K1][K2][K3][K4] - >( - k1: K1, - k2: K2, - k3: K3, - k4: K4, - k5: K5 - ): Atom + /** + * Modify atom value. + * + * The update function should be: + * - referentially transparent: return same result for same arguments + * - side-effect free: don't perform any mutations (including calling + * Atom.set/Atom.modify) and side effects + * + * @param updateFn value update function + */ + modify(updateFn: (current: T) => T): void + + /** + * Set new atom value. + * + * @param next new value + */ + set(next: T): void + + /** + * Create a lensed atom by supplying a lens. + * @template U destination value type + * @param lens a lens + * @returns a lensed atom + */ + lens(lens: Lens): Atom + + /** + * Create a lensed atom that's focused on a property of given name. + */ + lens(k: K): Atom + + /** + * Create a lensed atom that's focused on a given property path. + */ + lens(k1: K1, k2: K2): Atom + + /** + * Create a lensed atom that's focused on a given property path. + */ + lens( + k1: K1, + k2: K2, + k3: K3, + ): Atom + + /** + * Create a lensed atom that's focused on a given property path. + */ + lens( + k1: K1, + k2: K2, + k3: K3, + k4: K4, + ): Atom + + /** + * Create a lensed atom that's focused on a given property path. + */ + lens< + K1 extends keyof T, + K2 extends keyof T[K1], + K3 extends keyof T[K1][K2], + K4 extends keyof T[K1][K2][K3], + K5 extends keyof T[K1][K2][K3][K4], + >( + k1: K1, + k2: K2, + k3: K3, + k4: K4, + k5: K5, + ): Atom } export abstract class AbstractReadOnlyAtom extends BehaviorSubject implements ReadOnlyAtom { - private readonly viewedAtomsCache = new SimpleCache, ReadOnlyAtom>( - lens => new AtomViewImpl(this, lens.get) - ) - - abstract get(): T - - view(): ReadOnlyAtom - view(getter: (x: T) => U): ReadOnlyAtom - view(lens: Lens): ReadOnlyAtom - view(prism: Prism): ReadOnlyAtom> - view(k: K): ReadOnlyAtom - - view(...args: any[]): ReadOnlyAtom { - if (args[0] === undefined) { - return this - } else if (typeof args[0] === "function") { - return new AtomViewImpl(this, args[0] as (x: T) => U) - } else if (typeof args[0] === "string") { - const lens = Lens.compose(...args.map(Lens.key())) - return this.viewedAtomsCache.getOrCreate(lens) - } else { - return new AtomViewImpl(this, x => (args[0] as Lens).get(x)) - } - } + private readonly viewedAtomsCache = new SimpleCache, ReadOnlyAtom>( + lens => new AtomViewImpl(this, lens.get), + ) + + abstract get(): T + + view(): ReadOnlyAtom + view(getter: (x: T) => U): ReadOnlyAtom + view(lens: Lens): ReadOnlyAtom + view(prism: Prism): ReadOnlyAtom> + view(k: K): ReadOnlyAtom + + view(...args: any[]): ReadOnlyAtom { + if (args[0] === undefined) { + return this + } else if (typeof args[0] === "function") { + return new AtomViewImpl(this, args[0] as (x: T) => U) + } else if (typeof args[0] === "string") { + const lens = Lens.compose(...args.map(Lens.key())) + return this.viewedAtomsCache.getOrCreate(lens) + } else { + return new AtomViewImpl(this, x => (args[0] as Lens).get(x)) + } + } } export abstract class AbstractAtom extends AbstractReadOnlyAtom implements Atom { - private readonly lensedAtomsCache: SimpleCache, LensedAtom> = new SimpleCache< - Lens, - LensedAtom - >(lens => new LensedAtom(this, lens, structEq)) - - abstract modify(updateFn: (x: T) => T): void - - set(x: T) { - this.modify(() => x) - } - - lens(lens: Lens): Atom - lens(k: K): Atom - - lens(arg1: Lens | string, ...args: string[]): Atom { - const lens = - typeof arg1 === "string" || typeof arg1 === "number" - ? Lens.compose(Lens.key(arg1), ...args.map(k => Lens.key(k))) - : (arg1 as Lens) - return this.lensedAtomsCache.getOrCreate(lens) - } + private readonly lensedAtomsCache: SimpleCache, LensedAtom> = new SimpleCache< + Lens, + LensedAtom + >(lens => new LensedAtom(this, lens, structEq)) + + abstract modify(updateFn: (x: T) => T): void + + set(x: T) { + this.modify(() => x) + } + + lens(lens: Lens): Atom + lens(k: K): Atom + + lens(arg1: Lens | string, ...args: string[]): Atom { + const lens = + typeof arg1 === "string" || typeof arg1 === "number" + ? Lens.compose(Lens.key(arg1), ...args.map(k => Lens.key(k))) + : (arg1 as Lens) + return this.lensedAtomsCache.getOrCreate(lens) + } } export class JsonAtom extends AbstractAtom { - get() { - return this.getValue() - } + get() { + return this.getValue() + } - modify(updateFn: (x: T) => T) { - const prevValue = this.getValue() - const next = updateFn(prevValue) + modify(updateFn: (x: T) => T) { + const prevValue = this.getValue() + const next = updateFn(prevValue) - if (!structEq(prevValue, next)) this.next(next) - } + if (!structEq(prevValue, next)) this.next(next) + } - set(x: T) { - const prevValue = this.getValue() + set(x: T) { + const prevValue = this.getValue() - if (!structEq(prevValue, x)) this.next(x) - } + if (!structEq(prevValue, x)) this.next(x) + } } class LensedAtom extends AbstractAtom { - constructor( - private _source: Atom, - private _lens: Lens, - private _eq: (x: TDest, y: TDest) => boolean = structEq - ) { - // @NOTE this is a major hack to optimize for not calling - // _lens.get the extra time here. This makes the underlying - // BehaviorSubject to have an `undefined` for it's current value. - // - // But it works because before somebody subscribes to this - // atom, it will subscribe to the _source (which we expect to be a - // descendant of BehaviorSubject as well), which will emit a - // value right away, triggering our _onSourceValue. - super(undefined!) - } - - get() { - // Optimization: in case we're already subscribed to the - // source atom, the BehaviorSubject.getValue will return - // an up-to-date computed lens value. - // - // This way we don't need to recalculate the lens value - // every time. - return this._subscription ? this.getValue() : this._lens.get(this._source.get()) - } - - modify(updateFn: (x: TDest) => TDest) { - this._source.modify(x => this._lens.modify(updateFn, x)) - } - - set(next: TDest) { - this._source.modify(x => this._lens.set(next, x)) - } - - private _onSourceValue(x: TSource) { - const prevValue = this.getValue() - const next = this._lens.get(x) - - if (!this._eq(prevValue, next)) this.next(next) - } - - private _subscription: Subscription | null = null - private _refCount = 0 - - _subscribe(subscriber: Subscriber) { - if (!this._subscription) { - this._subscription = this._source.subscribe(x => this._onSourceValue(x)) - } - this._refCount = this._refCount + 1 - - const sub = new Subscription(() => { - this._refCount = this._refCount - 1 - if (this._refCount <= 0 && this._subscription) { - this._subscription.unsubscribe() - this._subscription = null - } - }) - sub.add(super._subscribe(subscriber)) - return sub - } - - unsubscribe() { - if (this._subscription) { - this._subscription.unsubscribe() - this._subscription = null - } - this._refCount = 0 - - super.unsubscribe() - } + constructor( + private _source: Atom, + private _lens: Lens, + private _eq: (x: TDest, y: TDest) => boolean = structEq, + ) { + // @NOTE this is a major hack to optimize for not calling + // _lens.get the extra time here. This makes the underlying + // BehaviorSubject to have an `undefined` for it's current value. + // + // But it works because before somebody subscribes to this + // atom, it will subscribe to the _source (which we expect to be a + // descendant of BehaviorSubject as well), which will emit a + // value right away, triggering our _onSourceValue. + super(undefined!) + } + + get() { + // Optimization: in case we're already subscribed to the + // source atom, the BehaviorSubject.getValue will return + // an up-to-date computed lens value. + // + // This way we don't need to recalculate the lens value + // every time. + return this._subscription ? this.getValue() : this._lens.get(this._source.get()) + } + + modify(updateFn: (x: TDest) => TDest) { + this._source.modify(x => this._lens.modify(updateFn, x)) + } + + set(next: TDest) { + this._source.modify(x => this._lens.set(next, x)) + } + + private _onSourceValue(x: TSource) { + const prevValue = this.getValue() + const next = this._lens.get(x) + + if (!this._eq(prevValue, next)) this.next(next) + } + + private _subscription: Subscription | null = null + private _refCount = 0 + + _subscribe(subscriber: Subscriber) { + if (!this._subscription) { + this._subscription = this._source.subscribe(x => this._onSourceValue(x)) + } + this._refCount = this._refCount + 1 + + const sub = new Subscription(() => { + this._refCount = this._refCount - 1 + if (this._refCount <= 0 && this._subscription) { + this._subscription.unsubscribe() + this._subscription = null + } + }) + sub.add(super._subscribe(subscriber)) + return sub + } + + unsubscribe() { + if (this._subscription) { + this._subscription.unsubscribe() + this._subscription = null + } + this._refCount = 0 + + super.unsubscribe() + } } class AtomViewImpl extends AbstractReadOnlyAtom { - constructor( - private _source: ReadOnlyAtom, - private _getter: (x: TSource) => TDest, - private _eq: (x: TDest, y: TDest) => boolean = structEq - ) { - // @NOTE this is a major hack to optimize for not calling - // _getter the extra time here. This makes the underlying - // BehaviorSubject to have an `undefined` for it's current value. - // - // But it works because before somebody subscribes to this - // atom, it will subscribe to the _source (which we expect to be a - // descendant of BehaviorSubject as well), which will emit a - // value right away, triggering our _onSourceValue. - super(undefined!) - } - - get() { - // Optimization: in case we're already subscribed to the - // source atom, the BehaviorSubject.getValue will return - // an up-to-date computed lens value. - // - // This way we don't need to recalculate the view value - // every time. - return this._subscription ? this.getValue() : this._getter(this._source.get()) - } - - private _onSourceValue(x: TSource) { - const prevValue = this.getValue() - const next = this._getter(x) - - if (!this._eq(prevValue, next)) this.next(next) - } - - private _subscription: Subscription | null = null - private _refCount = 0 - - _subscribe(subscriber: Subscriber) { - if (!this._subscription) { - this._subscription = this._source.subscribe(x => this._onSourceValue(x)) - } - this._refCount = this._refCount + 1 - - const sub = new Subscription(() => { - this._refCount = this._refCount - 1 - if (this._refCount <= 0 && this._subscription) { - this._subscription.unsubscribe() - this._subscription = null - } - }) - sub.add(super._subscribe(subscriber)) - return sub - } - - unsubscribe() { - if (this._subscription) { - this._subscription.unsubscribe() - this._subscription = null - } - this._refCount = 0 - - super.unsubscribe() - } + constructor( + private _source: ReadOnlyAtom, + private _getter: (x: TSource) => TDest, + private _eq: (x: TDest, y: TDest) => boolean = structEq, + ) { + // @NOTE this is a major hack to optimize for not calling + // _getter the extra time here. This makes the underlying + // BehaviorSubject to have an `undefined` for it's current value. + // + // But it works because before somebody subscribes to this + // atom, it will subscribe to the _source (which we expect to be a + // descendant of BehaviorSubject as well), which will emit a + // value right away, triggering our _onSourceValue. + super(undefined!) + } + + get() { + // Optimization: in case we're already subscribed to the + // source atom, the BehaviorSubject.getValue will return + // an up-to-date computed lens value. + // + // This way we don't need to recalculate the view value + // every time. + return this._subscription ? this.getValue() : this._getter(this._source.get()) + } + + private _onSourceValue(x: TSource) { + const prevValue = this.getValue() + const next = this._getter(x) + + if (!this._eq(prevValue, next)) this.next(next) + } + + private _subscription: Subscription | null = null + private _refCount = 0 + + _subscribe(subscriber: Subscriber) { + if (!this._subscription) { + this._subscription = this._source.subscribe(x => this._onSourceValue(x)) + } + this._refCount = this._refCount + 1 + + const sub = new Subscription(() => { + this._refCount = this._refCount - 1 + if (this._refCount <= 0 && this._subscription) { + this._subscription.unsubscribe() + this._subscription = null + } + }) + sub.add(super._subscribe(subscriber)) + return sub + } + + unsubscribe() { + if (this._subscription) { + this._subscription.unsubscribe() + this._subscription = null + } + this._refCount = 0 + + super.unsubscribe() + } } export class CombinedAtomViewImpl extends AbstractReadOnlyAtom { - constructor( - private _sources: ReadOnlyAtom[], - private _combineFn: (xs: any[]) => TResult, - private _eq: (x: TResult, y: TResult) => boolean = structEq - ) { - // @NOTE this is a major hack to optimize for not calling - // _combineFn and .get for each source the extra time here. - // This makes the underlying BehaviorSubject to have an - // `undefined` for it's current value. - // - // But it works because before somebody subscribes to this - // atom, it will subscribe to the _source (which we expect to be a - // descendant of BehaviorSubject as well), which will emit a - // value right away, triggering our _onSourceValue. - super(undefined!) - } - - get() { - // Optimization: in case we're already subscribed to - // source atoms, the BehaviorSubject.getValue will return - // an up-to-date computed view value. - // - // This way we don't need to recalculate the view value - // every time. - return this._subscription ? this.getValue() : this._combineFn(this._sources.map(x => x.get())) - } - - private _onSourceValues(xs: any[]) { - const prevValue = this.getValue() - const next = this._combineFn(xs) - - if (!this._eq(prevValue, next)) this.next(next) - } - - private _subscription: Subscription | null = null - private _refCount = 0 - - _subscribe(subscriber: Subscriber) { - if (!this._subscription) { - this._subscription = combineLatest(this._sources).subscribe(xs => this._onSourceValues(xs)) - } - this._refCount = this._refCount + 1 - - const sub = new Subscription(() => { - this._refCount = this._refCount - 1 - if (this._refCount <= 0 && this._subscription) { - this._subscription.unsubscribe() - this._subscription = null - } - }) - sub.add(super._subscribe(subscriber)) - return sub - } - - unsubscribe() { - if (this._subscription) { - this._subscription.unsubscribe() - this._subscription = null - } - this._refCount = 0 - - super.unsubscribe() - } + constructor( + private _sources: ReadOnlyAtom[], + private _combineFn: (xs: any[]) => TResult, + private _eq: (x: TResult, y: TResult) => boolean = structEq, + ) { + // @NOTE this is a major hack to optimize for not calling + // _combineFn and .get for each source the extra time here. + // This makes the underlying BehaviorSubject to have an + // `undefined` for it's current value. + // + // But it works because before somebody subscribes to this + // atom, it will subscribe to the _source (which we expect to be a + // descendant of BehaviorSubject as well), which will emit a + // value right away, triggering our _onSourceValue. + super(undefined!) + } + + get() { + // Optimization: in case we're already subscribed to + // source atoms, the BehaviorSubject.getValue will return + // an up-to-date computed view value. + // + // This way we don't need to recalculate the view value + // every time. + return this._subscription ? this.getValue() : this._combineFn(this._sources.map(x => x.get())) + } + + private _onSourceValues(xs: any[]) { + const prevValue = this.getValue() + const next = this._combineFn(xs) + + if (!this._eq(prevValue, next)) this.next(next) + } + + private _subscription: Subscription | null = null + private _refCount = 0 + + _subscribe(subscriber: Subscriber) { + if (!this._subscription) { + this._subscription = combineLatest(this._sources).subscribe(xs => this._onSourceValues(xs)) + } + this._refCount = this._refCount + 1 + + const sub = new Subscription(() => { + this._refCount = this._refCount - 1 + if (this._refCount <= 0 && this._subscription) { + this._subscription.unsubscribe() + this._subscription = null + } + }) + sub.add(super._subscribe(subscriber)) + return sub + } + + unsubscribe() { + if (this._subscription) { + this._subscription.unsubscribe() + this._subscription = null + } + this._refCount = 0 + + super.unsubscribe() + } } diff --git a/packages/atom/src/atom/index.test.ts b/packages/atom/src/atom/index.test.ts index df859c9..d9d0575 100644 --- a/packages/atom/src/atom/index.test.ts +++ b/packages/atom/src/atom/index.test.ts @@ -6,834 +6,834 @@ import type { ReadOnlyAtom } from "./index" import { Atom } from "./index" function firstValueFrom(obs$: Observable) { - return obs$.pipe(first()).toPromise() + return obs$.pipe(first()).toPromise() } function testAtom(next: (x: number) => Atom) { - it("atom/basic", async () => { - const a = next(1) - let expected = 1 + it("atom/basic", async () => { + const a = next(1) + let expected = 1 - const cb = (x: number) => { - expect(x).toEqual(expected) - expect(a.get()).toEqual(expected) - } + const cb = (x: number) => { + expect(x).toEqual(expected) + expect(a.get()).toEqual(expected) + } - const subscription = a.subscribe(cb) + const subscription = a.subscribe(cb) - expect(a.get()).toEqual(1) + expect(a.get()).toEqual(1) - expected = 2 - a.modify(x => x + 1) + expected = 2 + a.modify(x => x + 1) - expected = 500 - a.set(500) + expected = 500 + a.set(500) - subscription.unsubscribe() - }) + subscription.unsubscribe() + }) - it("atom/observable: distinct values", () => { - const a = next(1) - const observations: number[] = [] - const cb = (x: number) => observations.push(x) - const subscription = a.subscribe(cb) - ;[2, 3, 3, 3, 1].forEach(x => a.set(x)) + it("atom/observable: distinct values", () => { + const a = next(1) + const observations: number[] = [] + const cb = (x: number) => observations.push(x) + const subscription = a.subscribe(cb) + ;[2, 3, 3, 3, 1].forEach(x => a.set(x)) - expect(structEq(observations, [1, 2, 3, 1])).toBeTruthy() + expect(structEq(observations, [1, 2, 3, 1])).toBeTruthy() - subscription.unsubscribe() - }) + subscription.unsubscribe() + }) } function testDerivedAtom( - createDerived: (a: Atom, f: (x: number) => number, onCalled: (a: number) => void) => ReadOnlyAtom, - create: (x: number) => Atom = Atom.create + createDerived: (a: Atom, f: (x: number) => number, onCalled: (a: number) => void) => ReadOnlyAtom, + create: (x: number) => Atom = Atom.create, ) { - describe("unsub in modify", () => { - const a = create(5) + describe("unsub in modify", () => { + const a = create(5) - const viewFnCalls: number[] = [] - const os: number[] = [] + const viewFnCalls: number[] = [] + const os: number[] = [] - const v = createDerived( - a, - x => x + 1, - x => viewFnCalls.push(x) - ) + const v = createDerived( + a, + x => x + 1, + x => viewFnCalls.push(x), + ) - expect(viewFnCalls).toEqual([]) - expect(os).toEqual([]) + expect(viewFnCalls).toEqual([]) + expect(os).toEqual([]) - const sub = v.subscribe(x => { - os.push(x) - }) - expect(viewFnCalls).toEqual([5]) - expect(os).toEqual([6]) + const sub = v.subscribe(x => { + os.push(x) + }) + expect(viewFnCalls).toEqual([5]) + expect(os).toEqual([6]) - a.modify(x => x + 1) - expect(viewFnCalls).toEqual([5, 6]) - expect(os).toEqual([6, 7]) + a.modify(x => x + 1) + expect(viewFnCalls).toEqual([5, 6]) + expect(os).toEqual([6, 7]) - a.modify(() => { - sub.unsubscribe() - return 0 - }) - expect(viewFnCalls).toEqual([5, 6]) - expect(os).toEqual([6, 7]) - }) + a.modify(() => { + sub.unsubscribe() + return 0 + }) + expect(viewFnCalls).toEqual([5, 6]) + expect(os).toEqual([6, 7]) + }) - describe("two atoms subscriptions", () => { - const a = create(5) + describe("two atoms subscriptions", () => { + const a = create(5) - const viewFnCalls1: number[] = [] - const os1: number[] = [] - const viewFnCalls2: number[] = [] - const os2: number[] = [] + const viewFnCalls1: number[] = [] + const os1: number[] = [] + const viewFnCalls2: number[] = [] + const os2: number[] = [] - const v1 = createDerived( - a, - x => x + 1, - x => viewFnCalls1.push(x) - ) - const v2 = v1.view(x => { - viewFnCalls2.push(x) - return x + 5 - }) + const v1 = createDerived( + a, + x => x + 1, + x => viewFnCalls1.push(x), + ) + const v2 = v1.view(x => { + viewFnCalls2.push(x) + return x + 5 + }) - expect(viewFnCalls1).toEqual([]) - expect(viewFnCalls2).toEqual([]) + expect(viewFnCalls1).toEqual([]) + expect(viewFnCalls2).toEqual([]) - const sub1 = v1.subscribe(x => os1.push(x)) + const sub1 = v1.subscribe(x => os1.push(x)) - v2.subscribe(x => os2.push(x)) + v2.subscribe(x => os2.push(x)) - expect(viewFnCalls1).toEqual([5]) - expect(viewFnCalls2).toEqual([6]) - expect(os2).toEqual([11]) + expect(viewFnCalls1).toEqual([5]) + expect(viewFnCalls2).toEqual([6]) + expect(os2).toEqual([11]) - a.modify(x => x + 1) - expect(viewFnCalls1).toEqual([5, 6]) - expect(viewFnCalls2).toEqual([6, 7]) - expect(os2).toEqual([11, 12]) + a.modify(x => x + 1) + expect(viewFnCalls1).toEqual([5, 6]) + expect(viewFnCalls2).toEqual([6, 7]) + expect(os2).toEqual([11, 12]) - a.modify(() => { - sub1.unsubscribe() - return 0 - }) + a.modify(() => { + sub1.unsubscribe() + return 0 + }) - expect(viewFnCalls1).toEqual([5, 6, 0]) - expect(viewFnCalls2).toEqual([6, 7, 1]) - expect(os1).toEqual([6, 7]) - expect(os2).toEqual([11, 12, 6]) - }) + expect(viewFnCalls1).toEqual([5, 6, 0]) + expect(viewFnCalls2).toEqual([6, 7, 1]) + expect(os1).toEqual([6, 7]) + expect(os2).toEqual([11, 12, 6]) + }) - describe("resubscribe", () => { - const a = create(5) - const v = createDerived(a, x => x + 1, noop) + describe("resubscribe", () => { + const a = create(5) + const v = createDerived(a, x => x + 1, noop) - const os: number[] = [] - const sub1 = v.subscribe(x => os.push(x)) + const os: number[] = [] + const sub1 = v.subscribe(x => os.push(x)) - // immediate observation on subscription - expect(os).toEqual([6]) + // immediate observation on subscription + expect(os).toEqual([6]) - a.modify(x => x + 1) + a.modify(x => x + 1) - // observation on modify - expect(os).toEqual([6, 7]) + // observation on modify + expect(os).toEqual([6, 7]) - sub1.unsubscribe() + sub1.unsubscribe() - a.modify(x => x + 1) + a.modify(x => x + 1) - // no observation after unsubscription - expect(os).toEqual([6, 7]) + // no observation after unsubscription + expect(os).toEqual([6, 7]) - const sub2 = v.subscribe(x => os.push(x)) + const sub2 = v.subscribe(x => os.push(x)) - // immediate observation on subscription 2 - expect(os).toEqual([6, 7, 8]) + // immediate observation on subscription 2 + expect(os).toEqual([6, 7, 8]) - a.modify(x => x + 1) + a.modify(x => x + 1) - // observation on modify 2 - expect(os).toEqual([6, 7, 8, 9]) + // observation on modify 2 + expect(os).toEqual([6, 7, 8, 9]) - sub2.unsubscribe() + sub2.unsubscribe() - a.modify(x => x + 1) + a.modify(x => x + 1) - // no observation after unsubscription 2 - expect(os).toEqual([6, 7, 8, 9]) - }) + // no observation after unsubscription 2 + expect(os).toEqual([6, 7, 8, 9]) + }) - describe("multiple subscriptions", () => { - const a = create(5) - const v = createDerived( - a, - x => x + 1, - () => { - /* no-op */ - } - ) + describe("multiple subscriptions", () => { + const a = create(5) + const v = createDerived( + a, + x => x + 1, + () => { + /* no-op */ + }, + ) - const os1: number[] = [] - const sub1 = v.subscribe(x => os1.push(x)) + const os1: number[] = [] + const sub1 = v.subscribe(x => os1.push(x)) - const os2: number[] = [] - const sub2 = v.subscribe(x => os2.push(x)) + const os2: number[] = [] + const sub2 = v.subscribe(x => os2.push(x)) - // same initial observations upon subscription - expect(os1).toEqual(os2) + // same initial observations upon subscription + expect(os1).toEqual(os2) - a.set(6) + a.set(6) - // same observations on modify - expect(os1).toEqual(os2) + // same observations on modify + expect(os1).toEqual(os2) - sub1.unsubscribe() - a.set(7) + sub1.unsubscribe() + a.set(7) - // no modify observation after unsub 1 - expect(os1).toEqual([6, 7]) + // no modify observation after unsub 1 + expect(os1).toEqual([6, 7]) - // observation on 2 after unsub on 1 - expect(os2).toEqual([6, 7, 8]) + // observation on 2 after unsub on 1 + expect(os2).toEqual([6, 7, 8]) - sub2.unsubscribe() - a.set(8) + sub2.unsubscribe() + a.set(8) - // no more observations - expect(os1).toEqual([6, 7]) + // no more observations + expect(os1).toEqual([6, 7]) - // no more observations on 2 - expect(os2).toEqual([6, 7, 8]) - }) + // no more observations on 2 + expect(os2).toEqual([6, 7, 8]) + }) } describe("atom", () => { - describe("plain", () => { - testAtom(x => Atom.create(x)) - }) - - describe("lens", () => { - describe("lensed atoms cached", () => { - const atom = Atom.create({ a: { b: { c: 1 } } }) - const la1 = atom.lens("a") - const la2 = atom.lens("a") - expect(la1).toStrictEqual(la2) - const lab1 = atom.lens("a", "b") - const lab2 = atom.lens("a", "b") - expect(lab1).toStrictEqual(lab2) - }) - - describe("lensed, nested safe key", () => { - testAtom(x => { - const source = Atom.create({ a: { b: { c: x } } }) - return source.lens("a", "b", "c") - }) - }) - - describe("lensed, safe key, chained lenses", () => { - testAtom(x => { - const source = Atom.create({ a: { b: { c: x } } }) - return source.lens("a").lens("b").lens("c") - }) - }) - - describe("lensed, chained + complex", () => { - const source = Atom.create({ a: { b: { c: 5 } } }) - const lensed = source - .lens("a") - .lens("b") - .lens("c") - .lens( - Lens.create( - (x: number) => x + 1, - (v: number) => v - 1 - ) - ) - - expect(lensed.get()).toEqual(6) - - lensed.set(6) - - expect(lensed.get()).toEqual(6) - }) - - describe("lensed, safe key, chained + complex", () => { - const source = Atom.create({ a: { b: { c: 5 } } }) - const lensed = source - .lens("a") - .lens("b") - .lens("c") - .lens( - Lens.create( - (x: number) => x + 1, - (v: number) => v - 1 - ) - ) - - expect(lensed.get()).toEqual(6) - - lensed.set(6) - - expect(lensed.get()).toEqual(6) - }) - - describe("lens then view", () => { - const x1 = Atom.create({ a: { b: 5 } }) - const x2 = x1 - .lens("a") - .view(x => x.b) - .view(x => x + 1) - const x3 = x1.lens("a").lens("b") - - expect(x3.get()).toEqual(5) - expect(x2.get()).toEqual(6) - - x3.set(6) - - expect(x3.get()).toEqual(6) - expect(x2.get()).toEqual(7) - expect(structEq({ a: { b: 6 } }, x1.get())).toBeTruthy() - }) - - describe("index lens", () => { - describe("generic", () => { - testAtom(x => { - const source = Atom.create([x, 2, 3]) - return source.lens( - Lens.index(0).compose( - Lens.create( - (x: number | undefined) => x!, - v => v - ) - ) - ) as Atom - }) - }) - - describe("atom interface", () => { - const source = Atom.create([1, 2, 3]) - const first = source.lens(Lens.index(0)) - expect(first.get()).toEqual(1) - first.set(10) - expect(first.get()).toEqual(10) - expect(structEq(source.get(), [10, 2, 3])).toBeTruthy() - source.set([100, 2, 3]) - expect(first.get()).toEqual(100) - source.set([2, 3]) - expect(first.get()).toEqual(2) - }) - - describe("observing lensed", () => { - const source = Atom.create([1, 2, 3]) - const first = source.lens(Lens.index(0)) - const observations: Option[] = [] - const subscription = first.subscribe((x: Option) => observations.push(x)) - - first.set(10) - source.set([100, 2, 3]) - source.set([2, 3]) - source.set([1000, 2, 3]) - - expect(observations).toEqual([1, 10, 100, 2, 1000]) - subscription.unsubscribe() - }) - }) - - testDerivedAtom((a, f, onCalled) => - a.lens( - Lens.create( - x => { - onCalled(x) - return f(x) - }, - v => v - ) - ) - ) - }) - - describe("view", () => { - describe("readonly, getter, simple", () => { - const source = Atom.create(5) - const view = source.view(x => x + 1) - - expect(source.get()).toEqual(5) - expect(view.get()).toEqual(6) - - source.modify(x => x + 1) - - expect(source.get()).toEqual(6) - expect(view.get()).toEqual(7) - }) - - describe("viewed atoms are cached", () => { - const atom = Atom.create({ a: { b: { c: 1 } } }) - const la1 = atom.view("a") - const la2 = atom.view("a") - expect(la1).toStrictEqual(la2) - const lab1 = atom.view("a", "b") - const lab2 = atom.view("a", "b") - expect(lab1).toStrictEqual(lab2) - }) - - describe("readonly, safe key, simple", () => { - const source = Atom.create({ a: 5 }) - const view = source.view("a") - - expect(view.get()).toEqual(5) - - source.modify(x => ({ a: x.a + 1 })) - - expect(view.get()).toEqual(6) - }) - - describe("readonly, safe key, complex", () => { - const source = Atom.create({ a: { b: { c: 5 } } }) - const view = source.view("a", "b", "c") - - expect(view.get()).toEqual(5) - - source.modify(x => ({ a: { b: { c: x.a.b.c + 1 } } })) - - expect(view.get()).toEqual(6) - }) - - describe("observable semantics: distinct values", () => { - const source = Atom.create(1) - const view = source.view(x => x + 1) - - const sourceOs: number[] = [] - const sourceSub = source.subscribe(x => sourceOs.push(x)) - - const viewOs: number[] = [] - const viewSub = view.subscribe(x => viewOs.push(x)) - ;[2, 2, 2, 3, 3, 3, 1, 1, 1].forEach(x => source.set(x)) - - expect(viewOs).toEqual([2, 3, 4, 2]) - expect(sourceOs).toEqual([1, 2, 3, 1]) - - sourceSub.unsubscribe() - viewSub.unsubscribe() - }) - - testDerivedAtom((a, f, onCalled) => - a.view(x => { - onCalled(x) - return f(x) - }) - ) - - describe("complex expression", () => { - const source = Atom.create({ a: { b: { c: 5 } } }) - const lensed = source.lens("a", "b", "c") - const view = lensed.view(x => x + 5 > 0) - - expect(view.get()).toEqual(true) - - lensed.set(6) - expect(view.get()).toEqual(true) - - lensed.set(-5) - expect(view.get()).toEqual(false) - }) - }) - - describe("atom value caching", () => { - describe("static", () => { - const source = Atom.create(-1) - - let called1 = 0 - const a1 = source.view(x => { - called1 = called1 + 1 - return x + 1 - }) - - let called2 = 0 - const a2 = a1.view(x => { - called2 = called2 + 1 - return -x - }) - - let called3 = 0 - const a3 = a2.view(x => { - called3 = called3 + 1 - return `Hi ${x}` - }) - - let called4 = 0 - const a4 = a2.view(x => { - called4 = called4 + 1 - return `Ho ${x}` - }) - - function testCalls(a: number, b: number, c: number, d: number) { - expect([called1, called2, called3, called4]).toEqual([a, b, c, d]) - } - - source.set(0) - testCalls(0, 0, 0, 0) - - expect(a3.get()).toEqual("Hi -1") - testCalls(1, 1, 1, 0) - - expect(a4.get()).toEqual("Ho -1") - testCalls(2, 2, 1, 1) - - source.set(1) - testCalls(2, 2, 1, 1) - }) - - describe("subscribed", () => { - const source = Atom.create(0) - - let called1 = 0 - const a1 = source.view(x => { - called1 = called1 + 1 - return x + 1 - }) - - let called2 = 0 - const a2 = a1.view(x => { - called2 = called2 + 1 - return -x - }) - - let called3 = 0 - const a3 = a2.view(x => { - called3 = called3 + 1 - return x * 2 - }) - - let called4 = 0 - const a4 = a3.view(x => { - called4 = called4 + 1 - return `Hi ${x}` - }) - - let called5 = 0 - const a5 = a3.view(x => { - called5 = called5 + 1 - return `Ho ${x}` - }) - - let called6 = 0 - const a6 = a3.view(x => { - called6 = called6 + 1 - return `HU ${x}` - }) - - function testCalls(a: number, b: number, c: number, d: number, e: number, f: number) { - expect([called1, called2, called3, called4, called5, called5]).toEqual([a, b, c, d, e, f]) - } - - const test = merge(a4, a5, a6) - testCalls(0, 0, 0, 0, 0, 0) - - const observations: string[] = [] - const sub = test.subscribe(x => observations.push(x)) - testCalls(1, 1, 1, 1, 1, 1) - - source.set(1) - testCalls(2, 2, 2, 2, 2, 2) - - source.set(2) - testCalls(3, 3, 3, 3, 3, 3) - - expect(observations).toEqual(["Hi -2", "Ho -2", "HU -2", "Hi -4", "Ho -4", "HU -4", "Hi -6", "Ho -6", "HU -6"]) - - sub.unsubscribe() - }) - - describe("with Atom.combine/static", () => { - const source = Atom.create(0) - - let called1 = 0 - const a1 = source.view(x => { - called1 = called1 + 1 - return x + 1 - }) + describe("plain", () => { + testAtom(x => Atom.create(x)) + }) + + describe("lens", () => { + describe("lensed atoms cached", () => { + const atom = Atom.create({ a: { b: { c: 1 } } }) + const la1 = atom.lens("a") + const la2 = atom.lens("a") + expect(la1).toStrictEqual(la2) + const lab1 = atom.lens("a", "b") + const lab2 = atom.lens("a", "b") + expect(lab1).toStrictEqual(lab2) + }) + + describe("lensed, nested safe key", () => { + testAtom(x => { + const source = Atom.create({ a: { b: { c: x } } }) + return source.lens("a", "b", "c") + }) + }) + + describe("lensed, safe key, chained lenses", () => { + testAtom(x => { + const source = Atom.create({ a: { b: { c: x } } }) + return source.lens("a").lens("b").lens("c") + }) + }) + + describe("lensed, chained + complex", () => { + const source = Atom.create({ a: { b: { c: 5 } } }) + const lensed = source + .lens("a") + .lens("b") + .lens("c") + .lens( + Lens.create( + (x: number) => x + 1, + (v: number) => v - 1, + ), + ) + + expect(lensed.get()).toEqual(6) + + lensed.set(6) + + expect(lensed.get()).toEqual(6) + }) + + describe("lensed, safe key, chained + complex", () => { + const source = Atom.create({ a: { b: { c: 5 } } }) + const lensed = source + .lens("a") + .lens("b") + .lens("c") + .lens( + Lens.create( + (x: number) => x + 1, + (v: number) => v - 1, + ), + ) + + expect(lensed.get()).toEqual(6) + + lensed.set(6) + + expect(lensed.get()).toEqual(6) + }) + + describe("lens then view", () => { + const x1 = Atom.create({ a: { b: 5 } }) + const x2 = x1 + .lens("a") + .view(x => x.b) + .view(x => x + 1) + const x3 = x1.lens("a").lens("b") + + expect(x3.get()).toEqual(5) + expect(x2.get()).toEqual(6) + + x3.set(6) + + expect(x3.get()).toEqual(6) + expect(x2.get()).toEqual(7) + expect(structEq({ a: { b: 6 } }, x1.get())).toBeTruthy() + }) + + describe("index lens", () => { + describe("generic", () => { + testAtom(x => { + const source = Atom.create([x, 2, 3]) + return source.lens( + Lens.index(0).compose( + Lens.create( + (x: number | undefined) => x!, + v => v, + ), + ), + ) as Atom + }) + }) + + describe("atom interface", () => { + const source = Atom.create([1, 2, 3]) + const first = source.lens(Lens.index(0)) + expect(first.get()).toEqual(1) + first.set(10) + expect(first.get()).toEqual(10) + expect(structEq(source.get(), [10, 2, 3])).toBeTruthy() + source.set([100, 2, 3]) + expect(first.get()).toEqual(100) + source.set([2, 3]) + expect(first.get()).toEqual(2) + }) + + describe("observing lensed", () => { + const source = Atom.create([1, 2, 3]) + const first = source.lens(Lens.index(0)) + const observations: Option[] = [] + const subscription = first.subscribe((x: Option) => observations.push(x)) + + first.set(10) + source.set([100, 2, 3]) + source.set([2, 3]) + source.set([1000, 2, 3]) + + expect(observations).toEqual([1, 10, 100, 2, 1000]) + subscription.unsubscribe() + }) + }) + + testDerivedAtom((a, f, onCalled) => + a.lens( + Lens.create( + x => { + onCalled(x) + return f(x) + }, + v => v, + ), + ), + ) + }) + + describe("view", () => { + describe("readonly, getter, simple", () => { + const source = Atom.create(5) + const view = source.view(x => x + 1) + + expect(source.get()).toEqual(5) + expect(view.get()).toEqual(6) + + source.modify(x => x + 1) + + expect(source.get()).toEqual(6) + expect(view.get()).toEqual(7) + }) + + describe("viewed atoms are cached", () => { + const atom = Atom.create({ a: { b: { c: 1 } } }) + const la1 = atom.view("a") + const la2 = atom.view("a") + expect(la1).toStrictEqual(la2) + const lab1 = atom.view("a", "b") + const lab2 = atom.view("a", "b") + expect(lab1).toStrictEqual(lab2) + }) + + describe("readonly, safe key, simple", () => { + const source = Atom.create({ a: 5 }) + const view = source.view("a") + + expect(view.get()).toEqual(5) + + source.modify(x => ({ a: x.a + 1 })) + + expect(view.get()).toEqual(6) + }) + + describe("readonly, safe key, complex", () => { + const source = Atom.create({ a: { b: { c: 5 } } }) + const view = source.view("a", "b", "c") + + expect(view.get()).toEqual(5) + + source.modify(x => ({ a: { b: { c: x.a.b.c + 1 } } })) + + expect(view.get()).toEqual(6) + }) + + describe("observable semantics: distinct values", () => { + const source = Atom.create(1) + const view = source.view(x => x + 1) + + const sourceOs: number[] = [] + const sourceSub = source.subscribe(x => sourceOs.push(x)) + + const viewOs: number[] = [] + const viewSub = view.subscribe(x => viewOs.push(x)) + ;[2, 2, 2, 3, 3, 3, 1, 1, 1].forEach(x => source.set(x)) + + expect(viewOs).toEqual([2, 3, 4, 2]) + expect(sourceOs).toEqual([1, 2, 3, 1]) + + sourceSub.unsubscribe() + viewSub.unsubscribe() + }) + + testDerivedAtom((a, f, onCalled) => + a.view(x => { + onCalled(x) + return f(x) + }), + ) + + describe("complex expression", () => { + const source = Atom.create({ a: { b: { c: 5 } } }) + const lensed = source.lens("a", "b", "c") + const view = lensed.view(x => x + 5 > 0) + + expect(view.get()).toEqual(true) + + lensed.set(6) + expect(view.get()).toEqual(true) + + lensed.set(-5) + expect(view.get()).toEqual(false) + }) + }) + + describe("atom value caching", () => { + describe("static", () => { + const source = Atom.create(-1) + + let called1 = 0 + const a1 = source.view(x => { + called1 = called1 + 1 + return x + 1 + }) + + let called2 = 0 + const a2 = a1.view(x => { + called2 = called2 + 1 + return -x + }) + + let called3 = 0 + const a3 = a2.view(x => { + called3 = called3 + 1 + return `Hi ${x}` + }) + + let called4 = 0 + const a4 = a2.view(x => { + called4 = called4 + 1 + return `Ho ${x}` + }) + + function testCalls(a: number, b: number, c: number, d: number) { + expect([called1, called2, called3, called4]).toEqual([a, b, c, d]) + } + + source.set(0) + testCalls(0, 0, 0, 0) + + expect(a3.get()).toEqual("Hi -1") + testCalls(1, 1, 1, 0) + + expect(a4.get()).toEqual("Ho -1") + testCalls(2, 2, 1, 1) + + source.set(1) + testCalls(2, 2, 1, 1) + }) + + describe("subscribed", () => { + const source = Atom.create(0) + + let called1 = 0 + const a1 = source.view(x => { + called1 = called1 + 1 + return x + 1 + }) + + let called2 = 0 + const a2 = a1.view(x => { + called2 = called2 + 1 + return -x + }) + + let called3 = 0 + const a3 = a2.view(x => { + called3 = called3 + 1 + return x * 2 + }) + + let called4 = 0 + const a4 = a3.view(x => { + called4 = called4 + 1 + return `Hi ${x}` + }) + + let called5 = 0 + const a5 = a3.view(x => { + called5 = called5 + 1 + return `Ho ${x}` + }) + + let called6 = 0 + const a6 = a3.view(x => { + called6 = called6 + 1 + return `HU ${x}` + }) + + function testCalls(a: number, b: number, c: number, d: number, e: number, f: number) { + expect([called1, called2, called3, called4, called5, called5]).toEqual([a, b, c, d, e, f]) + } + + const test = merge(a4, a5, a6) + testCalls(0, 0, 0, 0, 0, 0) + + const observations: string[] = [] + const sub = test.subscribe(x => observations.push(x)) + testCalls(1, 1, 1, 1, 1, 1) + + source.set(1) + testCalls(2, 2, 2, 2, 2, 2) + + source.set(2) + testCalls(3, 3, 3, 3, 3, 3) + + expect(observations).toEqual(["Hi -2", "Ho -2", "HU -2", "Hi -4", "Ho -4", "HU -4", "Hi -6", "Ho -6", "HU -6"]) + + sub.unsubscribe() + }) + + describe("with Atom.combine/static", () => { + const source = Atom.create(0) + + let called1 = 0 + const a1 = source.view(x => { + called1 = called1 + 1 + return x + 1 + }) - let called2 = 0 - const a2 = a1.view(x => { - called2 = called2 + 1 - return -x - }) + let called2 = 0 + const a2 = a1.view(x => { + called2 = called2 + 1 + return -x + }) - let called3 = 0 - const a3 = a2.view(x => { - called3 = called3 + 1 - return x * 2 - }) + let called3 = 0 + const a3 = a2.view(x => { + called3 = called3 + 1 + return x * 2 + }) - let called4 = 0 - const a4 = a3.view(x => { - called4 = called4 + 1 - return `Hi ${x}` - }) + let called4 = 0 + const a4 = a3.view(x => { + called4 = called4 + 1 + return `Hi ${x}` + }) - let called5 = 0 - const a5 = a3.view(x => { - called5 = called5 + 1 - return `Ho ${x}` - }) + let called5 = 0 + const a5 = a3.view(x => { + called5 = called5 + 1 + return `Ho ${x}` + }) - let called6 = 0 - const a6 = a3.view(x => { - called6 = called6 + 1 - return `HU ${x}` - }) + let called6 = 0 + const a6 = a3.view(x => { + called6 = called6 + 1 + return `HU ${x}` + }) - function testCalls(a: number, b: number, c: number, d: number, e: number, f: number) { - expect([called1, called2, called3, called4, called5, called5]).toEqual([a, b, c, d, e, f]) - } - - testCalls(0, 0, 0, 0, 0, 0) - - const combined = Atom.combine(a4, a5, a6, (x, y, z) => [x, y, z]) - testCalls(0, 0, 0, 0, 0, 0) + function testCalls(a: number, b: number, c: number, d: number, e: number, f: number) { + expect([called1, called2, called3, called4, called5, called5]).toEqual([a, b, c, d, e, f]) + } + + testCalls(0, 0, 0, 0, 0, 0) + + const combined = Atom.combine(a4, a5, a6, (x, y, z) => [x, y, z]) + testCalls(0, 0, 0, 0, 0, 0) - expect(combined.get()).toEqual(["Hi -2", "Ho -2", "HU -2"]) - - // it's ok to have 3 calls here each as until we are subscribed to this - // atom it doesn't track its sources - testCalls(3, 3, 3, 1, 1, 1) + expect(combined.get()).toEqual(["Hi -2", "Ho -2", "HU -2"]) + + // it's ok to have 3 calls here each as until we are subscribed to this + // atom it doesn't track its sources + testCalls(3, 3, 3, 1, 1, 1) - source.set(1) - testCalls(3, 3, 3, 1, 1, 1) - - expect(a4.get()).toEqual("Hi -4") - testCalls(4, 4, 4, 2, 1, 1) - }) - }) - - describe("combine", () => { - describe("constant", () => { - const combined = Atom.combine(Atom.create(1), Atom.create(false), Atom.create("test"), (x, y, z) => - y && x < 0 ? z.toUpperCase() : "NO" - ) + source.set(1) + testCalls(3, 3, 3, 1, 1, 1) + + expect(a4.get()).toEqual("Hi -4") + testCalls(4, 4, 4, 2, 1, 1) + }) + }) + + describe("combine", () => { + describe("constant", () => { + const combined = Atom.combine(Atom.create(1), Atom.create(false), Atom.create("test"), (x, y, z) => + y && x < 0 ? z.toUpperCase() : "NO", + ) - expect(combined.get()).toEqual("NO") - }) + expect(combined.get()).toEqual("NO") + }) - describe("dynamic, unsubscribed", () => { - const s1 = Atom.create(1) - const s2 = Atom.create(false) - const s3 = Atom.create("test") + describe("dynamic, unsubscribed", () => { + const s1 = Atom.create(1) + const s2 = Atom.create(false) + const s3 = Atom.create("test") - const combined = Atom.combine(s1, s2, s3, (x, y, z) => (y && x < 0 ? z.toUpperCase() : "NO")) + const combined = Atom.combine(s1, s2, s3, (x, y, z) => (y && x < 0 ? z.toUpperCase() : "NO")) - expect(combined.get()).toEqual("NO") + expect(combined.get()).toEqual("NO") - s1.set(-1) - expect(combined.get()).toEqual("NO") + s1.set(-1) + expect(combined.get()).toEqual("NO") - s2.set(true) - expect(combined.get()).toEqual("TEST") - - s3.set("heLLo") - expect(combined.get()).toEqual("HELLO") + s2.set(true) + expect(combined.get()).toEqual("TEST") + + s3.set("heLLo") + expect(combined.get()).toEqual("HELLO") - s1.set(100) - expect(combined.get()).toEqual("NO") - }) + s1.set(100) + expect(combined.get()).toEqual("NO") + }) - describe("dynamic, subscribed", () => { - const s1 = Atom.create(1) - const s2 = Atom.create(false) - const s3 = Atom.create("test") - const observations: string[] = [] - - const combined = Atom.combine(s1, s2, s3, (x, y, z) => (y && x < 0 ? z.toUpperCase() : "NO")) - - const sub = combined.subscribe(x => observations.push(x)) + describe("dynamic, subscribed", () => { + const s1 = Atom.create(1) + const s2 = Atom.create(false) + const s3 = Atom.create("test") + const observations: string[] = [] + + const combined = Atom.combine(s1, s2, s3, (x, y, z) => (y && x < 0 ? z.toUpperCase() : "NO")) + + const sub = combined.subscribe(x => observations.push(x)) - expect(combined.get()).toEqual("NO") - - s1.set(-1) - expect(combined.get()).toEqual("NO") + expect(combined.get()).toEqual("NO") + + s1.set(-1) + expect(combined.get()).toEqual("NO") - s2.set(true) - expect(combined.get()).toEqual("TEST") - - s3.set("heLLo") - expect(combined.get()).toEqual("HELLO") + s2.set(true) + expect(combined.get()).toEqual("TEST") + + s3.set("heLLo") + expect(combined.get()).toEqual("HELLO") - s1.set(100) - expect(combined.get()).toEqual("NO") + s1.set(100) + expect(combined.get()).toEqual("NO") - expect(observations).toEqual(["NO", "TEST", "HELLO", "NO"]) + expect(observations).toEqual(["NO", "TEST", "HELLO", "NO"]) - sub.unsubscribe() - }) + sub.unsubscribe() + }) - testDerivedAtom((a, f, onCalled) => - Atom.combine(a, Atom.create(0), a => { - onCalled(a) - return f(a) - }) - ) - }) + testDerivedAtom((a, f, onCalled) => + Atom.combine(a, Atom.create(0), a => { + onCalled(a) + return f(a) + }), + ) + }) - describe("logger", () => { - let consoleLogFireTime = 0 + describe("logger", () => { + let consoleLogFireTime = 0 - const atom = Atom.create("bar") - const consoleLogArguments: string[][] = [] - const logAtom = Atom.log(atom, (prevState: string, next: string) => { - consoleLogFireTime = consoleLogFireTime + 1 - consoleLogArguments.push([prevState, next]) - }) - logAtom.set("foo") + const atom = Atom.create("bar") + const consoleLogArguments: string[][] = [] + const logAtom = Atom.log(atom, (prevState: string, next: string) => { + consoleLogFireTime = consoleLogFireTime + 1 + consoleLogArguments.push([prevState, next]) + }) + logAtom.set("foo") - expect(consoleLogFireTime).toEqual(2) - expect(consoleLogArguments).toEqual([ - ["bar", "bar"], - ["bar", "foo"], - ]) - }) + expect(consoleLogFireTime).toEqual(2) + expect(consoleLogArguments).toEqual([ + ["bar", "bar"], + ["bar", "foo"], + ]) + }) - describe("fromObservable", () => { - test("emits atom", async () => { - const a = await firstValueFrom(Atom.fromObservable(from([1]))) - expect(a.get()).toEqual(1) - }) + describe("fromObservable", () => { + test("emits atom", async () => { + const a = await firstValueFrom(Atom.fromObservable(from([1]))) + expect(a.get()).toEqual(1) + }) - test("emits atom once", async () => { - const a = await firstValueFrom( - merge( - Atom.fromObservable(of(Array.from(new Array(15).fill(1)).map(() => Math.random()))), - from(["hello"]) - ).pipe(take(2), toArray()) - ) - - expect(a[1]).toEqual("hello") - }) - - test("does not subscribe to source immediately", () => { - let subscribed = false - - const src = new Observable(o => { - subscribed = true - o.complete() - return () => (subscribed = false) - }) - - Atom.fromObservable(src) - expect(subscribed).toEqual(false) - }) - - test("one sub max, unsub when not in use", async () => { - let subCount = 0 - - const src = new Observable(o => { - subCount = subCount + 1 - o.next(1) - return () => (subCount = subCount - 1) - }) - - const a = Atom.fromObservable(src) - - // no subs until we have subscribed to use the atom - expect(subCount).toEqual(0) - - const subs = Array.from(new Array(5)).map(() => - a.subscribe(a => { - expect(a.get()).toEqual(1) - }) - ) - - // exactly one sub, no matter how many times the atom observable was subbed to - expect(subCount).toEqual(1) - - subs.forEach(s => s.unsubscribe()) - - // no subs to source when unused - expect(subCount).toEqual(0) - }) - - test("does not return atom if source has no value", async () => { - const r = await firstValueFrom(merge(Atom.fromObservable(NEVER), from(["hello"])).pipe(take(1), toArray())) - expect(r).toEqual(["hello"]) - }) - - test("atom values correspond to source", async () => { - const src = new Subject() - - const r = firstValueFrom( - Atom.fromObservable(src).pipe( - tap(async a => { - const srcValues = Array.from(new Array(10), () => Math.random()) - - srcValues.forEach(x => { - src.next(x) - expect(a.get()).toEqual(x) - }) - - const atomValues = await firstValueFrom(a.pipe(toArray())) - expect(atomValues).toEqual(srcValues) - }), - take(1) - ) - ) - - src.next(0) - await r - }) - - test("atom values not updated after unsubscribed from result", async () => { - const src = new Subject() - let atom!: ReadOnlyAtom - - const sub = Atom.fromObservable(src) - .pipe(tap(a => (atom = a))) - .subscribe() - - src.next(1) - expect(atom.get()).toEqual(1) - src.next(2) - expect(atom.get()).toEqual(2) - - sub.unsubscribe() - - src.next(5) - expect(atom.get()).toEqual(2) - }) - - test("source error is propagated", async () => { - try { - await firstValueFrom(Atom.fromObservable(throwError(new Error("hello")))) - fail() - } catch (e) { - expect(e).toEqual(new Error("hello")) - } - }) - - test("source completion is propagated 1", async () => { - expect( - await firstValueFrom( - Atom.fromObservable(EMPTY).pipe( - materialize(), - map(x => x.kind), - toArray() - ) - ) - ).toEqual(["C"]) - }) - - test("source completion is propagated 2", async () => { - expect( - await Atom.fromObservable(of([1, 2, 3])) - .pipe( - materialize(), - map(x => x.kind), - toArray() - ) - .toPromise() - ).toEqual(["N", "C"]) - }) - }) + test("emits atom once", async () => { + const a = await firstValueFrom( + merge( + Atom.fromObservable(of(Array.from(new Array(15).fill(1)).map(() => Math.random()))), + from(["hello"]), + ).pipe(take(2), toArray()), + ) + + expect(a[1]).toEqual("hello") + }) + + test("does not subscribe to source immediately", () => { + let subscribed = false + + const src = new Observable(o => { + subscribed = true + o.complete() + return () => (subscribed = false) + }) + + Atom.fromObservable(src) + expect(subscribed).toEqual(false) + }) + + test("one sub max, unsub when not in use", async () => { + let subCount = 0 + + const src = new Observable(o => { + subCount = subCount + 1 + o.next(1) + return () => (subCount = subCount - 1) + }) + + const a = Atom.fromObservable(src) + + // no subs until we have subscribed to use the atom + expect(subCount).toEqual(0) + + const subs = Array.from(new Array(5)).map(() => + a.subscribe(a => { + expect(a.get()).toEqual(1) + }), + ) + + // exactly one sub, no matter how many times the atom observable was subbed to + expect(subCount).toEqual(1) + + subs.forEach(s => s.unsubscribe()) + + // no subs to source when unused + expect(subCount).toEqual(0) + }) + + test("does not return atom if source has no value", async () => { + const r = await firstValueFrom(merge(Atom.fromObservable(NEVER), from(["hello"])).pipe(take(1), toArray())) + expect(r).toEqual(["hello"]) + }) + + test("atom values correspond to source", async () => { + const src = new Subject() + + const r = firstValueFrom( + Atom.fromObservable(src).pipe( + tap(async a => { + const srcValues = Array.from(new Array(10), () => Math.random()) + + srcValues.forEach(x => { + src.next(x) + expect(a.get()).toEqual(x) + }) + + const atomValues = await firstValueFrom(a.pipe(toArray())) + expect(atomValues).toEqual(srcValues) + }), + take(1), + ), + ) + + src.next(0) + await r + }) + + test("atom values not updated after unsubscribed from result", async () => { + const src = new Subject() + let atom!: ReadOnlyAtom + + const sub = Atom.fromObservable(src) + .pipe(tap(a => (atom = a))) + .subscribe() + + src.next(1) + expect(atom.get()).toEqual(1) + src.next(2) + expect(atom.get()).toEqual(2) + + sub.unsubscribe() + + src.next(5) + expect(atom.get()).toEqual(2) + }) + + test("source error is propagated", async () => { + try { + await firstValueFrom(Atom.fromObservable(throwError(new Error("hello")))) + throw new Error("Never happen") + } catch (e) { + expect(e).toEqual(new Error("hello")) + } + }) + + test("source completion is propagated 1", async () => { + expect( + await firstValueFrom( + Atom.fromObservable(EMPTY).pipe( + materialize(), + map(x => x.kind), + toArray(), + ), + ), + ).toEqual(["C"]) + }) + + test("source completion is propagated 2", async () => { + expect( + await Atom.fromObservable(of([1, 2, 3])) + .pipe( + materialize(), + map(x => x.kind), + toArray(), + ) + .toPromise(), + ).toEqual(["N", "C"]) + }) + }) }) diff --git a/packages/atom/src/lenses/by-index/index.test.ts b/packages/atom/src/lenses/by-index/index.test.ts index d7ee652..bcadd00 100644 --- a/packages/atom/src/lenses/by-index/index.test.ts +++ b/packages/atom/src/lenses/by-index/index.test.ts @@ -2,26 +2,26 @@ import { Atom } from "../../atom" import { byIndexFactory } from "." describe("byIndexFactory", () => { - it("should read and write to lens", () => { - const value$ = Atom.create(["hello"]) - const lensed$ = value$.lens(byIndexFactory(0, createDefaultValue)) - expect(lensed$.get()).toEqual("hello") - lensed$.set("world") - expect(lensed$.get()).toEqual("world") - expect(value$.get()).toEqual(["world"]) - }) + it("should read and write to lens", () => { + const value$ = Atom.create(["hello"]) + const lensed$ = value$.lens(byIndexFactory(0, createDefaultValue)) + expect(lensed$.get()).toEqual("hello") + lensed$.set("world") + expect(lensed$.get()).toEqual("world") + expect(value$.get()).toEqual(["world"]) + }) - it("should return default value when index not exist", () => { - const value$ = Atom.create(["hello"]) - const lensed$ = value$.lens(byIndexFactory(1, createDefaultValue)) - expect(lensed$.get()).toEqual("defaultValue") - expect(value$.get()).toEqual(["hello"]) - lensed$.set("world") - expect(lensed$.get()).toEqual("world") - expect(value$.get()).toEqual(["hello", "world"]) - }) + it("should return default value when index not exist", () => { + const value$ = Atom.create(["hello"]) + const lensed$ = value$.lens(byIndexFactory(1, createDefaultValue)) + expect(lensed$.get()).toEqual("defaultValue") + expect(value$.get()).toEqual(["hello"]) + lensed$.set("world") + expect(lensed$.get()).toEqual("world") + expect(value$.get()).toEqual(["hello", "world"]) + }) }) function createDefaultValue() { - return "defaultValue" + return "defaultValue" } diff --git a/packages/atom/src/lenses/by-index/index.ts b/packages/atom/src/lenses/by-index/index.ts index 6669bd9..0518d5e 100644 --- a/packages/atom/src/lenses/by-index/index.ts +++ b/packages/atom/src/lenses/by-index/index.ts @@ -6,12 +6,12 @@ import { Lens } from "@rixio/lens" */ export function byIndexFactory(index: number, createDefaultValue: () => T): Lens { - return Lens.create( - x => x[index] || createDefaultValue(), - (x, xs) => { - const result = [...xs] - result[index] = x || createDefaultValue() - return result - } - ) + return Lens.create( + x => x[index] || createDefaultValue(), + (x, xs) => { + const result = [...xs] + result[index] = x || createDefaultValue() + return result + }, + ) } diff --git a/packages/atom/src/lenses/by-key-immutable/index.test.ts b/packages/atom/src/lenses/by-key-immutable/index.test.ts index 2b69d93..62b0896 100644 --- a/packages/atom/src/lenses/by-key-immutable/index.test.ts +++ b/packages/atom/src/lenses/by-key-immutable/index.test.ts @@ -3,30 +3,30 @@ import { Atom } from "../../atom" import { byKeyImmutableFactory } from "./index" type UserId = string & { - __IS_USER_ID__: true + __IS_USER_ID__: true } type UserData = { - id: UserId - name: string - surname: string + id: UserId + name: string + surname: string } describe("byKeyImmutable", () => { - it("should write new entry by key", async () => { - const userFactory = byKeyImmutableFactory(id => ({ - id, - name: "John", - surname: "Doe", - })) - const value$ = Atom.create>(IM()) + it("should write new entry by key", async () => { + const userFactory = byKeyImmutableFactory(id => ({ + id, + name: "John", + surname: "Doe", + })) + const value$ = Atom.create>(IM()) - const userId = "user-1" as UserId - const lensed$ = value$.lens(userFactory(userId)) - expect(lensed$.get()).toEqual({ - id: userId, - name: "John", - surname: "Doe", - }) - }) + const userId = "user-1" as UserId + const lensed$ = value$.lens(userFactory(userId)) + expect(lensed$.get()).toEqual({ + id: userId, + name: "John", + surname: "Doe", + }) + }) }) diff --git a/packages/atom/src/lenses/by-key-immutable/index.ts b/packages/atom/src/lenses/by-key-immutable/index.ts index e621509..e93047a 100644 --- a/packages/atom/src/lenses/by-key-immutable/index.ts +++ b/packages/atom/src/lenses/by-key-immutable/index.ts @@ -1,4 +1,4 @@ -import { Map as IM } from "immutable" +import type { Map as IM } from "immutable" import { Lens, SimpleCache } from "@rixio/lens" /** @@ -7,11 +7,11 @@ import { Lens, SimpleCache } from "@rixio/lens" */ export function byKeyImmutableFactory(createDefaultValue: (key: K) => V): (key: K) => Lens, V> { - const cache = new SimpleCache, V>>((x: K) => - Lens.create( - (s: IM) => (s.has(x) ? s.get(x)! : createDefaultValue(x)), - (s, xs) => xs.set(x, s) - ) - ) - return x => cache.getOrCreate(x) + const cache = new SimpleCache, V>>((x: K) => + Lens.create( + (s: IM) => (s.has(x) ? s.get(x)! : createDefaultValue(x)), + (s, xs) => xs.set(x, s), + ), + ) + return x => cache.getOrCreate(x) } diff --git a/packages/atom/src/lenses/by-key/index.test.ts b/packages/atom/src/lenses/by-key/index.test.ts index 470862d..dc40706 100644 --- a/packages/atom/src/lenses/by-key/index.test.ts +++ b/packages/atom/src/lenses/by-key/index.test.ts @@ -2,30 +2,30 @@ import { Atom } from "../../atom" import { byKeyFactory } from "./index" type UserId = string & { - __IS_USER_ID__: true + __IS_USER_ID__: true } type UserData = { - id: UserId - name: string - surname: string + id: UserId + name: string + surname: string } describe("byKey", () => { - it("should write new entry by key", async () => { - const userFactory = byKeyFactory(id => ({ - id, - name: "John", - surname: "Doe", - })) - const value$ = Atom.create>({}) + it("should write new entry by key", async () => { + const userFactory = byKeyFactory(id => ({ + id, + name: "John", + surname: "Doe", + })) + const value$ = Atom.create>({}) - const userId = "user-1" as UserId - const lensed$ = value$.lens(userFactory(userId)) - expect(lensed$.get()).toEqual({ - id: userId, - name: "John", - surname: "Doe", - }) - }) + const userId = "user-1" as UserId + const lensed$ = value$.lens(userFactory(userId)) + expect(lensed$.get()).toEqual({ + id: userId, + name: "John", + surname: "Doe", + }) + }) }) diff --git a/packages/atom/src/lenses/by-key/index.ts b/packages/atom/src/lenses/by-key/index.ts index 95f9334..c1d6001 100644 --- a/packages/atom/src/lenses/by-key/index.ts +++ b/packages/atom/src/lenses/by-key/index.ts @@ -6,18 +6,18 @@ import { Lens, SimpleCache } from "@rixio/lens" */ export function byKeyFactory( - createDefaultValue: (key: K) => V + createDefaultValue: (key: K) => V, ): (key: K) => Lens, V> { - const cache = new SimpleCache, V>>((x: K) => - Lens.create( - s => (x in s ? s[x] : createDefaultValue(x)), - (s, xs) => { - return { - ...xs, - [x]: s, - } - } - ) - ) - return x => cache.getOrCreate(x) + const cache = new SimpleCache, V>>((x: K) => + Lens.create( + s => (x in s ? s[x] : createDefaultValue(x)), + (s, xs) => { + return { + ...xs, + [x]: s, + } + }, + ), + ) + return x => cache.getOrCreate(x) } diff --git a/packages/atom/src/lenses/equity/index.test.ts b/packages/atom/src/lenses/equity/index.test.ts index 3a78047..8327148 100644 --- a/packages/atom/src/lenses/equity/index.test.ts +++ b/packages/atom/src/lenses/equity/index.test.ts @@ -2,17 +2,17 @@ import { Atom } from "../../index" import { equityLensFactory } from "." describe("equityLensFactory", () => { - it("should return true", () => { - const factory = equityLensFactory() - const value$ = Atom.create("test") - const lensed$ = value$.lens(factory("test")) - expect(lensed$.get()).toEqual(true) - }) + it("should return true", () => { + const factory = equityLensFactory() + const value$ = Atom.create("test") + const lensed$ = value$.lens(factory("test")) + expect(lensed$.get()).toEqual(true) + }) - it("should return false", () => { - const factory = equityLensFactory() - const value$ = Atom.create("test") - const lensed$ = value$.lens(factory("test1")) - expect(lensed$.get()).toEqual(false) - }) + it("should return false", () => { + const factory = equityLensFactory() + const value$ = Atom.create("test") + const lensed$ = value$.lens(factory("test1")) + expect(lensed$.get()).toEqual(false) + }) }) diff --git a/packages/atom/src/lenses/equity/index.ts b/packages/atom/src/lenses/equity/index.ts index 8500251..dded678 100644 --- a/packages/atom/src/lenses/equity/index.ts +++ b/packages/atom/src/lenses/equity/index.ts @@ -1,11 +1,11 @@ import { Lens, SimpleCache } from "@rixio/lens" export function equityLensFactory() { - const cache = new SimpleCache>((key: T) => - Lens.create( - x => x === key, - () => key - ) - ) - return (x: T) => cache.getOrCreate(x) + const cache = new SimpleCache>((key: T) => + Lens.create( + x => x === key, + () => key, + ), + ) + return (x: T) => cache.getOrCreate(x) } diff --git a/packages/cache/src/domain.ts b/packages/cache/src/domain.ts index 84606b0..e592abc 100644 --- a/packages/cache/src/domain.ts +++ b/packages/cache/src/domain.ts @@ -1,67 +1,67 @@ export class CacheBase {} export class CacheIdle extends CacheBase { - readonly status = "idle" - static create = () => { - return new CacheIdle() - } + readonly status = "idle" + static create = () => { + return new CacheIdle() + } } export class CachePending extends CacheBase { - readonly status = "pending" - static create = () => { - return new CachePending() - } + readonly status = "pending" + static create = () => { + return new CachePending() + } } export class CacheRejected extends CacheBase { - readonly status = "rejected" - constructor(public readonly error: unknown) { - super() - } + readonly status = "rejected" + constructor(public readonly error: unknown) { + super() + } - static create = (err: unknown) => { - return new CacheRejected(err) - } + static create = (err: unknown) => { + return new CacheRejected(err) + } } export class CacheFulfilled extends CacheBase { - readonly status = "fulfilled" - readonly timestamp = Date.now() + readonly status = "fulfilled" + readonly timestamp = Date.now() - constructor(public readonly value: T) { - super() - } - static create = (value: T) => new CacheFulfilled(value) + constructor(public readonly value: T) { + super() + } + static create = (value: T) => new CacheFulfilled(value) } export type CacheState = CacheIdle | CachePending | CacheRejected | CacheFulfilled export type KeyEventAdd = { - type: "add" - key: K + type: "add" + key: K } export type KeyEventError = { - type: "error" - key: K - error: unknown + type: "error" + key: K + error: unknown } export type KeyEvent = KeyEventAdd | KeyEventError export function createAddKeyEvent(key: K): KeyEventAdd { - return { - type: "add", - key, - } + return { + type: "add", + key, + } } export function createErrorKeyEvent(key: K, error: unknown): KeyEventError { - return { - type: "error", - key, - error, - } + return { + type: "error", + key, + error, + } } export type DataLoader = (key: K) => Promise diff --git a/packages/cache/src/key-memo/index.test.ts b/packages/cache/src/key-memo/index.test.ts index ea7a41a..7210528 100644 --- a/packages/cache/src/key-memo/index.test.ts +++ b/packages/cache/src/key-memo/index.test.ts @@ -1,125 +1,126 @@ import { Atom } from "@rixio/atom" import { Map as IM } from "immutable" import { Subscription } from "rxjs" -import { KeyEvent, CacheState, createAddKeyEvent, createErrorKeyEvent, CacheFulfilled, KeyEventError } from "../domain" +import type { KeyEvent, CacheState, KeyEventError } from "../domain" +import { createAddKeyEvent, createErrorKeyEvent, CacheFulfilled } from "../domain" import { UnknownError } from "../utils/errors" import { KeyMemoImpl } from "../key-memo" describe("KeyMemoImpl", () => { - jest.useFakeTimers() - - test("should have 2 batched requests (2 and 1)", async () => { - const testset = new TestSet() - const sub = new Subscription() - - const single = testset.cache.single("testing") - expect(testset.atom.get().size).toBe(0) - sub.add(single.subscribe()) - const other$ = testset.cache.single("other") - sub.add(other$.subscribe()) - expect(testset.impl.mock.calls).toHaveLength(0) - single.get() - await jest.runAllTimersAsync() - expect(single.atom.get()).toEqual(CacheFulfilled.create("testing")) - expect(testset.atom.get().size).toBe(2) - expect(testset.impl.mock.calls).toHaveLength(1) - - const other2$ = testset.cache.single("other2") - sub.add(other2$.subscribe()) - other2$.get() - await jest.runAllTimersAsync() - expect(other2$.atom.get()).toEqual(CacheFulfilled.create("other2")) - - expect(testset.impl.mock.calls).toHaveLength(2) - expect(testset.atom.get().size).toBe(3) - - sub.unsubscribe() - }) - - test("should not batch requests if key already loaded", async () => { - const testset = new TestSet() - const loadAndCheck = async () => { - const single1 = testset.cache.single("single") - single1.get() - await jest.runAllTimersAsync() - } - await loadAndCheck() - expect(testset.impl.mock.calls).toHaveLength(1) - await loadAndCheck() - expect(testset.impl.mock.calls).toHaveLength(1) - }) - - test("should be reloaded if cleared", async () => { - const testset = new TestSet() - const emitted: Array = [] - const key1$ = testset.cache.single("key1") - const sub = key1$.subscribe(x => emitted.push(x)) - key1$.get() - await jest.runAllTimersAsync() - expect(testset.impl.mock.calls).toHaveLength(1) - expect(emitted).toEqual(["key1"]) - key1$.clear() - key1$.get() - await jest.runAllTimersAsync() - expect(testset.impl.mock.calls).toHaveLength(2) - expect(emitted).toEqual(["key1", "key1"]) - sub.unsubscribe() - }) - - test("should put new entry in events Subject", async () => { - const testset = new TestSet() - - const emitted: KeyEvent[] = [] - const sub = testset.cache.events.subscribe(x => emitted.push(x)) - testset.cache.get("test").then() - expect(emitted.length).toBe(1) - expect(emitted[0]).toStrictEqual(createAddKeyEvent("test")) - - testset.cache.get("test2").then() - expect(emitted.length).toBe(2) - expect(emitted[1]).toStrictEqual(createAddKeyEvent("test2")) - - testset.cache.set("test3", "test3") - expect(emitted.length).toBe(3) - expect(emitted[2]).toStrictEqual(createAddKeyEvent("test3")) - - sub.unsubscribe() - }) - - test("should mark items as errors if load list fails", async () => { - const loader = jest.fn().mockImplementation(() => Promise.reject("rejected")) - const testset = new TestSet(undefined, loader) - const valuesEmitted: string[] = [] - const emittedEvents: KeyEvent[] = [] - testset.cache.events.subscribe(x => emittedEvents.push(x)) - const errorsEmitted: unknown[] = [] - const test$ = testset.cache.single("test") - const sub = test$.subscribe({ - next: x => valuesEmitted.push(x), - error: e => errorsEmitted.push(e), - }) - await jest.runAllTimersAsync() - expect(valuesEmitted.length).toBe(0) - expect(errorsEmitted.length).toBe(1) - expect(errorsEmitted[0]).toBeInstanceOf(UnknownError) - expect(emittedEvents.length).toEqual(3) - expect(emittedEvents[0]).toStrictEqual(createAddKeyEvent("test")) - expect(emittedEvents[1]).toStrictEqual(createErrorKeyEvent("test", "rejected")) - expect((emittedEvents[2] as KeyEventError).error).toBeInstanceOf(UnknownError) - sub.unsubscribe() - }) + jest.useFakeTimers() + + test("should have 2 batched requests (2 and 1)", async () => { + const testset = new TestSet() + const sub = new Subscription() + + const single = testset.cache.single("testing") + expect(testset.atom.get().size).toBe(0) + sub.add(single.subscribe()) + const other$ = testset.cache.single("other") + sub.add(other$.subscribe()) + expect(testset.impl.mock.calls).toHaveLength(0) + single.get() + await jest.runAllTimersAsync() + expect(single.atom.get()).toEqual(CacheFulfilled.create("testing")) + expect(testset.atom.get().size).toBe(2) + expect(testset.impl.mock.calls).toHaveLength(1) + + const other2$ = testset.cache.single("other2") + sub.add(other2$.subscribe()) + other2$.get() + await jest.runAllTimersAsync() + expect(other2$.atom.get()).toEqual(CacheFulfilled.create("other2")) + + expect(testset.impl.mock.calls).toHaveLength(2) + expect(testset.atom.get().size).toBe(3) + + sub.unsubscribe() + }) + + test("should not batch requests if key already loaded", async () => { + const testset = new TestSet() + const loadAndCheck = async () => { + const single1 = testset.cache.single("single") + single1.get() + await jest.runAllTimersAsync() + } + await loadAndCheck() + expect(testset.impl.mock.calls).toHaveLength(1) + await loadAndCheck() + expect(testset.impl.mock.calls).toHaveLength(1) + }) + + test("should be reloaded if cleared", async () => { + const testset = new TestSet() + const emitted: Array = [] + const key1$ = testset.cache.single("key1") + const sub = key1$.subscribe(x => emitted.push(x)) + key1$.get() + await jest.runAllTimersAsync() + expect(testset.impl.mock.calls).toHaveLength(1) + expect(emitted).toEqual(["key1"]) + key1$.clear() + key1$.get() + await jest.runAllTimersAsync() + expect(testset.impl.mock.calls).toHaveLength(2) + expect(emitted).toEqual(["key1", "key1"]) + sub.unsubscribe() + }) + + test("should put new entry in events Subject", async () => { + const testset = new TestSet() + + const emitted: KeyEvent[] = [] + const sub = testset.cache.events.subscribe(x => emitted.push(x)) + testset.cache.get("test").then() + expect(emitted.length).toBe(1) + expect(emitted[0]).toStrictEqual(createAddKeyEvent("test")) + + testset.cache.get("test2").then() + expect(emitted.length).toBe(2) + expect(emitted[1]).toStrictEqual(createAddKeyEvent("test2")) + + testset.cache.set("test3", "test3") + expect(emitted.length).toBe(3) + expect(emitted[2]).toStrictEqual(createAddKeyEvent("test3")) + + sub.unsubscribe() + }) + + test("should mark items as errors if load list fails", async () => { + const loader = jest.fn().mockImplementation(() => Promise.reject("rejected")) + const testset = new TestSet(undefined, loader) + const valuesEmitted: string[] = [] + const emittedEvents: KeyEvent[] = [] + testset.cache.events.subscribe(x => emittedEvents.push(x)) + const errorsEmitted: unknown[] = [] + const test$ = testset.cache.single("test") + const sub = test$.subscribe({ + next: x => valuesEmitted.push(x), + error: e => errorsEmitted.push(e), + }) + await jest.runAllTimersAsync() + expect(valuesEmitted.length).toBe(0) + expect(errorsEmitted.length).toBe(1) + expect(errorsEmitted[0]).toBeInstanceOf(UnknownError) + expect(emittedEvents.length).toEqual(3) + expect(emittedEvents[0]).toStrictEqual(createAddKeyEvent("test")) + expect(emittedEvents[1]).toStrictEqual(createErrorKeyEvent("test", "rejected")) + expect((emittedEvents[2] as KeyEventError).error).toBeInstanceOf(UnknownError) + sub.unsubscribe() + }) }) class TestSet { - readonly impl = jest.fn().mockImplementation((keys: string[]) => { - const timeout = this.timeout - const getValues = () => keys.map(k => [k, k]) - if (typeof timeout !== "number") return Promise.resolve(getValues()) - return new Promise(r => setTimeout(() => r(getValues()), timeout)) - }) + readonly impl = jest.fn().mockImplementation((keys: string[]) => { + const timeout = this.timeout + const getValues = () => keys.map(k => [k, k]) + if (typeof timeout !== "number") return Promise.resolve(getValues()) + return new Promise(r => setTimeout(() => r(getValues()), timeout)) + }) - readonly atom = Atom.create(IM>()) - readonly cache = new KeyMemoImpl(this.atom, this.customImpl || this.impl) + readonly atom = Atom.create(IM>()) + readonly cache = new KeyMemoImpl(this.atom, this.customImpl || this.impl) - constructor(private readonly timeout?: number, private readonly customImpl?: jest.Mock) {} + constructor(private readonly timeout?: number, private readonly customImpl?: jest.Mock) {} } diff --git a/packages/cache/src/key-memo/index.ts b/packages/cache/src/key-memo/index.ts index 75d6040..19d1e05 100644 --- a/packages/cache/src/key-memo/index.ts +++ b/packages/cache/src/key-memo/index.ts @@ -1,69 +1,72 @@ import { Map as IM } from "immutable" -import { Atom } from "@rixio/atom" +import type { Atom } from "@rixio/atom" import { SimpleCache } from "@rixio/lens" import { byKeyImmutable } from "@rixio/lens/build/lenses/by-key-immutable" -import { Observable, Subject } from "rxjs" +import type { Observable } from "rxjs" +import { Subject } from "rxjs" import { first } from "rxjs/operators" -import { CacheState, CacheIdle, KeyEvent, createErrorKeyEvent, createAddKeyEvent, ListDataLoader } from "../domain" +import type { CacheState, KeyEvent, ListDataLoader } from "../domain" +import { CacheIdle, createErrorKeyEvent, createAddKeyEvent } from "../domain" import { Batcher } from "../utils/batcher" -import { Memo, MemoImpl } from "../memo" +import type { Memo } from "../memo" +import { MemoImpl } from "../memo" import { KeyNotFoundError, UnknownError } from "../utils/errors" export interface KeyMemo { - get: (key: K, force?: boolean) => Promise - set: (key: K, value: V) => void - getAtom: (key: K) => Atom> - single: (key: K) => Memo + get: (key: K, force?: boolean) => Promise + set: (key: K, value: V) => void + getAtom: (key: K) => Atom> + single: (key: K) => Memo } export class KeyMemoImpl implements KeyMemo { - private readonly _batch: Batcher - private readonly _results = new Subject<[K, V | Error]>() - private readonly _lensFactory = byKeyImmutable>(() => CacheIdle.create()) - private readonly _events = new Subject>() - readonly events: Observable> = this._events + private readonly _batch: Batcher + private readonly _results = new Subject<[K, V | Error]>() + private readonly _lensFactory = byKeyImmutable>(() => CacheIdle.create()) + private readonly _events = new Subject>() + readonly events: Observable> = this._events - private readonly singles = new SimpleCache>(key => { - return new MemoImpl(this.getAtom(key), () => this.load(key)) - }) + private readonly singles = new SimpleCache>(key => { + return new MemoImpl(this.getAtom(key), () => this.load(key)) + }) - constructor( - private readonly map: Atom>>, - private readonly loader: ListDataLoader, - readonly timeout: number = 200 - ) { - this._batch = new Batcher(async keys => { - try { - const values = await this.loader(keys) - const map = IM(values) - keys.forEach(key => { - this._results.next([key, map.has(key) ? map.get(key)! : new KeyNotFoundError(key)]) - }) - } catch (e) { - keys.forEach(k => { - this._events.next(createErrorKeyEvent(k, e)) - this._results.next([k, UnknownError.create(e, `Can't load entry with key ${k}`)]) - }) - } - }, timeout) - } + constructor( + private readonly map: Atom>>, + private readonly loader: ListDataLoader, + readonly timeout: number = 200, + ) { + this._batch = new Batcher(async keys => { + try { + const values = await this.loader(keys) + const map = IM(values) + keys.forEach(key => { + this._results.next([key, map.has(key) ? map.get(key)! : new KeyNotFoundError(key)]) + }) + } catch (e) { + keys.forEach(k => { + this._events.next(createErrorKeyEvent(k, e)) + this._results.next([k, UnknownError.create(e, `Can't load entry with key ${k}`)]) + }) + } + }, timeout) + } - single = (key: K): Memo => - this.singles.getOrCreate(key, () => { - this._events.next(createAddKeyEvent(key)) - }) + single = (key: K): Memo => + this.singles.getOrCreate(key, () => { + this._events.next(createAddKeyEvent(key)) + }) - get = (key: K, force?: boolean): Promise => this.single(key).get(force) - set = (key: K, value: V): void => this.single(key).set(value) - getAtom = (key: K): Atom> => this.map.lens(this._lensFactory(key)) + get = (key: K, force?: boolean): Promise => this.single(key).get(force) + set = (key: K, value: V): void => this.single(key).set(value) + getAtom = (key: K): Atom> => this.map.lens(this._lensFactory(key)) - private async load(key: K): Promise { - this._batch.add(key) - const [, v] = await this._results.pipe(first(([k]) => key === k)).toPromise() - if (v instanceof Error) { - this._events.next(createErrorKeyEvent(key, v)) - throw v - } - return v - } + private async load(key: K): Promise { + this._batch.add(key) + const [, v] = await this._results.pipe(first(([k]) => key === k)).toPromise() + if (v instanceof Error) { + this._events.next(createErrorKeyEvent(key, v)) + throw v + } + return v + } } diff --git a/packages/cache/src/memo/index.test.ts b/packages/cache/src/memo/index.test.ts index 53e7962..aa6020b 100644 --- a/packages/cache/src/memo/index.test.ts +++ b/packages/cache/src/memo/index.test.ts @@ -1,155 +1,156 @@ import { Atom } from "@rixio/atom" import { noop } from "rxjs" import { first, map } from "rxjs/operators" -import { CacheState, CacheIdle } from "../domain" +import type { CacheState } from "../domain" +import { CacheIdle } from "../domain" import { MemoImpl } from "./index" describe("MemoImpl", () => { - jest.useFakeTimers() - - test("should load data when subscribed and idle", async () => { - const testset = new TestSet({ timeout: 100 }) - const sub1 = testset.cache.subscribe(noop) - expect(testset.impl.mock.calls).toHaveLength(1) - expect(testset.statuses).toEqual(["idle"]) - await jest.runAllTimersAsync() - expect(testset.statuses).toEqual(["idle", "pending", "fulfilled"]) - sub1.unsubscribe() - await testset.cache.get() - expect(testset.impl.mock.calls).toHaveLength(1) - testset.reset() - }) - - test("should invalidate cache", async () => { - let changed = false - const loader = jest.fn().mockImplementation(() => (changed ? Promise.resolve("hello") : Promise.resolve("world"))) - const testset = new TestSet({ cacheLiveTime: 100, customImpl: loader }) - const data1 = await testset.cache.get() - expect(data1).toEqual("world") - expect(loader.mock.calls).toHaveLength(1) - expect(testset.statuses).toEqual(["idle", "fulfilled"]) - jest.advanceTimersByTime(110) - changed = true - const data2 = await testset.cache.get() - expect(loader.mock.calls).toHaveLength(2) - expect(data2).toEqual("hello") - expect(testset.statuses).toEqual(["idle", "fulfilled", "idle", "fulfilled"]) - testset.reset() - }) - - test("should load immediately", async () => { - const testset = new TestSet() - const sub1 = testset.cache.subscribe(noop) - expect(testset.statuses).toEqual(["idle"]) - await jest.runAllTimersAsync() - expect(testset.statuses).toEqual(["idle", "fulfilled"]) - sub1.unsubscribe() - testset.reset() - }) - - test("should force get", async () => { - const testset = new TestSet() - await Promise.all(new Array(5).fill(0).map(x => testset.cache.get())) - expect(testset.impl.mock.calls).toHaveLength(1) - await testset.cache.get(true) - expect(testset.impl.mock.calls).toHaveLength(2) - testset.reset() - }) - - test("should set idle after clear and not start loading", async () => { - const testset = new TestSet({ timeout: 100 }) - const cacheSub = testset.cache.subscribe(noop) - expect(testset.statuses).toEqual(["idle"]) - await jest.runAllTimersAsync() - expect(testset.statuses).toEqual(["idle", "pending", "fulfilled"]) - cacheSub.unsubscribe() - testset.cache.clear() - expect(testset.statuses).toEqual(["idle", "pending", "fulfilled", "idle"]) - testset.reset() - }) - - test("should start loading right after clear if someone subscribed", async () => { - const testset = new TestSet() - const sub = testset.cache.subscribe(noop) - expect(testset.statuses).toEqual(["idle"]) - await jest.runAllTimersAsync() - expect(testset.statuses).toEqual(["idle", "fulfilled"]) - testset.cache.clear() - expect(testset.statuses).toEqual(["idle", "fulfilled", "idle"]) - await jest.runAllTimersAsync() - expect(testset.statuses).toEqual(["idle", "fulfilled", "idle", "fulfilled"]) - expect(testset.impl.mock.calls).toHaveLength(2) - sub.unsubscribe() - testset.reset() - }) - - test("set should work", async () => { - const testset = new TestSet() - await testset.cache.get() - expect(testset.statuses).toEqual(["idle", "fulfilled"]) - testset.cache.set("other") - expect(testset.statuses).toEqual(["idle", "fulfilled", "fulfilled"]) - await testset.cache.get() - expect(testset.statuses).toEqual(["idle", "fulfilled", "fulfilled"]) - }) - - test("rxjs operators should work", async () => { - const testset = new TestSet() - const value1 = await testset.cache - .pipe( - map(() => "mapped"), - first() - ) - .toPromise() - expect(value1).toBe("mapped") - }) - - test("should start fetching after subscribe on rejected Memo", async () => { - const loader = jest - .fn() - .mockImplementation(() => - loader.mock.calls.length <= 1 ? Promise.reject("rejected") : Promise.resolve("resolved") - ) - const testset = new TestSet({ customImpl: loader }) - expect(loader.mock.calls.length).toEqual(0) - const emitted: string[] = [] - - // First call - const sub1 = testset.cache.subscribe(x => emitted.push(x), noop) - expect(loader.mock.calls.length).toEqual(1) - await jest.runAllTimersAsync() - expect(testset.statuses).toEqual(["idle", "rejected"]) - expect(emitted).toEqual([]) - sub1.unsubscribe() - - // Second call - const sub2 = testset.cache.subscribe(x => emitted.push(x)) - await jest.runAllTimersAsync() - expect(loader.mock.calls.length).toEqual(2) - expect(testset.statuses).toEqual(["idle", "rejected", "idle", "fulfilled"]) - expect(emitted).toStrictEqual(["resolved"]) - sub2.unsubscribe() - }) + jest.useFakeTimers() + + test("should load data when subscribed and idle", async () => { + const testset = new TestSet({ timeout: 100 }) + const sub1 = testset.cache.subscribe(noop) + expect(testset.impl.mock.calls).toHaveLength(1) + expect(testset.statuses).toEqual(["idle"]) + await jest.runAllTimersAsync() + expect(testset.statuses).toEqual(["idle", "pending", "fulfilled"]) + sub1.unsubscribe() + await testset.cache.get() + expect(testset.impl.mock.calls).toHaveLength(1) + testset.reset() + }) + + test("should invalidate cache", async () => { + let changed = false + const loader = jest.fn().mockImplementation(() => (changed ? Promise.resolve("hello") : Promise.resolve("world"))) + const testset = new TestSet({ cacheLiveTime: 100, customImpl: loader }) + const data1 = await testset.cache.get() + expect(data1).toEqual("world") + expect(loader.mock.calls).toHaveLength(1) + expect(testset.statuses).toEqual(["idle", "fulfilled"]) + jest.advanceTimersByTime(110) + changed = true + const data2 = await testset.cache.get() + expect(loader.mock.calls).toHaveLength(2) + expect(data2).toEqual("hello") + expect(testset.statuses).toEqual(["idle", "fulfilled", "idle", "fulfilled"]) + testset.reset() + }) + + test("should load immediately", async () => { + const testset = new TestSet() + const sub1 = testset.cache.subscribe(noop) + expect(testset.statuses).toEqual(["idle"]) + await jest.runAllTimersAsync() + expect(testset.statuses).toEqual(["idle", "fulfilled"]) + sub1.unsubscribe() + testset.reset() + }) + + test("should force get", async () => { + const testset = new TestSet() + await Promise.all(new Array(5).fill(0).map(() => testset.cache.get())) + expect(testset.impl.mock.calls).toHaveLength(1) + await testset.cache.get(true) + expect(testset.impl.mock.calls).toHaveLength(2) + testset.reset() + }) + + test("should set idle after clear and not start loading", async () => { + const testset = new TestSet({ timeout: 100 }) + const cacheSub = testset.cache.subscribe(noop) + expect(testset.statuses).toEqual(["idle"]) + await jest.runAllTimersAsync() + expect(testset.statuses).toEqual(["idle", "pending", "fulfilled"]) + cacheSub.unsubscribe() + testset.cache.clear() + expect(testset.statuses).toEqual(["idle", "pending", "fulfilled", "idle"]) + testset.reset() + }) + + test("should start loading right after clear if someone subscribed", async () => { + const testset = new TestSet() + const sub = testset.cache.subscribe(noop) + expect(testset.statuses).toEqual(["idle"]) + await jest.runAllTimersAsync() + expect(testset.statuses).toEqual(["idle", "fulfilled"]) + testset.cache.clear() + expect(testset.statuses).toEqual(["idle", "fulfilled", "idle"]) + await jest.runAllTimersAsync() + expect(testset.statuses).toEqual(["idle", "fulfilled", "idle", "fulfilled"]) + expect(testset.impl.mock.calls).toHaveLength(2) + sub.unsubscribe() + testset.reset() + }) + + test("set should work", async () => { + const testset = new TestSet() + await testset.cache.get() + expect(testset.statuses).toEqual(["idle", "fulfilled"]) + testset.cache.set("other") + expect(testset.statuses).toEqual(["idle", "fulfilled", "fulfilled"]) + await testset.cache.get() + expect(testset.statuses).toEqual(["idle", "fulfilled", "fulfilled"]) + }) + + test("rxjs operators should work", async () => { + const testset = new TestSet() + const value1 = await testset.cache + .pipe( + map(() => "mapped"), + first(), + ) + .toPromise() + expect(value1).toBe("mapped") + }) + + test("should start fetching after subscribe on rejected Memo", async () => { + const loader = jest + .fn() + .mockImplementation(() => + loader.mock.calls.length <= 1 ? Promise.reject("rejected") : Promise.resolve("resolved"), + ) + const testset = new TestSet({ customImpl: loader }) + expect(loader.mock.calls.length).toEqual(0) + const emitted: string[] = [] + + // First call + const sub1 = testset.cache.subscribe(x => emitted.push(x), noop) + expect(loader.mock.calls.length).toEqual(1) + await jest.runAllTimersAsync() + expect(testset.statuses).toEqual(["idle", "rejected"]) + expect(emitted).toEqual([]) + sub1.unsubscribe() + + // Second call + const sub2 = testset.cache.subscribe(x => emitted.push(x)) + await jest.runAllTimersAsync() + expect(loader.mock.calls.length).toEqual(2) + expect(testset.statuses).toEqual(["idle", "rejected", "idle", "fulfilled"]) + expect(emitted).toStrictEqual(["resolved"]) + sub2.unsubscribe() + }) }) type TestSetConfig = { - customImpl?: jest.Mock - timeout?: number - cacheLiveTime?: number + customImpl?: jest.Mock + timeout?: number + cacheLiveTime?: number } class TestSet { - readonly impl = jest.fn().mockImplementation(() => { - const timeout = this.config.timeout - if (typeof timeout !== "number") return Promise.resolve("loaded") - return new Promise(r => setTimeout(() => r("loaded"), timeout)) - }) - - readonly atom = Atom.create(CacheIdle.create()) - readonly cache = new MemoImpl(this.atom, this.config.customImpl || this.impl, this.config.cacheLiveTime) - readonly statuses: CacheState["status"][] = [] - private readonly sub = this.atom.subscribe(x => this.statuses.push(x.status)) - - constructor(private readonly config: TestSetConfig = {}) {} - reset = () => this.sub.unsubscribe() + readonly impl = jest.fn().mockImplementation(() => { + const timeout = this.config.timeout + if (typeof timeout !== "number") return Promise.resolve("loaded") + return new Promise(r => setTimeout(() => r("loaded"), timeout)) + }) + + readonly atom = Atom.create(CacheIdle.create()) + readonly cache = new MemoImpl(this.atom, this.config.customImpl || this.impl, this.config.cacheLiveTime) + readonly statuses: CacheState["status"][] = [] + private readonly sub = this.atom.subscribe(x => this.statuses.push(x.status)) + + constructor(private readonly config: TestSetConfig = {}) {} + reset = () => this.sub.unsubscribe() } diff --git a/packages/cache/src/memo/index.ts b/packages/cache/src/memo/index.ts index 8703175..acf24c3 100644 --- a/packages/cache/src/memo/index.ts +++ b/packages/cache/src/memo/index.ts @@ -1,121 +1,125 @@ import type { Atom } from "@rixio/atom" -import { Observable, ReplaySubject, Subscription } from "rxjs" +import type { Subscription } from "rxjs" +import { Observable, ReplaySubject } from "rxjs" import { first } from "rxjs/operators" -import { CacheState, CacheFulfilled, CacheIdle } from "../domain" +import type { CacheState } from "../domain" +import { CacheFulfilled, CacheIdle } from "../domain" import { runPromiseWithCache } from "../utils" export interface Memo extends Observable { - get: (force?: boolean) => Promise - set: (value: T) => void - modifyIfFulfilled: (updateFn: (currentValue: T) => T) => void - clear: () => void - atom: Atom> + get: (force?: boolean) => Promise + set: (value: T) => void + modifyIfFulfilled: (updateFn: (currentValue: T) => T) => void + clear: () => void + atom: Atom> } export class MemoImpl extends Observable implements Memo { - private _sharedBuffer$: ReplaySubject | undefined = undefined - private _subscription: Subscription | undefined = undefined - private _refCount = 0 + private _sharedBuffer$: ReplaySubject | undefined = undefined + private _subscription: Subscription | undefined = undefined + private _refCount = 0 - constructor( - public readonly atom: Atom>, - private readonly _loader: () => Promise, - private readonly cacheLiveTimeMs: number = 1000 * 60 * 10 // 10 minutes cache - ) { - super(subscriber => { - const initial = atom.get() + constructor( + public readonly atom: Atom>, + private readonly _loader: () => Promise, + private readonly cacheLiveTimeMs: number = 1000 * 60 * 10, // 10 minutes cache + ) { + super(subscriber => { + const initial = atom.get() - // When user subscribes and it's in failed state then we have to reset it - if (initial.status === "rejected") this.clear() + // When user subscribes and it's in failed state then we have to reset it + if (initial.status === "rejected") this.clear() - // When user subscribes and it's in stalled state then we have to reset it - if (initial.status === "fulfilled" && this.shouldRefresh(initial)) this.clear() + // When user subscribes and it's in stalled state then we have to reset it + if (initial.status === "fulfilled" && this.shouldRefresh(initial)) this.clear() - if (!this._sharedBuffer$) { - this._sharedBuffer$ = new ReplaySubject(1) - this._subscription = atom.subscribe({ - next: x => { - switch (x.status) { - case "idle": - runPromiseWithCache(this._loader(), this.atom).then() - break - case "rejected": - this._sharedBuffer$?.error(x.error) - break - case "fulfilled": - this._sharedBuffer$?.next(x.value) - break - } - }, - }) - } + if (!this._sharedBuffer$) { + this._sharedBuffer$ = new ReplaySubject(1) + this._subscription = atom.subscribe({ + next: x => { + switch (x.status) { + case "idle": + runPromiseWithCache(this._loader(), this.atom).then() + break + case "rejected": + this._sharedBuffer$?.error(x.error) + break + case "fulfilled": + this._sharedBuffer$?.next(x.value) + break + default: + break + } + }, + }) + } - this._refCount = this._refCount + 1 - const localSub = this._sharedBuffer$.subscribe({ - error: err => subscriber.error(err), - next: value => subscriber.next(value), - }) + this._refCount = this._refCount + 1 + const localSub = this._sharedBuffer$.subscribe({ + error: err => subscriber.error(err), + next: value => subscriber.next(value), + }) - subscriber.add(() => { - localSub.unsubscribe() - this._refCount = this._refCount - 1 - if (this._refCount === 0 && this._subscription) { - this._subscription.unsubscribe() - this._subscription = undefined - this._sharedBuffer$ = undefined - } - }) + subscriber.add(() => { + localSub.unsubscribe() + this._refCount = this._refCount - 1 + if (this._refCount === 0 && this._subscription) { + this._subscription.unsubscribe() + this._subscription = undefined + this._sharedBuffer$ = undefined + } + }) - return subscriber - }) - } + return subscriber + }) + } - get = async (force = false): Promise => { - if (force) this.clear() + get = async (force = false): Promise => { + if (force) this.clear() - const [result] = await Promise.all([ - // This will emit subscribe - this.pipe(first()).toPromise(), - // It may be previous value so just to be sure that it's - // fresh value after reset we need to wait for atom state change - new Promise((resolve, reject) => { - this.atom - .pipe( - // It will kill subscription right after first result - first(x => x.status === "fulfilled" || x.status === "rejected") - ) - .subscribe(value => { - switch (value.status) { - case "fulfilled": - return resolve(value.value) - case "rejected": - return reject(value.error) - default: - throw new Error("Never happen") - } - }) - }), - ]) - return result - } + const [result] = await Promise.all([ + // This will emit subscribe + this.pipe(first()).toPromise(), + // It may be previous value so just to be sure that it's + // fresh value after reset we need to wait for atom state change + new Promise((resolve, reject) => { + this.atom + .pipe( + // It will kill subscription right after first result + first(x => x.status === "fulfilled" || x.status === "rejected"), + ) + .subscribe(value => { + switch (value.status) { + case "fulfilled": + return resolve(value.value) + case "rejected": + return reject(value.error) + default: + throw new Error("Never happen") + } + }) + }), + ]) + return result + } - set = (value: T): void => this.atom.set(CacheFulfilled.create(value)) + set = (value: T): void => this.atom.set(CacheFulfilled.create(value)) - modifyIfFulfilled = (fn: (current: T) => T): void => - this.atom.modify(s => { - if (s.status === "fulfilled") { - return { - ...s, - value: fn(s.value), - } - } - return s - }) + modifyIfFulfilled = (fn: (current: T) => T): void => + this.atom.modify(s => { + if (s.status === "fulfilled") { + return { + ...s, + value: fn(s.value), + } + } + return s + }) - clear = (): void => this.atom.set(CacheIdle.create()) + clear = (): void => this.atom.set(CacheIdle.create()) - private shouldRefresh(value: CacheFulfilled) { - // Refresh cache in case when live time is exceeded - return value.timestamp + this.cacheLiveTimeMs < Date.now() - } + private shouldRefresh(value: CacheFulfilled) { + // Refresh cache in case when live time is exceeded + return value.timestamp + this.cacheLiveTimeMs < Date.now() + } } diff --git a/packages/cache/src/utils/batcher.ts b/packages/cache/src/utils/batcher.ts index 0c7e816..ca924e7 100644 --- a/packages/cache/src/utils/batcher.ts +++ b/packages/cache/src/utils/batcher.ts @@ -1,17 +1,17 @@ export class Batcher { - private timer: NodeJS.Timeout | null = null - private batch: K[] = [] + private timer: ReturnType | null = null + private batch: K[] = [] - constructor(private readonly onDrop: (items: K[]) => void, private readonly timeout: number) {} + constructor(private readonly onDrop: (items: K[]) => void, private readonly timeout: number) {} - add = (item: K) => { - if (this.timer === null) { - this.timer = setTimeout(() => { - this.timer = null - this.onDrop(this.batch) - this.batch = [] - }, this.timeout) - } - this.batch.push(item) - } + add = (item: K) => { + if (this.timer === null) { + this.timer = setTimeout(() => { + this.timer = null + this.onDrop(this.batch) + this.batch = [] + }, this.timeout) + } + this.batch.push(item) + } } diff --git a/packages/cache/src/utils/errors.ts b/packages/cache/src/utils/errors.ts index 3e4906e..ce96269 100644 --- a/packages/cache/src/utils/errors.ts +++ b/packages/cache/src/utils/errors.ts @@ -1,14 +1,14 @@ export class KeyNotFoundError extends Error { - readonly name = "KeyNotFoundError" - constructor(key: T) { - super(`Entity with key "${key}" not found`) - } + readonly name = "KeyNotFoundError" + constructor(key: T) { + super(`Entity with key "${key}" not found`) + } } export class UnknownError extends Error { - readonly name = "UnknownError" - static create(originalError: unknown, fallbackMessage: string) { - if (originalError instanceof Error) return originalError - return new UnknownError(fallbackMessage) - } + readonly name = "UnknownError" + static create(originalError: unknown, fallbackMessage: string) { + if (originalError instanceof Error) return originalError + return new UnknownError(fallbackMessage) + } } diff --git a/packages/cache/src/utils/run-promise-with-cache/index.test.ts b/packages/cache/src/utils/run-promise-with-cache/index.test.ts index 045d5ec..c87015e 100644 --- a/packages/cache/src/utils/run-promise-with-cache/index.test.ts +++ b/packages/cache/src/utils/run-promise-with-cache/index.test.ts @@ -1,40 +1,41 @@ import { Atom } from "@rixio/atom" -import { CacheIdle, CacheState } from "../../domain" +import type { CacheState } from "../../domain" +import { CacheIdle } from "../../domain" import { runPromiseWithCache } from "." describe("runPromiseWithCache", () => { - jest.useFakeTimers() + jest.useFakeTimers() - it("should run promise with cache", async () => { - const loader = () => new Promise(r => setTimeout(r, 16)) - const atom$ = Atom.create(CacheIdle.create()) - const values: CacheState["status"][] = [] - const sub = atom$.subscribe(x => values.push(x.status)) - runPromiseWithCache(loader(), atom$) - await jest.runAllTimersAsync() - expect(values).toEqual(["idle", "pending", "fulfilled"]) - sub.unsubscribe() - }) + it("should run promise with cache", async () => { + const loader = () => new Promise(r => setTimeout(r, 16)) + const atom$ = Atom.create(CacheIdle.create()) + const values: CacheState["status"][] = [] + const sub = atom$.subscribe(x => values.push(x.status)) + runPromiseWithCache(loader(), atom$) + await jest.runAllTimersAsync() + expect(values).toEqual(["idle", "pending", "fulfilled"]) + sub.unsubscribe() + }) - it("should set rejected cache", async () => { - const loader = () => new Promise((_, r) => setTimeout(r, 16)) - const atom$ = Atom.create(CacheIdle.create()) - const values: CacheState["status"][] = [] - const sub = atom$.subscribe(x => values.push(x.status)) - runPromiseWithCache(loader(), atom$) - await jest.runAllTimersAsync() - expect(values).toEqual(["idle", "pending", "rejected"]) - sub.unsubscribe() - }) + it("should set rejected cache", async () => { + const loader = () => new Promise((_, r) => setTimeout(r, 16)) + const atom$ = Atom.create(CacheIdle.create()) + const values: CacheState["status"][] = [] + const sub = atom$.subscribe(x => values.push(x.status)) + runPromiseWithCache(loader(), atom$) + await jest.runAllTimersAsync() + expect(values).toEqual(["idle", "pending", "rejected"]) + sub.unsubscribe() + }) - it("shouldn't set pending when promise already resolved", async () => { - const loader = () => Promise.resolve() - const atom$ = Atom.create(CacheIdle.create()) - const values: CacheState["status"][] = [] - const sub = atom$.subscribe(x => values.push(x.status)) - runPromiseWithCache(loader(), atom$) - await jest.runAllTimersAsync() - expect(values).toEqual(["idle", "fulfilled"]) - sub.unsubscribe() - }) + it("shouldn't set pending when promise already resolved", async () => { + const loader = () => Promise.resolve() + const atom$ = Atom.create(CacheIdle.create()) + const values: CacheState["status"][] = [] + const sub = atom$.subscribe(x => values.push(x.status)) + runPromiseWithCache(loader(), atom$) + await jest.runAllTimersAsync() + expect(values).toEqual(["idle", "fulfilled"]) + sub.unsubscribe() + }) }) diff --git a/packages/cache/src/utils/run-promise-with-cache/index.ts b/packages/cache/src/utils/run-promise-with-cache/index.ts index 4aa1919..4f86d71 100644 --- a/packages/cache/src/utils/run-promise-with-cache/index.ts +++ b/packages/cache/src/utils/run-promise-with-cache/index.ts @@ -1,17 +1,18 @@ -import { Atom } from "@rixio/atom" -import { CacheFulfilled, CachePending, CacheRejected, CacheState } from "../../domain" +import type { Atom } from "@rixio/atom" +import type { CacheState } from "../../domain" +import { CacheFulfilled, CachePending, CacheRejected } from "../../domain" export async function runPromiseWithCache( - promise: Promise, - atom: Atom> + promise: Promise, + atom: Atom>, ): Promise { - const timer = setTimeout(() => atom.set(CachePending.create()), 0) - try { - const result = await promise - return atom.set(CacheFulfilled.create(result)) - } catch (error) { - return atom.set(CacheRejected.create(error)) - } finally { - clearTimeout(timer) - } + const timer = setTimeout(() => atom.set(CachePending.create()), 0) + try { + const result = await promise + return atom.set(CacheFulfilled.create(result)) + } catch (error) { + return atom.set(CacheRejected.create(error)) + } finally { + clearTimeout(timer) + } } diff --git a/packages/cache/src/utils/save/index.test.ts b/packages/cache/src/utils/save/index.test.ts index 4d232c9..4870657 100644 --- a/packages/cache/src/utils/save/index.test.ts +++ b/packages/cache/src/utils/save/index.test.ts @@ -1,48 +1,49 @@ import { Atom } from "@rixio/atom" -import { CacheIdle, CacheState } from "../../domain" +import type { CacheState } from "../../domain" +import { CacheIdle } from "../../domain" import { save } from "." describe("save", () => { - it("should save value", async () => { - const result: CacheState["status"][] = [] - const data$ = Atom.create(CacheIdle.create()) - const sub = data$.subscribe(x => result.push(x.status)) - expect(result).toEqual(["idle"]) - const promise = save(new Promise(r => setTimeout(() => r(10), 16)), data$) - await expect(promise).resolves.toEqual(10) - expect(result.length).toEqual(3) - expect(result).toEqual(["idle", "pending", "fulfilled"]) - sub.unsubscribe() - }) + it("should save value", async () => { + const result: CacheState["status"][] = [] + const data$ = Atom.create(CacheIdle.create()) + const sub = data$.subscribe(x => result.push(x.status)) + expect(result).toEqual(["idle"]) + const promise = save(new Promise(r => setTimeout(() => r(10), 16)), data$) + await expect(promise).resolves.toEqual(10) + expect(result.length).toEqual(3) + expect(result).toEqual(["idle", "pending", "fulfilled"]) + sub.unsubscribe() + }) - it("should throw error", async () => { - const result: CacheState["status"][] = [] - const data$ = Atom.create(CacheIdle.create()) - const sub = data$.subscribe(x => result.push(x.status)) - expect(result).toEqual(["idle"]) - const promise = save(new Promise((_, r) => setTimeout(() => r(10), 16)), data$) - await expect(promise).rejects.toEqual(10) - expect(result).toEqual(["idle", "pending", "rejected"]) - sub.unsubscribe() - }) + it("should throw error", async () => { + const result: CacheState["status"][] = [] + const data$ = Atom.create(CacheIdle.create()) + const sub = data$.subscribe(x => result.push(x.status)) + expect(result).toEqual(["idle"]) + const promise = save(new Promise((_, r) => setTimeout(() => r(10), 16)), data$) + await expect(promise).rejects.toEqual(10) + expect(result).toEqual(["idle", "pending", "rejected"]) + sub.unsubscribe() + }) - it("should not put pending state when promise immediately resolved", async () => { - const result: CacheState["status"][] = [] - const data$ = Atom.create(CacheIdle.create()) - const sub = data$.subscribe(x => result.push(x.status)) - expect(result).toEqual(["idle"]) - const promise = save(Promise.resolve(10), data$) - await expect(promise).resolves.toEqual(10) - expect(result).toEqual(["idle", "fulfilled"]) - sub.unsubscribe() - }) + it("should not put pending state when promise immediately resolved", async () => { + const result: CacheState["status"][] = [] + const data$ = Atom.create(CacheIdle.create()) + const sub = data$.subscribe(x => result.push(x.status)) + expect(result).toEqual(["idle"]) + const promise = save(Promise.resolve(10), data$) + await expect(promise).resolves.toEqual(10) + expect(result).toEqual(["idle", "fulfilled"]) + sub.unsubscribe() + }) - it("result from save should be the same as promise's result", async () => { - const data$ = Atom.create(CacheIdle.create()) - const originalPromise = new Promise(r => setTimeout(() => r(10), 16)) - const promise = save(originalPromise, data$) - const originalResult = await originalPromise - const promiseResult = await promise - expect(originalResult).toEqual(promiseResult) - }) + it("result from save should be the same as promise's result", async () => { + const data$ = Atom.create(CacheIdle.create()) + const originalPromise = new Promise(r => setTimeout(() => r(10), 16)) + const promise = save(originalPromise, data$) + const originalResult = await originalPromise + const promiseResult = await promise + expect(originalResult).toEqual(promiseResult) + }) }) diff --git a/packages/cache/src/utils/save/index.ts b/packages/cache/src/utils/save/index.ts index a91edd3..d766cbe 100644 --- a/packages/cache/src/utils/save/index.ts +++ b/packages/cache/src/utils/save/index.ts @@ -1,8 +1,8 @@ -import { Atom } from "@rixio/atom" -import { CacheState } from "../../domain" +import type { Atom } from "@rixio/atom" +import type { CacheState } from "../../domain" import { runPromiseWithCache } from "../run-promise-with-cache" export async function save(promise: Promise, atom: Atom>): Promise { - await runPromiseWithCache(promise, atom) - return await promise + await runPromiseWithCache(promise, atom) + return await promise } diff --git a/packages/cache/src/utils/to-cache.ts b/packages/cache/src/utils/to-cache.ts index 9de7bc3..87f8acf 100644 --- a/packages/cache/src/utils/to-cache.ts +++ b/packages/cache/src/utils/to-cache.ts @@ -1,25 +1,31 @@ -import { WrappedFulfilled, WrappedRejected, WrappedPending, Wrapped } from "@rixio/wrapped" -import { CacheFulfilled, CachePending, CacheRejected, CacheState } from "../domain" +import type { Wrapped } from "@rixio/wrapped" +import { WrappedFulfilled, WrappedRejected, WrappedPending } from "@rixio/wrapped" +import type { CacheState } from "../domain" +import { CacheFulfilled, CachePending, CacheRejected } from "../domain" export function toCache(wrapped: Wrapped): CacheState { - switch (wrapped.status) { - case "fulfilled": - return CacheFulfilled.create(wrapped.value) - case "pending": - return CachePending.create() - case "rejected": - return CacheRejected.create(wrapped.error) - } + switch (wrapped.status) { + case "fulfilled": + return CacheFulfilled.create(wrapped.value) + case "pending": + return CachePending.create() + case "rejected": + return CacheRejected.create(wrapped.error) + default: + throw new Error("Unknown Wrapped state") + } } export function fromCache(cache: CacheState): Wrapped { - switch (cache.status) { - case "fulfilled": - return WrappedFulfilled.create(cache.value) - case "idle": - case "pending": - return WrappedPending.create() - case "rejected": - return WrappedRejected.create(cache.error) - } + switch (cache.status) { + case "fulfilled": + return WrappedFulfilled.create(cache.value) + case "idle": + case "pending": + return WrappedPending.create() + case "rejected": + return WrappedRejected.create(cache.error) + default: + throw new Error("Unknown Cache state") + } } diff --git a/packages/cache/src/utils/to-list-loader/index.test.ts b/packages/cache/src/utils/to-list-loader/index.test.ts index 988445d..dac5268 100644 --- a/packages/cache/src/utils/to-list-loader/index.test.ts +++ b/packages/cache/src/utils/to-list-loader/index.test.ts @@ -1,28 +1,28 @@ import { toListLoader } from "./index" describe("toListLoader", () => { - it("should resolve all values", async () => { - const loader = toListLoader(x => Promise.resolve(x + "1"), undefined) - const result = await loader(["hello", "world"]) - expect(result).toStrictEqual([ - ["hello", "hello1"], - ["world", "world1"], - ]) - }) + it("should resolve all values", async () => { + const loader = toListLoader(x => Promise.resolve(x + "1"), undefined) + const result = await loader(["hello", "world"]) + expect(result).toStrictEqual([ + ["hello", "hello1"], + ["world", "world1"], + ]) + }) - it("should apply fallback when error", async () => { - const err = new Error("My error") - const loader = toListLoader( - x => { - if (x === "hello") return Promise.reject(err) - return Promise.resolve(x + "1") - }, - () => "fallback" - ) - const result = await loader(["hello", "world"]) - expect(result).toStrictEqual([ - ["hello", "fallback"], - ["world", "world1"], - ]) - }) + it("should apply fallback when error", async () => { + const err = new Error("My error") + const loader = toListLoader( + x => { + if (x === "hello") return Promise.reject(err) + return Promise.resolve(x + "1") + }, + () => "fallback", + ) + const result = await loader(["hello", "world"]) + expect(result).toStrictEqual([ + ["hello", "fallback"], + ["world", "world1"], + ]) + }) }) diff --git a/packages/cache/src/utils/to-list-loader/index.ts b/packages/cache/src/utils/to-list-loader/index.ts index 77ec217..56ce13b 100644 --- a/packages/cache/src/utils/to-list-loader/index.ts +++ b/packages/cache/src/utils/to-list-loader/index.ts @@ -1,28 +1,28 @@ -import { DataLoader, ListDataLoader } from "../../domain" +import type { DataLoader, ListDataLoader } from "../../domain" export function toListLoader( - loader: DataLoader, - createFallback?: (key: K, error: unknown) => V + loader: DataLoader, + createFallback?: (key: K, error: unknown) => V, ): ListDataLoader { - return async keys => { - const map = new Map() - await Promise.all( - keys.map(key => - loader(key) - .then(v => map.set(key, [key, v])) - .catch(error => { - if (createFallback) { - map.set(key, [key, createFallback(key, error)]) - } - }) - ) - ) - const results: [K, V][] = [] - keys.forEach(key => { - if (map.has(key)) { - results.push(map.get(key)!) - } - }) - return results - } + return async keys => { + const map = new Map() + await Promise.all( + keys.map(key => + loader(key) + .then(v => map.set(key, [key, v])) + .catch(error => { + if (createFallback) { + map.set(key, [key, createFallback(key, error)]) + } + }), + ), + ) + const results: [K, V][] = [] + keys.forEach(key => { + if (map.has(key)) { + results.push(map.get(key)!) + } + }) + return results + } } diff --git a/packages/form-store/src/domain.ts b/packages/form-store/src/domain.ts index 8b1d328..94b5f83 100644 --- a/packages/form-store/src/domain.ts +++ b/packages/form-store/src/domain.ts @@ -1,25 +1,25 @@ -import { Observable } from "rxjs" +import type { Observable } from "rxjs" export interface ValidationResultValidating { - status: "validating" + status: "validating" } export interface ValidationResultSuccess { - status: "success" + status: "success" } export interface ValidationResultError { - status: "error" - error: string - children: { - [P in keyof T]+?: ValidationResult - } + status: "error" + error: string + children: { + [P in keyof T]+?: ValidationResult + } } export type ValidationResult = ValidationResultSuccess | ValidationResultValidating | ValidationResultError export type Validate = ( - value: T + value: T, ) => ValidationResult | PromiseLike> | Observable> export type ValidationStatus = ValidationResult["status"] diff --git a/packages/form-store/src/index.spec.ts b/packages/form-store/src/index.spec.ts index b37a4e4..e6e4cc2 100644 --- a/packages/form-store/src/index.spec.ts +++ b/packages/form-store/src/index.spec.ts @@ -2,196 +2,197 @@ import Joi from "joi" import { filter, first, map, reduce, takeWhile } from "rxjs/operators" import { Atom } from "@rixio/atom" import { Lens } from "@rixio/lens" -import { Observable, timer } from "rxjs" -import { ValidationResult, ValidationResultError, ValidationResultSuccess, ValidationStatus } from "./domain" +import type { Observable } from "rxjs" +import { timer } from "rxjs" +import type { ValidationResult, ValidationResultError, ValidationResultSuccess, ValidationStatus } from "./domain" import { validateJoi } from "./utils/validate-joi" import { FormStore } from "./index" interface SignUpFormSchema { - firstName: string - lastName: string + firstName: string + lastName: string } class SignUpForm { - readonly schema = Joi.object().keys({ - firstName: Joi.string().required().min(2).max(100), - lastName: Joi.string().required(), - }) + readonly schema = Joi.object().keys({ + firstName: Joi.string().required().min(2).max(100), + lastName: Joi.string().required(), + }) - readonly validation = validateJoi(this.schema) - readonly asyncValidation = (value: SignUpFormSchema) => - new Promise>(r => setTimeout(() => r(this.validation(value)), 100)) + readonly validation = validateJoi(this.schema) + readonly asyncValidation = (value: SignUpFormSchema) => + new Promise>(r => setTimeout(() => r(this.validation(value)), 100)) - getAtom = (value: SignUpFormSchema = createDefaultSignUpForm()) => Atom.create(value) + getAtom = (value: SignUpFormSchema = createDefaultSignUpForm()) => Atom.create(value) } class KeyedForm { - constructor(private readonly childSchema: Joi.ObjectSchema, private readonly getDefault: (key: string) => T) {} - - private readonly schema = Joi.object({}).pattern(Joi.string(), this.childSchema) - readonly validation = validateJoi>(this.schema) - - getLensByKey = (key: string) => - Lens.create, T>( - s => s[key] || this.getDefault(key), - (s, xs) => { - xs[key] = s - return xs - } - ) + constructor(private readonly childSchema: Joi.ObjectSchema, private readonly getDefault: (key: string) => T) {} + + private readonly schema = Joi.object({}).pattern(Joi.string(), this.childSchema) + readonly validation = validateJoi>(this.schema) + + getLensByKey = (key: string) => + Lens.create, T>( + s => s[key] || this.getDefault(key), + (s, xs) => { + xs[key] = s + return xs + }, + ) } function collectTill(vr: Observable>, status: ValidationStatus) { - return vr - .pipe( - takeWhile(x => x.status !== status, true), - reduce, ValidationResult[]>((xs, x) => [...xs, x], []), - first() - ) - .toPromise() + return vr + .pipe( + takeWhile(x => x.status !== status, true), + reduce, ValidationResult[]>((xs, x) => [...xs, x], []), + first(), + ) + .toPromise() } describe("FormStore", () => { - const signUpForm = new SignUpForm() - - it("create FormStore and perform validation", async () => { - expect.assertions(2) - const atom = signUpForm.getAtom() - const form = FormStore.create(atom, signUpForm.validation) - const firstNameValidation = form.bind("firstName").validationResult - - const withError = await firstNameValidation - .pipe( - filter(x => x.status === "error"), - map(x => x as ValidationResultError), - first() - ) - .toPromise() - expect(withError.error).toBe('"firstName" is not allowed to be empty') - - atom.modify(it => ({ - ...it, - firstName: "First Name", - })) - const success = await firstNameValidation - .pipe( - filter(x => x.status === "success"), - map(x => x as ValidationResultSuccess), - first() - ) - .toPromise() - expect(success.status).toBe("success") - }) - - it("should display validating status while perform async validation", async () => { - expect.assertions(3) - const atom = signUpForm.getAtom() - const form = FormStore.create(atom, signUpForm.asyncValidation) - - const tillError = await collectTill(form.validationResult, "error") - expect(tillError.length).toBe(2) - expect(tillError[0].status).toBe("validating") - expect(tillError[1].status).toBe("error") - }) - - it("should display validating status while perform async validation (with Observable)", async () => { - const atom = signUpForm.getAtom() - const validate = (form: SignUpFormSchema): Observable> => { - return timer(0, 1000).pipe( - map(x => { - if (x > 0) { - return { - status: "error", - error: "some value", - children: {}, - } - } else { - return { - status: "success", - } - } - }) - ) - } - - const form = FormStore.create(atom, validate) - await collectTill(form.validationResult, "success") - const tillError = await collectTill(form.validationResult, "error") - expect(tillError.length).toBe(2) - expect(tillError[0].status).toBe("success") - expect(tillError[1].status).toBe("error") - }) - - it("if there is error, no validation status should be emitted", async () => { - expect.assertions(7) - const atom = signUpForm.getAtom() - const form = FormStore.create(atom, signUpForm.asyncValidation) - - const results1 = await collectTill(form.validationResult, "error") - expect(results1.length).toBe(2) - expect(results1[0].status).toBe("validating") - expect(results1[1].status).toBe("error") - - atom.lens("firstName").set("First name") - atom.lens("lastName").set("Last name") - const results2 = await collectTill(form.validationResult, "success") - expect(results2.length).toBe(3) - expect(results2[0].status).toBe("error") - expect(results2[1].status).toBe("error") - expect(results2[2].status).toBe("success") - }) - - it("validating should be emitted after success", async () => { - expect.assertions(6) - const atom = signUpForm.getAtom(createDefaultSignUpForm("First name", "Last name")) - const form = FormStore.create(atom, signUpForm.asyncValidation) - const results1 = await collectTill(form.validationResult, "success") - expect(results1.length).toBe(2) - expect(results1[0].status).toBe("validating") - expect(results1[1].status).toBe("success") - atom.lens("firstName").set("") - atom.lens("lastName").set("") - - const results2 = await collectTill(form.validationResult, "error") - expect(results2.length).toBe(2) - expect(results2[0].status).toBe("validating") - expect(results2[1].status).toBe("error") - }) - - it("should work with custom lens", async () => { - expect.assertions(7) - const keyed = new KeyedForm(signUpForm.schema, () => createDefaultSignUpForm()) - const atom = Atom.create({}) - const form = FormStore.create(atom, keyed.validation) - const binded = form.bind("unknown-key", keyed.getLensByKey) - - const firstName$ = binded.value.lens("firstName") - const lastName$ = binded.value.lens("lastName") - expect(firstName$.get()).toEqual("") - expect(lastName$.get()).toEqual("") - - firstName$.set("John") - lastName$.set("Doe") - - expect(firstName$.get()).toEqual("John") - expect(lastName$.get()).toEqual("Doe") - - expect(atom.get()).toEqual({ - "unknown-key": createDefaultSignUpForm("John", "Doe"), - }) - - binded.bind("firstName").value.set("John") - binded.bind("lastName").value.set("Doe") - - const results2 = await collectTill(form.validationResult, "success") - expect(results2.length).toBe(1) - expect(results2[0].status).toBe("success") - }) + const signUpForm = new SignUpForm() + + it("create FormStore and perform validation", async () => { + expect.assertions(2) + const atom = signUpForm.getAtom() + const form = FormStore.create(atom, signUpForm.validation) + const firstNameValidation = form.bind("firstName").validationResult + + const withError = await firstNameValidation + .pipe( + filter(x => x.status === "error"), + map(x => x as ValidationResultError), + first(), + ) + .toPromise() + expect(withError.error).toBe('"firstName" is not allowed to be empty') + + atom.modify(it => ({ + ...it, + firstName: "First Name", + })) + const success = await firstNameValidation + .pipe( + filter(x => x.status === "success"), + map(x => x as ValidationResultSuccess), + first(), + ) + .toPromise() + expect(success.status).toBe("success") + }) + + it("should display validating status while perform async validation", async () => { + expect.assertions(3) + const atom = signUpForm.getAtom() + const form = FormStore.create(atom, signUpForm.asyncValidation) + + const tillError = await collectTill(form.validationResult, "error") + expect(tillError.length).toBe(2) + expect(tillError[0].status).toBe("validating") + expect(tillError[1].status).toBe("error") + }) + + it("should display validating status while perform async validation (with Observable)", async () => { + const atom = signUpForm.getAtom() + const validate = (): Observable> => { + return timer(0, 1000).pipe( + map(x => { + if (x > 0) { + return { + status: "error", + error: "some value", + children: {}, + } + } else { + return { + status: "success", + } + } + }), + ) + } + + const form = FormStore.create(atom, validate) + await collectTill(form.validationResult, "success") + const tillError = await collectTill(form.validationResult, "error") + expect(tillError.length).toBe(2) + expect(tillError[0].status).toBe("success") + expect(tillError[1].status).toBe("error") + }) + + it("if there is error, no validation status should be emitted", async () => { + expect.assertions(7) + const atom = signUpForm.getAtom() + const form = FormStore.create(atom, signUpForm.asyncValidation) + + const results1 = await collectTill(form.validationResult, "error") + expect(results1.length).toBe(2) + expect(results1[0].status).toBe("validating") + expect(results1[1].status).toBe("error") + + atom.lens("firstName").set("First name") + atom.lens("lastName").set("Last name") + const results2 = await collectTill(form.validationResult, "success") + expect(results2.length).toBe(3) + expect(results2[0].status).toBe("error") + expect(results2[1].status).toBe("error") + expect(results2[2].status).toBe("success") + }) + + it("validating should be emitted after success", async () => { + expect.assertions(6) + const atom = signUpForm.getAtom(createDefaultSignUpForm("First name", "Last name")) + const form = FormStore.create(atom, signUpForm.asyncValidation) + const results1 = await collectTill(form.validationResult, "success") + expect(results1.length).toBe(2) + expect(results1[0].status).toBe("validating") + expect(results1[1].status).toBe("success") + atom.lens("firstName").set("") + atom.lens("lastName").set("") + + const results2 = await collectTill(form.validationResult, "error") + expect(results2.length).toBe(2) + expect(results2[0].status).toBe("validating") + expect(results2[1].status).toBe("error") + }) + + it("should work with custom lens", async () => { + expect.assertions(7) + const keyed = new KeyedForm(signUpForm.schema, () => createDefaultSignUpForm()) + const atom = Atom.create({}) + const form = FormStore.create(atom, keyed.validation) + const binded = form.bind("unknown-key", keyed.getLensByKey) + + const firstName$ = binded.value.lens("firstName") + const lastName$ = binded.value.lens("lastName") + expect(firstName$.get()).toEqual("") + expect(lastName$.get()).toEqual("") + + firstName$.set("John") + lastName$.set("Doe") + + expect(firstName$.get()).toEqual("John") + expect(lastName$.get()).toEqual("Doe") + + expect(atom.get()).toEqual({ + "unknown-key": createDefaultSignUpForm("John", "Doe"), + }) + + binded.bind("firstName").value.set("John") + binded.bind("lastName").value.set("Doe") + + const results2 = await collectTill(form.validationResult, "success") + expect(results2.length).toBe(1) + expect(results2[0].status).toBe("success") + }) }) function createDefaultSignUpForm(firstName = "", lastName = ""): SignUpFormSchema { - return { - firstName, - lastName, - } + return { + firstName, + lastName, + } } diff --git a/packages/form-store/src/index.ts b/packages/form-store/src/index.ts index cf56b04..1e14d9f 100644 --- a/packages/form-store/src/index.ts +++ b/packages/form-store/src/index.ts @@ -1,48 +1,48 @@ -import { Observable } from "rxjs" -import { Atom } from "@rixio/atom" -import { Lens } from "@rixio/lens" +import type { Observable } from "rxjs" +import type { Atom } from "@rixio/atom" +import type { Lens } from "@rixio/lens" import { map } from "rxjs/operators" import type { - Validate, - ValidationResult, - ValidationResultError, - ValidationResultSuccess, - ValidationResultValidating, + Validate, + ValidationResult, + ValidationResultError, + ValidationResultSuccess, + ValidationResultValidating, } from "./domain" import { createValidationResult } from "./utils/create-validation-result" export class FormStore { - readonly canSubmit$ = this.validationResult.pipe(map(x => x.status === "success")) - private readonly bindCache: Map> = new Map() + readonly canSubmit$ = this.validationResult.pipe(map(x => x.status === "success")) + private readonly bindCache: Map> = new Map() - constructor(public readonly value: Atom, public readonly validationResult: Observable>) {} + constructor(public readonly value: Atom, public readonly validationResult: Observable>) {} - bind(field: K, getCustomLens?: (key: K) => Lens): FormStore { - const cached = this.bindCache.get(field) - if (cached) return cached - const lensed$ = getCustomLens ? this.value.lens(getCustomLens(field)) : this.value.lens(field) - const created = new FormStore(lensed$, this.getChild(field)) - this.bindCache.set(field, created) - return created - } + bind(field: K, getCustomLens?: (key: K) => Lens): FormStore { + const cached = this.bindCache.get(field) + if (cached) return cached + const lensed$ = getCustomLens ? this.value.lens(getCustomLens(field)) : this.value.lens(field) + const created = new FormStore(lensed$, this.getChild(field)) + this.bindCache.set(field, created) + return created + } - private getChild(field: K): Observable> { - return this.validationResult.pipe( - map(x => { - if (x.status === "validating") { - return { status: "validating" } as ValidationResultValidating - } - if (x.status === "error" && x.children?.[field]) { - return x.children[field] as ValidationResultError - } - return { status: "success" } as ValidationResultSuccess - }) - ) - } + private getChild(field: K): Observable> { + return this.validationResult.pipe( + map(x => { + if (x.status === "validating") { + return { status: "validating" } as ValidationResultValidating + } + if (x.status === "error" && x.children?.[field]) { + return x.children[field] as ValidationResultError + } + return { status: "success" } as ValidationResultSuccess + }), + ) + } - static create(value: Atom, validate: Validate) { - return new FormStore(value, createValidationResult(value, validate)) - } + static create(value: Atom, validate: Validate) { + return new FormStore(value, createValidationResult(value, validate)) + } } export * from "./is-submit-disabled" diff --git a/packages/form-store/src/is-submit-disabled.ts b/packages/form-store/src/is-submit-disabled.ts index a2abcf7..fda6f66 100644 --- a/packages/form-store/src/is-submit-disabled.ts +++ b/packages/form-store/src/is-submit-disabled.ts @@ -1,9 +1,10 @@ -import { combineLatest, Observable } from "rxjs" +import type { Observable } from "rxjs" +import { combineLatest } from "rxjs" import { map } from "rxjs/operators" -import { FormStore } from "./index" +import type { FormStore } from "./index" export function isSubmitDisabled(form: FormStore, displayErrors: Observable) { - return combineLatest([form.canSubmit$, displayErrors]).pipe( - map(([canSubmit, displayErrors]) => displayErrors && !canSubmit) - ) + return combineLatest([form.canSubmit$, displayErrors]).pipe( + map(([canSubmit, displayErrors]) => displayErrors && !canSubmit), + ) } diff --git a/packages/form-store/src/utils/create-validation-result.ts b/packages/form-store/src/utils/create-validation-result.ts index 977ab9d..f4ae2b5 100644 --- a/packages/form-store/src/utils/create-validation-result.ts +++ b/packages/form-store/src/utils/create-validation-result.ts @@ -1,38 +1,38 @@ import { concat, Observable, of } from "rxjs" import { fromPromise } from "rxjs/internal-compatibility" import { mergeMap, scan, shareReplay } from "rxjs/operators" -import { Validate, ValidationResult, ValidationResultValidating } from "../domain" +import type { Validate, ValidationResult, ValidationResultValidating } from "../domain" export function createValidationResult( - value: Observable, - validate: Validate + value: Observable, + validate: Validate, ): Observable> { - return value.pipe( - mergeMap(simplify(validate)), - scan>( - (prev, next) => { - if (next.status === "validating" && prev.status === "error") { - return prev - } - return next - }, - { - status: "validating", - } as ValidationResultValidating - ), - shareReplay(1) - ) + return value.pipe( + mergeMap(simplify(validate)), + scan>( + (prev, next) => { + if (next.status === "validating" && prev.status === "error") { + return prev + } + return next + }, + { + status: "validating", + } as ValidationResultValidating, + ), + shareReplay(1), + ) } function simplify(validate: Validate): (value: T) => Observable> { - return value => { - const vr = validate(value) - if (vr instanceof Observable) { - return vr - } else if ("then" in vr) { - return concat(of({ status: "validating" } as ValidationResultValidating), fromPromise(vr)) - } else { - return of(vr) - } - } + return value => { + const vr = validate(value) + if (vr instanceof Observable) { + return vr + } else if ("then" in vr) { + return concat(of({ status: "validating" } as ValidationResultValidating), fromPromise(vr)) + } else { + return of(vr) + } + } } diff --git a/packages/form-store/src/utils/validate-joi.spec.ts b/packages/form-store/src/utils/validate-joi.spec.ts index ec57d82..8366942 100644 --- a/packages/form-store/src/utils/validate-joi.spec.ts +++ b/packages/form-store/src/utils/validate-joi.spec.ts @@ -2,131 +2,131 @@ import Joi from "joi" import { validateJoi } from ".." describe("validate-joi", () => { - test("should validate plain multiple fields", () => { - const schema = Joi.object().keys({ - firstName: Joi.string().required().min(2).max(100), - lastName: Joi.string().required(), - }) + test("should validate plain multiple fields", () => { + const schema = Joi.object().keys({ + firstName: Joi.string().required().min(2).max(100), + lastName: Joi.string().required(), + }) - const validator = validateJoi<{ firstName?: string; lastName?: string }>(schema) + const validator = validateJoi<{ firstName?: string; lastName?: string }>(schema) - const resultGood = validator({ - firstName: "qwe", - lastName: "eee", - }) + const resultGood = validator({ + firstName: "qwe", + lastName: "eee", + }) - expect(resultGood.status).toBe("success") + expect(resultGood.status).toBe("success") - const resultBad = validator({ - firstName: "", - }) + const resultBad = validator({ + firstName: "", + }) - expect(resultBad.status).toBe("error") - if (resultBad.status !== "error") throw new Error() + expect(resultBad.status).toBe("error") + if (resultBad.status !== "error") throw new Error("Error") - expect(resultBad.children.firstName).toBeDefined() - expect(resultBad.children.firstName?.status).toBe("error") + expect(resultBad.children.firstName).toBeDefined() + expect(resultBad.children.firstName?.status).toBe("error") - expect(resultBad.children.lastName).toBeDefined() - expect(resultBad.children.lastName?.status).toBe("error") - }) + expect(resultBad.children.lastName).toBeDefined() + expect(resultBad.children.lastName?.status).toBe("error") + }) - test("should validate plain multiple fields", () => { - const schema = Joi.object().keys({ - a: Joi.object().keys({ - b: Joi.object().keys({ - c: Joi.string().required().allow("hello").only(), - d: Joi.string().required().allow("world").only(), - }), - e: Joi.string().required().allow("how").only(), - }), - f: Joi.object().keys({ - j: Joi.array().items( - Joi.object().keys({ - x: Joi.string().required().allow("hello").only(), - y: Joi.string().required().allow("world").only(), - }), - Joi.object().keys({ - x: Joi.string().required().allow("hello").only(), - y: Joi.string().required().allow("world").only(), - }) - ), - h: Joi.string().required().allow("how").only(), - }), - }) + test("should validate plain multiple fields", () => { + const schema = Joi.object().keys({ + a: Joi.object().keys({ + b: Joi.object().keys({ + c: Joi.string().required().allow("hello").only(), + d: Joi.string().required().allow("world").only(), + }), + e: Joi.string().required().allow("how").only(), + }), + f: Joi.object().keys({ + j: Joi.array().items( + Joi.object().keys({ + x: Joi.string().required().allow("hello").only(), + y: Joi.string().required().allow("world").only(), + }), + Joi.object().keys({ + x: Joi.string().required().allow("hello").only(), + y: Joi.string().required().allow("world").only(), + }), + ), + h: Joi.string().required().allow("how").only(), + }), + }) - type DeepSchema = { - a: { b: { c: string; d: string }; e: string } - f: { j: { x: string; y: string }[]; h: string } - } + type DeepSchema = { + a: { b: { c: string; d: string }; e: string } + f: { j: { x: string; y: string }[]; h: string } + } - const validator = validateJoi(schema) + const validator = validateJoi(schema) - const resultGood = validator({ - a: { - b: { - c: "hello", - d: "world", - }, - e: "how", - }, - f: { - j: [ - { x: "hello", y: "world" }, - { x: "hello", y: "world" }, - ], - h: "how", - }, - }) + const resultGood = validator({ + a: { + b: { + c: "hello", + d: "world", + }, + e: "how", + }, + f: { + j: [ + { x: "hello", y: "world" }, + { x: "hello", y: "world" }, + ], + h: "how", + }, + }) - expect(resultGood.status).toBe("success") + expect(resultGood.status).toBe("success") - const resultBad = validator({ - a: { - b: { - c: "world", - d: "hello", - }, - e: "ww", - }, - f: { - j: [ - { x: "world", y: "hello" }, - { x: "world", y: "hello" }, - ], - h: "how", - }, - }) + const resultBad = validator({ + a: { + b: { + c: "world", + d: "hello", + }, + e: "ww", + }, + f: { + j: [ + { x: "world", y: "hello" }, + { x: "world", y: "hello" }, + ], + h: "how", + }, + }) - expect(resultBad).toMatchObject({ - status: "error", - children: { - a: { - status: "error", - children: { - b: { - status: "error", - children: { - c: { status: "error" }, - d: { status: "error" }, - }, - }, - e: { status: "error" }, - }, - }, - f: { - status: "error", - children: { - j: { - status: "error", - children: { - 0: { status: "error" }, - 1: { status: "error" }, - }, - }, - }, - }, - }, - }) - }) + expect(resultBad).toMatchObject({ + status: "error", + children: { + a: { + status: "error", + children: { + b: { + status: "error", + children: { + c: { status: "error" }, + d: { status: "error" }, + }, + }, + e: { status: "error" }, + }, + }, + f: { + status: "error", + children: { + j: { + status: "error", + children: { + 0: { status: "error" }, + 1: { status: "error" }, + }, + }, + }, + }, + }, + }) + }) }) diff --git a/packages/form-store/src/utils/validate-joi.ts b/packages/form-store/src/utils/validate-joi.ts index 9450361..050725f 100644 --- a/packages/form-store/src/utils/validate-joi.ts +++ b/packages/form-store/src/utils/validate-joi.ts @@ -2,34 +2,34 @@ import type { ObjectSchema, ValidationOptions } from "joi" import type { ValidationResult, ValidationResultError, ValidationResultSuccess } from "../domain" export function validateJoi( - schema: ObjectSchema, - options: ValidationOptions = {} + schema: ObjectSchema, + options: ValidationOptions = {}, ): (value: T) => ValidationResult { - return value => { - const vr = schema.validate(value, { - abortEarly: false, - stripUnknown: true, - ...options, - }) - if (vr.error != null) { - const result: ValidationResultError = { - status: "error", - error: vr.error.message, - children: {}, - } - vr.error.details.forEach(err => { - let current: ValidationResultError = result - err.path.forEach(path => { - const next: ValidationResultError = { status: "error", error: err.message, children: {} } - if (current.children[path] !== undefined && (current.children[path] as any).children !== undefined) { - next.children = (current.children[path] as any).children - } - current.children[path] = next - current = next - }) - }) - return result as ValidationResultError - } - return { status: "success" } as ValidationResultSuccess - } + return value => { + const vr = schema.validate(value, { + abortEarly: false, + stripUnknown: true, + ...options, + }) + if (vr.error != null) { + const result: ValidationResultError = { + status: "error", + error: vr.error.message, + children: {}, + } + vr.error.details.forEach(err => { + let current: ValidationResultError = result + err.path.forEach(path => { + const next: ValidationResultError = { status: "error", error: err.message, children: {} } + if (current.children[path] !== undefined && (current.children[path] as any).children !== undefined) { + next.children = (current.children[path] as any).children + } + current.children[path] = next + current = next + }) + }) + return result as ValidationResultError + } + return { status: "success" } as ValidationResultSuccess + } } diff --git a/packages/lens/src/base.ts b/packages/lens/src/base.ts index d157afe..e10d624 100644 --- a/packages/lens/src/base.ts +++ b/packages/lens/src/base.ts @@ -1,29 +1,29 @@ -import { Option } from "./utils" +import type { Option } from "./utils" import { SimpleCache } from "./simple-cache" export interface Optic { - get(s: TSource): T - set(v: U, s: TSource): TSource - modify(updateFn: (v: T) => U, s: TSource): TSource + get(s: TSource): T + set(v: U, s: TSource): TSource + modify(updateFn: (v: T) => U, s: TSource): TSource } function createModify(getter: (s: TSource) => T, setter: (v: U, s: TSource) => TSource) { - return function modify(updateFn: (v: T) => U, s: TSource) { - return setter(updateFn(getter(s)), s) - } + return function modify(updateFn: (v: T) => U, s: TSource) { + return setter(updateFn(getter(s)), s) + } } export namespace Optic { - export function optic( - getter: (s: TSource) => T, - setter: (v: U, s: TSource) => TSource - ): Optic { - return { - get: getter, - set: setter, - modify: createModify(getter, setter), - } - } + export function optic( + getter: (s: TSource) => T, + setter: (v: U, s: TSource) => TSource, + ): Optic { + return { + get: getter, + set: setter, + modify: createModify(getter, setter), + } + } } // @NOTE lens and prism are monomorphic: can't change the type of @@ -38,142 +38,142 @@ export namespace Optic { * Read more here: https://en.wikibooks.org/wiki/Haskell/Lenses_and_functional_references */ export interface Lens extends Optic { - compose(next: Lens): Lens - compose(next: Prism): Prism + compose(next: Lens): Lens + compose(next: Prism): Prism } export interface Prism extends Optic, T> { - compose(next: Lens): Prism - compose(next: Lens, U>): Lens - compose(next: Prism): Prism + compose(next: Lens): Prism + compose(next: Lens, U>): Lens + compose(next: Prism): Prism } export namespace Prism { - export function create( - getter: (s: TSource) => Option, - setter: (v: T, s: TSource) => TSource - ): Prism { - return { - get: getter, - set: setter, - modify: createModify(getter, setter), - - compose(next: Lens | Prism): Prism { - // no runtime dispatch – the implementation works for both - // lens and prism argument - return create( - (s: TSource) => { - const x = getter(s) - return x !== undefined ? next.get(x) : undefined - }, - (v: U, s: TSource) => { - const x = getter(s) - return x !== undefined ? setter(next.set(v, x), s) : s - } - ) - }, - } - } + export function create( + getter: (s: TSource) => Option, + setter: (v: T, s: TSource) => TSource, + ): Prism { + return { + get: getter, + set: setter, + modify: createModify(getter, setter), + + compose(next: Lens | Prism): Prism { + // no runtime dispatch – the implementation works for both + // lens and prism argument + return create( + (s: TSource) => { + const x = getter(s) + return x !== undefined ? next.get(x) : undefined + }, + (v: U, s: TSource) => { + const x = getter(s) + return x !== undefined ? setter(next.set(v, x), s) : s + }, + ) + }, + } + } } export namespace Lens { - /** - * Create a lens. - * - * @export - * @template O type of the source - * @template P type of the destination - * @param getter a getter function - * @param setter a setter function - * @returns a lens that operates by given getter and setter - */ - export function create( - getter: (s: TSource) => T, - setter: (v: T, s: TSource) => TSource - ): Lens { - const cache = new SimpleCache>(next => - create( - (s: TSource) => next.get(getter(s)), - (v: any, s: TSource) => setter(next.set(v, getter(s)), s) - ) - ) - - return { - get: getter, - set: setter, - modify: createModify(getter, setter), - - compose(next: Lens): Lens { - return cache.getOrCreate(next) - }, - } - } - - /** - * Compose several lenses, where each subsequent lens' state type is the previous - * lens' output type. - * - * You need to explicitly say what will be the type of resulting lens, and you - * need to do it right as there are no guarantees at compile time. - * - * @static - * @template S the resulting lens' state - * @template A the resulting lens' output - */ - export function compose(l: Lens): Lens - - export function compose(l1: Lens, l2: Lens): Lens - - export function compose(l1: Lens, l2: Lens, l3: Lens): Lens - - export function compose( - l1: Lens, - l2: Lens, - l3: Lens, - l4: Lens - ): Lens - - export function compose( - l1: Lens, - l2: Lens, - l3: Lens, - l4: Lens, - l5: Lens - ): Lens - - export function compose(...lenses: Lens[]): Lens - - export function compose(...lenses: Lens[]): Lens { - if (lenses.length === 0) { - throw new TypeError("Can not compose zero lenses. You probably want `Lens.identity`.") - } else if (lenses.length === 1) { - return lenses[0] - } else { - return lenses.slice(1).reduce((c, l) => c.compose(l), lenses[0]) as Lens - } - } - - const _identity = create( - x => x, - (x: any, _: any) => x - ) - - /** - * The identity lens – a lens that reads and writes the object itself. - */ - export function identity(): Lens { - return _identity as Lens - } - - const _nothing = Prism.create( - _ => undefined, - (_: any, o: any) => o - ) - - /** - * A lens that always returns `undefined` on `get` and does no change on `set`. - */ - export function nothing() { - return _nothing as Prism - } + /** + * Create a lens. + * + * @export + * @template O type of the source + * @template P type of the destination + * @param getter a getter function + * @param setter a setter function + * @returns a lens that operates by given getter and setter + */ + export function create( + getter: (s: TSource) => T, + setter: (v: T, s: TSource) => TSource, + ): Lens { + const cache = new SimpleCache>(next => + create( + (s: TSource) => next.get(getter(s)), + (v: any, s: TSource) => setter(next.set(v, getter(s)), s), + ), + ) + + return { + get: getter, + set: setter, + modify: createModify(getter, setter), + + compose(next: Lens): Lens { + return cache.getOrCreate(next) + }, + } + } + + /** + * Compose several lenses, where each subsequent lens' state type is the previous + * lens' output type. + * + * You need to explicitly say what will be the type of resulting lens, and you + * need to do it right as there are no guarantees at compile time. + * + * @static + * @template S the resulting lens' state + * @template A the resulting lens' output + */ + export function compose(l: Lens): Lens + + export function compose(l1: Lens, l2: Lens): Lens + + export function compose(l1: Lens, l2: Lens, l3: Lens): Lens + + export function compose( + l1: Lens, + l2: Lens, + l3: Lens, + l4: Lens, + ): Lens + + export function compose( + l1: Lens, + l2: Lens, + l3: Lens, + l4: Lens, + l5: Lens, + ): Lens + + export function compose(...lenses: Lens[]): Lens + + export function compose(...lenses: Lens[]): Lens { + if (lenses.length === 0) { + throw new TypeError("Can not compose zero lenses. You probably want `Lens.identity`.") + } else if (lenses.length === 1) { + return lenses[0] + } else { + return lenses.slice(1).reduce((c, l) => c.compose(l), lenses[0]) as Lens + } + } + + const _identity = create( + x => x, + x => x, + ) + + /** + * The identity lens – a lens that reads and writes the object itself. + */ + export function identity(): Lens { + return _identity as Lens + } + + const _nothing = Prism.create( + () => undefined, + (_: any, o: any) => o, + ) + + /** + * A lens that always returns `undefined` on `get` and does no change on `set`. + */ + export function nothing() { + return _nothing as Prism + } } diff --git a/packages/lens/src/equals.ts b/packages/lens/src/equals.ts index 5a09217..2e0fb34 100644 --- a/packages/lens/src/equals.ts +++ b/packages/lens/src/equals.ts @@ -29,23 +29,23 @@ // tslint:disable no-function-expression function arrayFromIterator(iter: Iterator) { - const result: T[] = [] - let next: IteratorResult - while (!(next = iter.next()).done) { - // tslint:disable-line no-conditional-assignment - result.push(next.value) - } - return result + const result: T[] = [] + let next: IteratorResult + while (!(next = iter.next()).done) { + // tslint:disable-line no-conditional-assignment + result.push(next.value) + } + return result } function functionName(f: Function) { - // String(x => x) evaluates to "x => x", so the pattern may not match. - const match = String(f).match(/^function (\w*)/) - return match == null ? "" : match[1] + // String(x => x) evaluates to "x => x", so the pattern may not match. + const match = String(f).match(/^function (\w*)/) + return match == null ? "" : match[1] } function has(prop: string, obj: any) { - return Object.prototype.hasOwnProperty.call(obj, prop) + return Object.prototype.hasOwnProperty.call(obj, prop) } /** @@ -64,28 +64,28 @@ function has(prop: string, obj: any) { * R.identical(NaN, NaN); //=> true */ function identical(a: any, b: any) { - // SameValue algorithm - if (a === b) { - // Steps 1-5, 7-10 - // Steps 6.b-6.e: +0 != -0 - return a !== 0 || 1 / a === 1 / b - } else { - // Step 6.a: NaN == NaN - // eslint-disable-next-line no-self-compare - return a !== a && b !== b - } + // SameValue algorithm + if (a === b) { + // Steps 1-5, 7-10 + // Steps 6.b-6.e: +0 != -0 + return a !== 0 || 1 / a === 1 / b + } else { + // Step 6.a: NaN == NaN + // eslint-disable-next-line no-self-compare + return a !== a && b !== b + } } const _isArguments = (function () { - const toString = Object.prototype.toString - - return toString.call(arguments) === "[object Arguments]" - ? function isArguments(x: any) { - return toString.call(x) === "[object Arguments]" - } - : function isArguments(x: any) { - return has("callee", x) - } + const toString = Object.prototype.toString + + return toString.call(arguments) === "[object Arguments]" + ? function isArguments(x: any) { + return toString.call(x) === "[object Arguments]" + } + : function isArguments(x: any) { + return has("callee", x) + } })() /** @@ -95,92 +95,92 @@ const _isArguments = (function () { * across different JS platforms. */ const keys = (function () { - // cover IE < 9 keys issues - const hasEnumBug = !{ toString: null }.propertyIsEnumerable("toString") - - const nonEnumerableProps = [ - "constructor", - "valueOf", - "isPrototypeOf", - "toString", - "propertyIsEnumerable", - "hasOwnProperty", - "toLocaleString", - ] - - // Safari bug - const hasArgsEnumBug = (function () { - return arguments.propertyIsEnumerable("length") - })() - - const contains = function contains(list: T[], item: T) { - var idx = 0 // tslint:disable-line no-var-keyword - - while (idx < list.length) { - if (list[idx] === item) return true - - idx += 1 - } - - return false - } - - return typeof Object.keys === "function" && !hasArgsEnumBug - ? function keys(obj: any) { - return Object(obj) !== obj ? [] : Object.keys(obj) - } - : function keys(obj: any) { - if (Object(obj) !== obj) return [] - - let prop: string, nIdx: number - const ks: string[] = [] - const checkArgsLength = hasArgsEnumBug && _isArguments(obj) - - for (prop in obj) { - if (has(prop, obj) && (!checkArgsLength || prop !== "length")) { - ks[ks.length] = prop - } - } - - if (hasEnumBug) { - nIdx = nonEnumerableProps.length - 1 - while (nIdx >= 0) { - prop = nonEnumerableProps[nIdx] - if (has(prop, obj) && !contains(ks, prop)) { - ks[ks.length] = prop - } - nIdx -= 1 - } - } - - return ks - } + // cover IE < 9 keys issues + const hasEnumBug = !{ toString: null }.propertyIsEnumerable("toString") + + const nonEnumerableProps = [ + "constructor", + "valueOf", + "isPrototypeOf", + "toString", + "propertyIsEnumerable", + "hasOwnProperty", + "toLocaleString", + ] + + // Safari bug + const hasArgsEnumBug = (function () { + return arguments.propertyIsEnumerable("length") + })() + + const contains = function contains(list: T[], item: T) { + var idx = 0 // tslint:disable-line no-var-keyword + + while (idx < list.length) { + if (list[idx] === item) return true + + idx += 1 + } + + return false + } + + return typeof Object.keys === "function" && !hasArgsEnumBug + ? function keys(obj: any) { + return Object(obj) !== obj ? [] : Object.keys(obj) + } + : function keys(obj: any) { + if (Object(obj) !== obj) return [] + + let prop: string, nIdx: number + const ks: string[] = [] + const checkArgsLength = hasArgsEnumBug && _isArguments(obj) + + for (prop in obj) { + if (has(prop, obj) && (!checkArgsLength || prop !== "length")) { + ks[ks.length] = prop + } + } + + if (hasEnumBug) { + nIdx = nonEnumerableProps.length - 1 + while (nIdx >= 0) { + prop = nonEnumerableProps[nIdx] + if (has(prop, obj) && !contains(ks, prop)) { + ks[ks.length] = prop + } + nIdx -= 1 + } + } + + return ks + } })() type TypeDescString = - | "Object" - | "Number" - | "Boolean" - | "String" - | "Null" - | "Array" - | "RegExp" - | "Int8Array" - | "Uint8Array" - | "Uint8ClampedArray" - | "Int16Array" - | "Uint16Array" - | "Int32Array" - | "Uint32Array" - | "Float32Array" - | "Float64Array" - | "Arguments" - | "Map" - | "Set" - | "Date" - | "Error" - | "ArrayBuffer" - | "Undefined" + | "Object" + | "Number" + | "Boolean" + | "String" + | "Null" + | "Array" + | "RegExp" + | "Int8Array" + | "Uint8Array" + | "Uint8ClampedArray" + | "Int16Array" + | "Uint16Array" + | "Int32Array" + | "Uint32Array" + | "Float32Array" + | "Float64Array" + | "Arguments" + | "Map" + | "Set" + | "Date" + | "Error" + | "ArrayBuffer" + | "Undefined" /** * Gives a single-word string description of the (native) type of a value, @@ -199,12 +199,12 @@ type TypeDescString = * R.type(/[A-z]/); //=> "RegExp" */ function type(val: any) { - // eslint-disable-next-line no-nested-ternary - return val === null - ? ("Null" as TypeDescString) - : val === undefined - ? ("Undefined" as TypeDescString) - : (Object.prototype.toString.call(val).slice(8, -1) as TypeDescString) + // eslint-disable-next-line no-nested-ternary + return val === null + ? ("Null" as TypeDescString) + : val === undefined + ? ("Undefined" as TypeDescString) + : (Object.prototype.toString.call(val).slice(8, -1) as TypeDescString) } /** @@ -225,101 +225,101 @@ function type(val: any) { * equals(a, b); //=> true */ export function equals(a: any, b: any, stackA: any[] = [], stackB: any[] = []) { - if (identical(a, b)) return true - if (type(a) !== type(b)) return false - if (a == null || b == null) return false - - if (typeof a.equals === "function" || typeof b.equals === "function") { - return typeof a.equals === "function" && a.equals(b) && typeof b.equals === "function" && b.equals(a) - } - - switch (type(a)) { - case "Arguments": - case "Array": - case "Object": - if (typeof a.constructor === "function" && functionName(a.constructor) === "Promise") { - return a === b - } - break - - case "Boolean": - case "Number": - case "String": - if (!(typeof a === typeof b && identical(a.valueOf(), b.valueOf()))) return false - - break - - case "Date": - if (!identical(a.valueOf(), b.valueOf())) return false - - break - - case "Error": - return a.name === b.name && a.message === b.message - - case "RegExp": - if ( - !( - a.source === b.source && - a.global === b.global && - a.ignoreCase === b.ignoreCase && - a.multiline === b.multiline && - a.sticky === b.sticky && - a.unicode === b.unicode - ) - ) { - return false - } - - break - - case "Map": - case "Set": - if (!equals(arrayFromIterator(a.entries()), arrayFromIterator(b.entries()), stackA, stackB)) return false - - break - - case "Int8Array": - case "Uint8Array": - case "Uint8ClampedArray": - case "Int16Array": - case "Uint16Array": - case "Int32Array": - case "Uint32Array": - case "Float32Array": - case "Float64Array": - break - - case "ArrayBuffer": - break - - default: - // Values of other types are only equal if identical. - return false - } - - const keysA = keys(a) - if (keysA.length !== keys(b).length) return false - - let idx = stackA.length - 1 - while (idx >= 0) { - if (stackA[idx] === a) return stackB[idx] === b - - idx -= 1 - } - - stackA.push(a) - stackB.push(b) - idx = keysA.length - 1 - while (idx >= 0) { - const key = keysA[idx] - - if (!(has(key, b) && equals(b[key], a[key], stackA, stackB))) return false - - idx -= 1 - } - stackA.pop() - stackB.pop() - - return true + if (identical(a, b)) return true + if (type(a) !== type(b)) return false + if (a == null || b == null) return false + + if (typeof a.equals === "function" || typeof b.equals === "function") { + return typeof a.equals === "function" && a.equals(b) && typeof b.equals === "function" && b.equals(a) + } + + switch (type(a)) { + case "Arguments": + case "Array": + case "Object": + if (typeof a.constructor === "function" && functionName(a.constructor) === "Promise") { + return a === b + } + break + + case "Boolean": + case "Number": + case "String": + if (!(typeof a === typeof b && identical(a.valueOf(), b.valueOf()))) return false + + break + + case "Date": + if (!identical(a.valueOf(), b.valueOf())) return false + + break + + case "Error": + return a.name === b.name && a.message === b.message + + case "RegExp": + if ( + !( + a.source === b.source && + a.global === b.global && + a.ignoreCase === b.ignoreCase && + a.multiline === b.multiline && + a.sticky === b.sticky && + a.unicode === b.unicode + ) + ) { + return false + } + + break + + case "Map": + case "Set": + if (!equals(arrayFromIterator(a.entries()), arrayFromIterator(b.entries()), stackA, stackB)) return false + + break + + case "Int8Array": + case "Uint8Array": + case "Uint8ClampedArray": + case "Int16Array": + case "Uint16Array": + case "Int32Array": + case "Uint32Array": + case "Float32Array": + case "Float64Array": + break + + case "ArrayBuffer": + break + + default: + // Values of other types are only equal if identical. + return false + } + + const keysA = keys(a) + if (keysA.length !== keys(b).length) return false + + let idx = stackA.length - 1 + while (idx >= 0) { + if (stackA[idx] === a) return stackB[idx] === b + + idx -= 1 + } + + stackA.push(a) + stackB.push(b) + idx = keysA.length - 1 + while (idx >= 0) { + const key = keysA[idx] + + if (!(has(key, b) && equals(b[key], a[key], stackA, stackB))) return false + + idx -= 1 + } + stackA.pop() + stackB.pop() + + return true } diff --git a/packages/lens/src/json.ts b/packages/lens/src/json.ts index e71ea8b..84b9ee9 100644 --- a/packages/lens/src/json.ts +++ b/packages/lens/src/json.ts @@ -3,29 +3,30 @@ * @module */ -import { structEq, setKey, conservatively, findIndex, Option } from "./utils" +import type { Option } from "./utils" +import { structEq, setKey, conservatively, findIndex } from "./utils" import { Lens, Prism } from "./base" import { SimpleCache } from "./simple-cache" // @NOTE only need this interface to add JSDocs for this call. export interface KeyImplFor { - /** - * Create a lens focusing on a key of an object. - * - * Requires two subsequent calls, first with only a type argument and no function - * arguments and second with the key argument. - * - * This enables better auto-completion, and is required because TypeScript does not - * allow to specify only some of the type arguments. - * - * This is the second call, where you supply the key argument. - * @example - * interface SomeObject { - * someProp: number - * } - * - * const lens = Lens.key()('someProp') - */ (k: K): Lens + /** + * Create a lens focusing on a key of an object. + * + * Requires two subsequent calls, first with only a type argument and no function + * arguments and second with the key argument. + * + * This enables better auto-completion, and is required because TypeScript does not + * allow to specify only some of the type arguments. + * + * This is the second call, where you supply the key argument. + * @example + * interface SomeObject { + * someProp: number + * } + * + * const lens = Lens.key()('someProp') + */ (k: K): Lens } /** @@ -71,43 +72,43 @@ export function keyImpl(k: string): Prism<{ [k: string]: TValue }, // Pretty cool! export function keyImpl(): KeyImplFor export function keyImpl(k?: string) { - if (k === undefined) { - return (k: K): Lens => - keyCache.getOrCreate(k as string) as Lens - } - return keyCache.getOrCreate(k) + if (k === undefined) { + return (k: K): Lens => + keyCache.getOrCreate(k as string) as Lens + } + return keyCache.getOrCreate(k) } export function indexImpl(i: number): Prism { - if (i < 0) throw new TypeError(`${i} is not a valid array index, expected >= 0`) - return indexCache.getOrCreate(i) + if (i < 0) throw new TypeError(`${i} is not a valid array index, expected >= 0`) + return indexCache.getOrCreate(i) } export function withDefaultImpl(defaultValue: T): Lens, T> { - // @TODO is this cast safe? - return Lens.replace(undefined, defaultValue) as Lens, T> + // @TODO is this cast safe? + return Lens.replace(undefined, defaultValue) as Lens, T> } function choose(getLens: (state: T) => Lens): Lens { - return Lens.create( - (s: T) => getLens(s).get(s), - (v: U, s: T) => getLens(s).set(v, s) - ) + return Lens.create( + (s: T) => getLens(s).get(s), + (v: U, s: T) => getLens(s).set(v, s), + ) } -export function replaceImpl(originalValue: T, newValue: T): Lens { - return Lens.create( - x => (structEq(x, originalValue) ? newValue : x), - conservatively((y: T) => (structEq(y, newValue) ? originalValue : y)) - ) +export function replaceImpl(originalValue: T, nextValue: T): Lens { + return Lens.create( + x => (structEq(x, originalValue) ? nextValue : x), + conservatively((y: T) => (structEq(y, nextValue) ? originalValue : y)), + ) } export function findImpl(predicate: (x: T) => boolean): Prism { - return choose((xs: T[]) => { - const i = findIndex(xs, predicate) + return choose((xs: T[]) => { + const i = findIndex(xs, predicate) - return i < 0 ? Lens.nothing() : Lens.index(i) - }) + return i < 0 ? Lens.nothing() : Lens.index(i) + }) } // augment the base lens module with JSON-specific lens functions. @@ -115,38 +116,38 @@ export function findImpl(predicate: (x: T) => boolean): Prism { // for a nice consumer API with all lens function under the same namespace, // together with the lens type. declare module "./base" { - export namespace Lens { - export let key: typeof keyImpl - - /** - * Create a lens that looks at an element at particular index position - * in an array. - * - * @template TItem type of array elements - * @param i the index - * @returns a lens to an element at particular position in an array - */ - export let index: typeof indexImpl - - /** - * Create a lens that will show a given default value if the actual - * value is absent (is undefined). - * - * @param defaultValue default value to return - */ - export let withDefault: typeof withDefaultImpl - - /** - * Create a lens that replaces a given value with a new one. - */ - export let replace: typeof replaceImpl - - /** - * Create a prism that focuses on an array's element that - * satisfies a given predicate. - */ - export let find: typeof findImpl - } + export namespace Lens { + export let key: typeof keyImpl + + /** + * Create a lens that looks at an element at particular index position + * in an array. + * + * @template TItem type of array elements + * @param i the index + * @returns a lens to an element at particular position in an array + */ + export let index: typeof indexImpl + + /** + * Create a lens that will show a given default value if the actual + * value is absent (is undefined). + * + * @param defaultValue default value to return + */ + export let withDefault: typeof withDefaultImpl + + /** + * Create a lens that replaces a given value with a new one. + */ + export let replace: typeof replaceImpl + + /** + * Create a prism that focuses on an array's element that + * satisfies a given predicate. + */ + export let find: typeof findImpl + } } Lens.key = keyImpl @@ -156,18 +157,18 @@ Lens.replace = replaceImpl Lens.find = findImpl const keyCache = new SimpleCache((key: string) => - Lens.create( - s => s[key], - (v, s) => setKey(key, v, s) - ) + Lens.create( + s => s[key], + (v, s) => setKey(key, v, s), + ), ) const indexCache = new SimpleCache>(i => - Prism.create( - xs => xs[i], - (v, xs) => { - if (xs.length <= i) return xs.concat(Array(i - xs.length), [v]) - if (structEq(v, xs[i])) return xs - return xs.slice(0, i).concat([v], xs.slice(i + 1)) - } - ) + Prism.create( + xs => xs[i], + (v, xs) => { + if (xs.length <= i) return xs.concat(new Array(i - xs.length), [v]) + if (structEq(v, xs[i])) return xs + return xs.slice(0, i).concat([v], xs.slice(i + 1)) + }, + ), ) diff --git a/packages/lens/src/lens.test.ts b/packages/lens/src/lens.test.ts index f5a92e3..26ae923 100644 --- a/packages/lens/src/lens.test.ts +++ b/packages/lens/src/lens.test.ts @@ -1,179 +1,179 @@ import { structEq } from "./utils" import { Lens } from "./index" -function roundtrip(name: string, l: Lens, obj: T, oldVal: U, newVal: U) { - describe(`lens roundtrip: ${name}`, () => { - it("get", () => expect(l.get(obj)).toEqual(oldVal)) - it("set", () => expect(l.get(l.set(newVal, obj))).toEqual(newVal)) - }) +function roundtrip(name: string, l: Lens, obj: T, oldVal: U, nextValue: U) { + describe(`lens roundtrip: ${name}`, () => { + it("get", () => expect(l.get(obj)).toEqual(oldVal)) + it("set", () => expect(l.get(l.set(nextValue, obj))).toEqual(nextValue)) + }) } // @see https://www.schoolofhaskell.com/school/to-infinity-and-beyond/pick-of-the-week/a-little-lens-starter-tutorial#the-lens-laws- function testLaws(l: Lens, object: T, value1: U, value2: U, name: string) { - describe(`lens laws: ${name}`, () => { - it("get-put", () => expect(structEq(object, l.set(l.get(object), object))).toBeTruthy()) - it("put-get", () => expect(structEq(value1, l.get(l.set(value1, object)))).toBeTruthy()) - it("put-put", () => expect(structEq(l.set(value2, l.set(value1, object)), l.set(value2, object))).toBeTruthy()) - }) + describe(`lens laws: ${name}`, () => { + it("get-put", () => expect(structEq(object, l.set(l.get(object), object))).toBeTruthy()) + it("put-get", () => expect(structEq(value1, l.get(l.set(value1, object)))).toBeTruthy()) + it("put-put", () => expect(structEq(l.set(value2, l.set(value1, object)), l.set(value2, object))).toBeTruthy()) + }) } -function testLens(name: string, l: Lens, obj: O, currentValue: P, newValue1: P, newValue2: P) { - testLaws(l, obj, newValue1, newValue2, name) - roundtrip(name, l, obj, currentValue, newValue1) +function testLens(name: string, l: Lens, obj: O, currentValue: P, nextValue: P, nextValue2: P) { + testLaws(l, obj, nextValue, nextValue2, name) + roundtrip(name, l, obj, currentValue, nextValue) } describe("identity", () => { - testLens("basic", Lens.identity(), "any", "any", "other", "another") + testLens("basic", Lens.identity(), "any", "any", "other", "another") - testLens("composed", Lens.identity(), "any", "any", "other", "another") + testLens("composed", Lens.identity(), "any", "any", "other", "another") }) describe("json", () => { - describe("key lenses are cached", () => { - const a1 = Lens.key("a") - const a2 = Lens.key("a") - const a3 = Lens.key()("a") - const b = Lens.key("b") - expect(a1).toStrictEqual(a2) - expect(a2).toStrictEqual(a3) - expect(a1).not.toStrictEqual(b) - }) - - describe("index lenses are cached", () => { - const a1 = Lens.index(0) - const a2 = Lens.index(0) - const a3 = Lens.index(1) - expect(a1).toStrictEqual(a2) - expect(a2).not.toStrictEqual(a3) - }) - - describe("simple", () => { - const a = Lens.key("a") - const b = Lens.key("b") - const c = Lens.key("c") - const i0 = Lens.index(0) - const i1 = Lens.index(1) - - testLens("keys", a, { a: "one" }, "one", "two", "three") - - testLens("indices", i0, ["one"], "one", "two", "three") - - testLens( - "composed", - a.compose(i0).compose(b).compose(i1).compose(c), - { a: [{ b: ["boo", { c: "one" }] }] }, - "one", - "two", - "three" - ) - - testLens( - "composed, right associative", - a.compose(i0.compose(b.compose(i1.compose(c)))), - { a: [{ b: ["boo", { c: "one" }] }] }, - "one", - "two", - "three" - ) - - testLens( - "composed with Lens.compose", - Lens.compose(a, i0, b, i1, c), - { a: [{ b: ["boo", { c: "one" }] }] }, - "one", - "two", - "three" - ) - }) - - describe("typed", () => { - interface Leg { - length: string - } - interface Raccoon { - legs: Leg[] - } - interface Forest { - raccoons: Raccoon[] - } - - const forest: Forest = { - raccoons: [ - { legs: [{ length: "short" }, { length: "long" }] }, - { legs: [{ length: "fat" }, { length: "thick" }] }, - ], - } - - const raccoons = Lens.key()("raccoons") - const legs = Lens.key()("legs") - const length = Lens.key()("length") - - testLens( - "case 1", - raccoons.compose(Lens.index(0)).compose(legs).compose(Lens.index(0)).compose(length), - forest, - "short", - "bold", - "cursive" - ) - - testLens( - "case 2", - raccoons.compose(Lens.index(1)).compose(legs).compose(Lens.index(1)).compose(length), - forest, - "thick", - "broken", - "beautiful" - ) - - testLens( - "compose", - Lens.compose(raccoons, Lens.index(0), legs, Lens.index(1), length), - forest, - "long", - "metal", - "deus ex" - ) - }) - - describe("find", () => { - const xs = [1, 2, 3, 4, 5] - const l = Lens.find((x: number) => x === 3) - - const oldVal = 3 - const newVal = 5 - - it("get", () => expect(l.get(xs)).toEqual(oldVal)) - it("set", () => expect(l.get(l.set(newVal, xs))).toEqual(undefined)) - }) - - describe("withDefault", () => { - const s = { a: 5, b: 6 } // c is undefined - const l1 = Lens.key("a").compose(Lens.withDefault(666)) - const l2 = Lens.key("c").compose(Lens.withDefault(666)) - - it("get defined", () => expect(l1.get(s)).toEqual(5)) - it("set defined", () => expect(l1.set(6, s)).toEqual({ a: 6, b: 6 })) - it("get undefined", () => expect(l2.get(s)).toEqual(666)) - it("set undefined", () => expect(l2.set(6, s)).toEqual({ a: 5, b: 6, c: 6 })) - }) - - describe("withDefault transforms Prism into Lens", () => { - const s = { a: 5, b: 6 } // c is undefined - const l1 = Lens.key("a").compose(Lens.withDefault(666)) - const l2 = Lens.key("c").compose(Lens.withDefault(666)) - - // the lines below should compile - let _: number = l1.get(s) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _ = l2.get(s) - }) - - describe("type safe key", () => { - const s = { a: 5, b: "6" } - - testLens("type safe key 1", Lens.key()("a"), s, 5, 6, 7) - - testLens("type safe key 2", Lens.key()("b"), s, "6", "7", "hello") - }) + describe("key lenses are cached", () => { + const a1 = Lens.key("a") + const a2 = Lens.key("a") + const a3 = Lens.key()("a") + const b = Lens.key("b") + expect(a1).toStrictEqual(a2) + expect(a2).toStrictEqual(a3) + expect(a1).not.toStrictEqual(b) + }) + + describe("index lenses are cached", () => { + const a1 = Lens.index(0) + const a2 = Lens.index(0) + const a3 = Lens.index(1) + expect(a1).toStrictEqual(a2) + expect(a2).not.toStrictEqual(a3) + }) + + describe("simple", () => { + const a = Lens.key("a") + const b = Lens.key("b") + const c = Lens.key("c") + const i0 = Lens.index(0) + const i1 = Lens.index(1) + + testLens("keys", a, { a: "one" }, "one", "two", "three") + + testLens("indices", i0, ["one"], "one", "two", "three") + + testLens( + "composed", + a.compose(i0).compose(b).compose(i1).compose(c), + { a: [{ b: ["boo", { c: "one" }] }] }, + "one", + "two", + "three", + ) + + testLens( + "composed, right associative", + a.compose(i0.compose(b.compose(i1.compose(c)))), + { a: [{ b: ["boo", { c: "one" }] }] }, + "one", + "two", + "three", + ) + + testLens( + "composed with Lens.compose", + Lens.compose(a, i0, b, i1, c), + { a: [{ b: ["boo", { c: "one" }] }] }, + "one", + "two", + "three", + ) + }) + + describe("typed", () => { + interface Leg { + length: string + } + interface Raccoon { + legs: Leg[] + } + interface Forest { + raccoons: Raccoon[] + } + + const forest: Forest = { + raccoons: [ + { legs: [{ length: "short" }, { length: "long" }] }, + { legs: [{ length: "fat" }, { length: "thick" }] }, + ], + } + + const raccoons = Lens.key()("raccoons") + const legs = Lens.key()("legs") + const length = Lens.key()("length") + + testLens( + "case 1", + raccoons.compose(Lens.index(0)).compose(legs).compose(Lens.index(0)).compose(length), + forest, + "short", + "bold", + "cursive", + ) + + testLens( + "case 2", + raccoons.compose(Lens.index(1)).compose(legs).compose(Lens.index(1)).compose(length), + forest, + "thick", + "broken", + "beautiful", + ) + + testLens( + "compose", + Lens.compose(raccoons, Lens.index(0), legs, Lens.index(1), length), + forest, + "long", + "metal", + "deus ex", + ) + }) + + describe("find", () => { + const xs = [1, 2, 3, 4, 5] + const l = Lens.find((x: number) => x === 3) + + const oldVal = 3 + const nextValue = 5 + + it("get", () => expect(l.get(xs)).toEqual(oldVal)) + it("set", () => expect(l.get(l.set(nextValue, xs))).toEqual(undefined)) + }) + + describe("withDefault", () => { + const s = { a: 5, b: 6 } // c is undefined + const l1 = Lens.key("a").compose(Lens.withDefault(666)) + const l2 = Lens.key("c").compose(Lens.withDefault(666)) + + it("get defined", () => expect(l1.get(s)).toEqual(5)) + it("set defined", () => expect(l1.set(6, s)).toEqual({ a: 6, b: 6 })) + it("get undefined", () => expect(l2.get(s)).toEqual(666)) + it("set undefined", () => expect(l2.set(6, s)).toEqual({ a: 5, b: 6, c: 6 })) + }) + + describe("withDefault transforms Prism into Lens", () => { + const s = { a: 5, b: 6 } // c is undefined + const l1 = Lens.key("a").compose(Lens.withDefault(666)) + const l2 = Lens.key("c").compose(Lens.withDefault(666)) + + // the lines below should compile + let _: number = l1.get(s) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _ = l2.get(s) + }) + + describe("type safe key", () => { + const s = { a: 5, b: "6" } + + testLens("type safe key 1", Lens.key()("a"), s, 5, 6, 7) + + testLens("type safe key 2", Lens.key()("b"), s, "6", "7", "hello") + }) }) diff --git a/packages/lens/src/simple-cache.ts b/packages/lens/src/simple-cache.ts index 94aaf45..1455f36 100644 --- a/packages/lens/src/simple-cache.ts +++ b/packages/lens/src/simple-cache.ts @@ -1,16 +1,16 @@ export class SimpleCache { - private readonly map = new Map() + private readonly map = new Map() - constructor(private readonly factory: (key: K) => V) {} + constructor(private readonly factory: (key: K) => V) {} - getOrCreate(key: K, onCreate?: (next: V) => void): V { - const cached = this.map.get(key) - if (cached !== undefined) { - return cached - } - const created = this.factory(key) - this.map.set(key, created) - onCreate?.(created) - return created - } + getOrCreate(key: K, onCreate?: (next: V) => void): V { + const cached = this.map.get(key) + if (cached !== undefined) { + return cached + } + const created = this.factory(key) + this.map.set(key, created) + onCreate?.(created) + return created + } } diff --git a/packages/lens/src/utils.ts b/packages/lens/src/utils.ts index 8173d46..a88f4ac 100644 --- a/packages/lens/src/utils.ts +++ b/packages/lens/src/utils.ts @@ -2,24 +2,24 @@ import { equals as structEq } from "./equals" export { equals as structEq } from "./equals" export function setKey(k: K, v: T[K], o: T): T { - if (k in o && structEq(v, o[k])) { - return o - } else { - // this is the fastest way to do it, see - // https://jsperf.com/focal-setkey-for-loop-vs-object-assign - const r: { [k in keyof T]: T[k] } = {} as any - for (const p in o) r[p] = o[p] - r[k] = v + if (k in o && structEq(v, o[k])) { + return o + } else { + // this is the fastest way to do it, see + // https://jsperf.com/focal-setkey-for-loop-vs-object-assign + const r: { [k in keyof T]: T[k] } = {} as any + for (const p in o) r[p] = o[p] + r[k] = v - return r - } + return r + } } /** * 'Conserve' a value's identity if its structure doesn't change. */ function conserve(x: T, y: T): T { - return structEq(x, y) ? y : x + return structEq(x, y) ? y : x } /** @@ -27,14 +27,14 @@ function conserve(x: T, y: T): T { * identity. */ export function conservatively(fn: (y: T, c0: U) => U) { - return (y: T, c0: U) => conserve(fn(y, c0), c0) + return (y: T, c0: U) => conserve(fn(y, c0), c0) } export function findIndex(xs: T[], p: (x: T) => boolean): number { - for (let i = 0; i < xs.length; i++) { - if (p(xs[i])) return i - } - return -1 + for (let i = 0; i < xs.length; i++) { + if (p(xs[i])) return i + } + return -1 } export type Option = T | undefined diff --git a/packages/list-react/src/domain.ts b/packages/list-react/src/domain.ts index 980688d..a6e4a5d 100644 --- a/packages/list-react/src/domain.ts +++ b/packages/list-react/src/domain.ts @@ -1,9 +1,9 @@ -import React from "react" +import type React from "react" export type ListReactRenderer = ( - item: T, - measure: () => void, - index: number, - isScrolling: boolean + item: T, + measure: () => void, + index: number, + isScrolling: boolean, ) => React.ReactNode export type GridReactRenderer = (item: T, index: number, isScrolling: boolean) => React.ReactNode diff --git a/packages/list-react/src/grid/index.stories.tsx b/packages/list-react/src/grid/index.stories.tsx index a89d4e2..fa6020e 100644 --- a/packages/list-react/src/grid/index.stories.tsx +++ b/packages/list-react/src/grid/index.stories.tsx @@ -2,61 +2,63 @@ import "react-virtualized/styles.css" import React from "react" import { storiesOf } from "@storybook/react" import { Atom } from "@rixio/atom" -import { InfiniteList, InfiniteListState, ListItem, listStateIdle } from "@rixio/list" -import { GridReactRenderer } from "../domain" -import { GridRect, RxGridList, RxGridListWindow } from "./index" +import type { InfiniteListState, ListItem } from "@rixio/list" +import { InfiniteList, listStateIdle } from "@rixio/list" +import type { GridReactRenderer } from "../domain" +import type { GridRect } from "./index" +import { RxGridList, RxGridListWindow } from "./index" const delay = (timeout: number) => new Promise(r => setTimeout(r, timeout)) const items = new Array(100).fill(1).map((_, i) => i) async function load(pageSize: number, c: number | null): Promise<[number[], number]> { - await delay(1000) - const current = c || 0 - return [items.slice(current, current + pageSize), current + pageSize] + await delay(1000) + const current = c || 0 + return [items.slice(current, current + pageSize), current + pageSize] } const state$ = Atom.create>(listStateIdle) const list$ = new InfiniteList(state$, load, 20, { initial: "wrapped" }) const Comp = ({ item, index }: { index: number; item: ListItem }) => { - if (item) { - if (item.type === "item") { - return ( -
-

- {item.value}-{index} -

-
- ) - } - if (item.type === "pending") { - return
Loading..
- } - } - return null + if (item) { + if (item.type === "item") { + return ( +
+

+ {item.value}-{index} +

+
+ ) + } + if (item.type === "pending") { + return
Loading..
+ } + } + return null } const renderer: GridReactRenderer> = (item, index) => { - return + return } const rect: GridRect = { - rowHeight: 300, - columnCount: 5, - gap: 16, - height: 500, - width: 1000, + rowHeight: 300, + columnCount: 5, + gap: 16, + height: 500, + width: 1000, } storiesOf("grid-window-list", module) - .add("basic", () => ( - - -
Content from bottom
-
- )) - .add("with window-scroller", () => ( - - -
Content from bottom
-
- )) + .add("basic", () => ( + + +
Content from bottom
+
+ )) + .add("with window-scroller", () => ( + + +
Content from bottom
+
+ )) diff --git a/packages/list-react/src/grid/index.tsx b/packages/list-react/src/grid/index.tsx index 6300a42..ad8dbbd 100644 --- a/packages/list-react/src/grid/index.tsx +++ b/packages/list-react/src/grid/index.tsx @@ -1,199 +1,203 @@ -import React, { CSSProperties, memo, useCallback, useMemo, useRef } from "react" +import type { CSSProperties } from "react" +import React, { memo, useCallback, useMemo, useRef } from "react" import { isFakeItem } from "@rixio/list" -import { InfiniteLoader, InfiniteLoaderProps } from "react-virtualized/dist/es/InfiniteLoader" -import { - Grid, - GridCellRenderer, - GridProps, - OverscanIndicesGetter, - RenderedSection, +import type { InfiniteLoaderProps } from "react-virtualized/dist/es/InfiniteLoader" +import { InfiniteLoader } from "react-virtualized/dist/es/InfiniteLoader" +import type { + GridCellRenderer, + GridProps, + OverscanIndicesGetter, + RenderedSection, } from "react-virtualized/dist/es/Grid" -import { WindowScroller, WindowScrollerChildProps } from "react-virtualized/dist/es/WindowScroller" +import { Grid } from "react-virtualized/dist/es/Grid" +import type { WindowScrollerChildProps } from "react-virtualized/dist/es/WindowScroller" +import { WindowScroller } from "react-virtualized/dist/es/WindowScroller" import type { Index, IndexRange } from "react-virtualized" import type { GridReactRenderer } from "../domain" -import { liftReactList, RxReactListProps } from "../rx" +import type { RxReactListProps } from "../rx" +import { liftReactList } from "../rx" import { identity } from "../utils" export type GridRect = { - rowHeight: number - columnCount: number - gap: number - height: number - width: number + rowHeight: number + columnCount: number + gap: number + height: number + width: number } export type GridListProps = Partial> & { - renderer: GridReactRenderer - data: T[] - minimumBatchRequest?: number - rect: GridRect - gridProps?: Partial - pendingSize?: number - loadNext: () => void - mapKey?: (key: string) => string + renderer: GridReactRenderer + data: T[] + minimumBatchRequest?: number + rect: GridRect + gridProps?: Partial + pendingSize?: number + loadNext: () => void + mapKey?: (key: string) => string } export function GridList({ - mapKey = identity, - data, - rect, - minimumBatchRequest = 10, - renderer, - gridProps = {}, - threshold = 3, - loadNext, + mapKey = identity, + data, + rect, + minimumBatchRequest = 10, + renderer, + gridProps = {}, + threshold = 3, + loadNext, }: GridListProps) { - const onSectionRendered = useRef<(r: RenderedSection) => void>() - const rowCount = useMemo(() => Math.ceil(data.length / rect.columnCount), [data.length, rect.columnCount]) - - const isRowLoaded = useCallback( - ({ index }: Index) => { - const rowStart = rect.columnCount * index - return rowStart < data.length && !isFakeItem(data[rowStart]) - }, - [data, rect.columnCount] - ) - - const loadMoreRows = useCallback<(params: IndexRange) => Promise>(async () => loadNext(), [loadNext]) - const cellRenderer = useCallback( - ({ key, ...restCellProps }) => ( - - ), - [data, mapKey, rect.columnCount, rect.gap, renderer] - ) - - const { onScroll, ...restGridProps } = gridProps - - return ( - - - {({ registerChild, onRowsRendered }) => { - if (!onSectionRendered.current) { - onSectionRendered.current = (r: RenderedSection) => - onRowsRendered({ - startIndex: r.rowStartIndex, - stopIndex: r.rowStopIndex, - }) - } - return ( - 0 ? onScroll : undefined} - {...restGridProps} - overscanIndicesGetter={overscanIndicesGetter} - ref={registerChild} - onSectionRendered={onSectionRendered.current} - /> - ) - }} - - - ) + const onSectionRendered = useRef<(r: RenderedSection) => void>() + const rowCount = useMemo(() => Math.ceil(data.length / rect.columnCount), [data.length, rect.columnCount]) + + const isRowLoaded = useCallback( + ({ index }: Index) => { + const rowStart = rect.columnCount * index + return rowStart < data.length && !isFakeItem(data[rowStart]) + }, + [data, rect.columnCount], + ) + + const loadMoreRows = useCallback<(params: IndexRange) => Promise>(async () => loadNext(), [loadNext]) + const cellRenderer = useCallback( + ({ key, ...restCellProps }) => ( + + ), + [data, mapKey, rect.columnCount, rect.gap, renderer], + ) + + const { onScroll, ...restGridProps } = gridProps + + return ( + + + {({ registerChild, onRowsRendered }) => { + if (!onSectionRendered.current) { + onSectionRendered.current = (r: RenderedSection) => + onRowsRendered({ + startIndex: r.rowStartIndex, + stopIndex: r.rowStopIndex, + }) + } + return ( + 0 ? onScroll : undefined} + {...restGridProps} + overscanIndicesGetter={overscanIndicesGetter} + ref={registerChild} + onSectionRendered={onSectionRendered.current} + /> + ) + }} + + + ) } const overscanIndicesGetter: OverscanIndicesGetter = ({ - cellCount, // Number of rows or columns in the current axis - overscanCellsCount, // Maximum number of cells to over-render in either direction - startIndex, // Begin of range of visible cells - stopIndex, // End of range of visible cells + cellCount, // Number of rows or columns in the current axis + overscanCellsCount, // Maximum number of cells to over-render in either direction + startIndex, // Begin of range of visible cells + stopIndex, // End of range of visible cells }) => ({ - overscanStartIndex: Math.max(0, startIndex - overscanCellsCount), - overscanStopIndex: Math.min(cellCount - 1, stopIndex + overscanCellsCount), + overscanStartIndex: Math.max(0, startIndex - overscanCellsCount), + overscanStopIndex: Math.min(cellCount - 1, stopIndex + overscanCellsCount), }) export type RxGridListProps = RxReactListProps> -export const RxGridList: (props: RxGridListProps) => JSX.Element = liftReactList(GridList) as any +export const RxGridList: (props: RxGridListProps) => React.ReactElement = liftReactList(GridList) as any export type GridWindowRect = Omit export type GridListWindowProps = Omit, "rect"> & { - rect: GridWindowRect + rect: GridWindowRect } export function GridListWindow(props: GridListWindowProps) { - return {childProps => } + return {childProps => } } export type RxGridListWindowProps = RxReactListProps> -export const RxGridListWindow: (props: RxGridListWindowProps) => JSX.Element = liftReactList( - GridListWindow +export const RxGridListWindow: (props: RxGridListWindowProps) => React.ReactElement = liftReactList( + GridListWindow, ) as any type GridListWindowChildProps = GridListWindowProps & - WindowScrollerChildProps & { - height: number - } + WindowScrollerChildProps & { + height: number + } function GridListWindowChild(props: GridListWindowChildProps) { - const { height, rect, isScrolling, scrollTop, onChildScroll, gridProps = {}, ...restProps } = props - const finalRect = useMemo(() => ({ ...rect, height }), [height, rect]) - - const finalGridProps = useMemo( - () => ({ - ...gridProps, - autoHeight: true, - isScrolling, - scrollTop, - onScroll: onChildScroll, - }), - [isScrolling, scrollTop, onChildScroll, gridProps] - ) - - return + const { height, rect, isScrolling, scrollTop, onChildScroll, gridProps = {}, ...restProps } = props + const finalRect = useMemo(() => ({ ...rect, height }), [height, rect]) + + const finalGridProps = useMemo( + () => ({ + ...gridProps, + autoHeight: true, + isScrolling, + scrollTop, + onScroll: onChildScroll, + }), + [isScrolling, scrollTop, onChildScroll, gridProps], + ) + + return } type GridListCellProps = { - rowIndex: number - columnIndex: number - style: CSSProperties - columnCount: number - renderer: GridReactRenderer - gap: number - data: T[] - isScrolling: boolean + rowIndex: number + columnIndex: number + style: CSSProperties + columnCount: number + renderer: GridReactRenderer + gap: number + data: T[] + isScrolling: boolean } const GridListCell = memo(function GridListCell(props: GridListCellProps) { - const { renderer, rowIndex, columnCount, columnIndex, style, gap, data, isScrolling } = props - const index = rowIndex * columnCount + columnIndex - const finalStyle = useMemo( - () => ({ - position: "absolute", - width: style.width, - height: style.height, - top: style.top, - left: style.left, - ...getStylesWithGap(gap, columnIndex, columnCount), - }), - [columnCount, columnIndex, gap, style] - ) - const item = data[index] - const children = useMemo(() => renderer(item, index, isScrolling), [item, index, renderer, isScrolling]) - return
+ const { renderer, rowIndex, columnCount, columnIndex, style, gap, data, isScrolling } = props + const index = rowIndex * columnCount + columnIndex + const finalStyle = useMemo( + () => ({ + position: "absolute", + width: style.width, + height: style.height, + top: style.top, + left: style.left, + ...getStylesWithGap(gap, columnIndex, columnCount), + }), + [columnCount, columnIndex, gap, style], + ) + const item = data[index] + const children = useMemo(() => renderer(item, index, isScrolling), [item, index, renderer, isScrolling]) + return
}) const getStylesWithGap = (gap: number, col: number, columnCount: number) => { - const halfGap = gap / 2 - return { - paddingLeft: col !== 0 ? halfGap : 0, - paddingRight: col !== columnCount - 1 ? halfGap : 0, - paddingTop: halfGap, - paddingBottom: halfGap, - } + const halfGap = gap / 2 + return { + paddingLeft: col !== 0 ? halfGap : 0, + paddingRight: col !== columnCount - 1 ? halfGap : 0, + paddingTop: halfGap, + paddingBottom: halfGap, + } } diff --git a/packages/list-react/src/index.ts b/packages/list-react/src/index.ts index 0335d5c..42a3168 100644 --- a/packages/list-react/src/index.ts +++ b/packages/list-react/src/index.ts @@ -1,10 +1,10 @@ export { GridList, GridListWindow, GridListProps, GridListWindowProps, RxGridList, RxGridListWindow } from "./grid" export { - VerticalList, - VerticalListWindow, - VerticalListProps, - VerticalListWindowProps, - RxVerticalList, - RxVerticalListWindow, + VerticalList, + VerticalListWindow, + VerticalListProps, + VerticalListWindowProps, + RxVerticalList, + RxVerticalListWindow, } from "./vertical" export { ListReactRenderer, GridReactRenderer } from "./domain" diff --git a/packages/list-react/src/rx.tsx b/packages/list-react/src/rx.tsx index 0427472..ff839d6 100644 --- a/packages/list-react/src/rx.tsx +++ b/packages/list-react/src/rx.tsx @@ -1,52 +1,55 @@ -import { RxPropsBase, useRx } from "@rixio/react" -import { OWLike } from "@rixio/wrapped" +import type { RxPropsBase } from "@rixio/react" +import { useRx } from "@rixio/react" +import type { OWLike } from "@rixio/wrapped" import React, { useCallback, useState } from "react" export type BaseListProps = { - data: T[] - loadNext: () => void + data: T[] + loadNext: () => void } type InferItemType> = Props extends BaseListProps ? T : never export type RxReactListProps> = Omit & - RxPropsBase & { - data$: OWLike>> - } + RxPropsBase & { + data$: OWLike>> + } export function liftReactList>(Component: React.ComponentType) { - return function LiftedList({ data$, pending, rejected, ...rest }: RxReactListProps, Props>) { - const [nonce, setNonce] = useState(0) - const data = useRx(data$, [data$, nonce]) - const loadNext = useCallback(() => { - if ("loadNext" in data$) { - ;(data$ as any).loadNext() - } - }, [data$]) + return function LiftedList({ data$, pending, rejected, ...rest }: RxReactListProps, Props>) { + const [nonce, setNonce] = useState(0) + const data = useRx(data$, [data$, nonce]) + const loadNext = useCallback(() => { + if ("loadNext" in data$) { + ;(data$ as any).loadNext() + } + }, [data$]) - switch (data.status) { - case "fulfilled": - // @ts-ignore - return - case "pending": - if (pending) { - return pending - } else { - // @ts-ignore - return - } - case "rejected": - if (typeof rejected === "function") { - return rejected(data.error, () => { - data.reload() - setNonce(n => n + 1) - }) - } else if (rejected) { - return rejected - } else { - // @ts-ignore - return - } - } - } + switch (data.status) { + case "fulfilled": + // @ts-ignore + return + case "pending": + if (pending) { + return pending + } else { + // @ts-ignore + return + } + case "rejected": + if (typeof rejected === "function") { + return rejected(data.error, () => { + data.reload() + setNonce(n => n + 1) + }) + } else if (rejected) { + return rejected + } else { + // @ts-ignore + return + } + default: + return null + } + } } diff --git a/packages/list-react/src/utils.ts b/packages/list-react/src/utils.ts index e669d35..91b6ef1 100644 --- a/packages/list-react/src/utils.ts +++ b/packages/list-react/src/utils.ts @@ -1,3 +1,3 @@ export function identity(x: T): T { - return x + return x } diff --git a/packages/list-react/src/vertical/index.stories.tsx b/packages/list-react/src/vertical/index.stories.tsx index d6bd815..ec8e5d1 100644 --- a/packages/list-react/src/vertical/index.stories.tsx +++ b/packages/list-react/src/vertical/index.stories.tsx @@ -2,86 +2,87 @@ import "react-virtualized/styles.css" import React, { useEffect } from "react" import { storiesOf } from "@storybook/react" import { Atom } from "@rixio/atom" -import { InfiniteList, InfiniteListState, ListItem, listStateIdle } from "@rixio/list" -import { ListReactRenderer } from "../domain" +import type { InfiniteListState, ListItem } from "@rixio/list" +import { InfiniteList, listStateIdle } from "@rixio/list" +import type { ListReactRenderer } from "../domain" import { RxVerticalList, RxVerticalListWindow } from "./index" const delay = (timeout: number) => new Promise(r => setTimeout(r, timeout)) function randomNumber(min: number, max: number) { - const r = Math.random() * (max - min) + min - return Math.floor(r) + const r = Math.random() * (max - min) + min + return Math.floor(r) } type Item = { - index: number - height: number + index: number + height: number } const items = new Array(100).fill(1).map((_, i) => ({ - index: i, - height: randomNumber(100, 300), + index: i, + height: randomNumber(100, 300), })) as Item[] async function load(pageSize: number, c: number | null): Promise<[Item[], number]> { - await delay(1500) - const current = c || 0 - return [items.slice(current, current + pageSize), current + pageSize] + await delay(1500) + const current = c || 0 + return [items.slice(current, current + pageSize), current + pageSize] } const state$ = Atom.create>(listStateIdle) const list$ = new InfiniteList(state$, load, 20, { initial: "fake" }) type BlockProps = { - height: number | string - children: React.ReactNode + height: number | string + children: React.ReactNode } const Block = ({ height, children }: BlockProps) => { - return
{children}
+ return
{children}
} type Props = { - item: ListItem - isScrolling: boolean - onRender: () => void - index: number + item: ListItem + isScrolling: boolean + onRender: () => void + index: number } const Comp = ({ item, ...rest }: Props) => { - if (item && item.type === "item") { - return - } - return + if (item && item.type === "item") { + return + } + return } type LoadedProps = Omit & { - item: Item + item: Item } const Loaded = ({ item, onRender, index, isScrolling }: LoadedProps) => { - useEffect(() => { - onRender() - }, [onRender]) + useEffect(() => { + onRender() + }, [onRender]) - return ( - - {isScrolling ? "scrolling" : `${item.height}-${index}`} - - ) + return ( + + {isScrolling ? "scrolling" : `${item.height}-${index}`} + + ) } const renderer: ListReactRenderer> = (item, onMeasure, index, isScrolling) => { - return + return } const rect = { - width: 500, - minRowHeight: 100, - height: 400, - gap: 32, + width: 500, + minRowHeight: 100, + height: 400, + gap: 32, } storiesOf("vertical-list", module) - .add("basic", () => ( - - )) - .add("with window-scroller", () => ( - - )) + .add("basic", () => ( + + )) + .add("with window-scroller", () => ( + + )) const rejected = () =>
Some error
const pending =
First load
diff --git a/packages/list-react/src/vertical/index.tsx b/packages/list-react/src/vertical/index.tsx index 8f277a6..3e7bb2a 100644 --- a/packages/list-react/src/vertical/index.tsx +++ b/packages/list-react/src/vertical/index.tsx @@ -1,158 +1,160 @@ import React, { useCallback, useMemo } from "react" import type { Index, WindowScrollerChildProps, WindowScrollerProps } from "react-virtualized" -import { InfiniteLoader, InfiniteLoaderProps } from "react-virtualized/dist/es/InfiniteLoader" -import { List, ListProps, ListRowProps } from "react-virtualized/dist/es/List" +import type { InfiniteLoaderProps } from "react-virtualized/dist/es/InfiniteLoader" +import { InfiniteLoader } from "react-virtualized/dist/es/InfiniteLoader" +import type { ListProps, ListRowProps } from "react-virtualized/dist/es/List" +import { List } from "react-virtualized/dist/es/List" import { CellMeasurerCache, CellMeasurer } from "react-virtualized/dist/es/CellMeasurer" import { WindowScroller } from "react-virtualized/dist/es/WindowScroller" import { isFakeItem } from "@rixio/list" import type { ListReactRenderer } from "../domain" -import { liftReactList, RxReactListProps } from "../rx" +import type { RxReactListProps } from "../rx" +import { liftReactList } from "../rx" import { identity } from "../utils" export type VerticalListRect = { - width: number - height: number - minRowHeight: number - gap: number + width: number + height: number + minRowHeight: number + gap: number } export type VerticalListProps = Partial> & { - renderer: ListReactRenderer - data: T[] - minimumBatchRequest?: number - rect: VerticalListRect - listProps?: Partial - loadNext: () => void - mapKey?: (key: string) => string + renderer: ListReactRenderer + data: T[] + minimumBatchRequest?: number + rect: VerticalListRect + listProps?: Partial + loadNext: () => void + mapKey?: (key: string) => string } export function VerticalList(props: VerticalListProps) { - const { mapKey = identity, data, rect, minimumBatchRequest, renderer, listProps = {}, loadNext, ...restProps } = props - const isRowLoaded = useCallback(({ index }: Index) => index < data.length && !isFakeItem(data[index]), [data]) - const loadMoreRows = useCallback(() => Promise.resolve(loadNext()), [loadNext]) - const cellMeasurerCache = useMemo( - () => - new CellMeasurerCache({ - fixedWidth: true, - minHeight: rect.minRowHeight, - defaultHeight: rect.minRowHeight, - defaultWidth: rect.width, - }), - [rect.minRowHeight, rect.width] - ) + const { mapKey = identity, data, rect, minimumBatchRequest, renderer, listProps = {}, loadNext, ...restProps } = props + const isRowLoaded = useCallback(({ index }: Index) => index < data.length && !isFakeItem(data[index]), [data]) + const loadMoreRows = useCallback(() => Promise.resolve(loadNext()), [loadNext]) + const cellMeasurerCache = useMemo( + () => + new CellMeasurerCache({ + fixedWidth: true, + minHeight: rect.minRowHeight, + defaultHeight: rect.minRowHeight, + defaultWidth: rect.width, + }), + [rect.minRowHeight, rect.width], + ) - const rowRenderer = useCallback( - ({ key, ...rest }: ListRowProps) => ( - - {...rest} - key={mapKey(key)} - rowCount={data.length} - gap={rect.gap} - cellMeasurerCache={cellMeasurerCache} - renderer={renderer} - data={data} - /> - ), - [data, mapKey, rect.gap, renderer, cellMeasurerCache] - ) + const rowRenderer = useCallback( + ({ key, ...rest }: ListRowProps) => ( + + {...rest} + key={mapKey(key)} + rowCount={data.length} + gap={rect.gap} + cellMeasurerCache={cellMeasurerCache} + renderer={renderer} + data={data} + /> + ), + [data, mapKey, rect.gap, renderer, cellMeasurerCache], + ) - return ( - - {({ registerChild, onRowsRendered, ...rest }) => ( - - )} - - ) + return ( + + {({ registerChild, onRowsRendered, ...rest }) => ( + + )} + + ) } export type VerticalListWindowRect = Omit export type VerticalListWindowProps = Omit, "rect"> & { - rect: VerticalListWindowRect - onRef?(ref: WindowScroller): void - windowScrollerProps?: Partial + rect: VerticalListWindowRect + onRef?(ref: WindowScroller): void + windowScrollerProps?: Partial } export function VerticalListWindow(props: VerticalListWindowProps) { - return ( - - {childProps => } - - ) + return ( + + {childProps => } + + ) } export type RxVerticalListWindowProps = RxReactListProps> -export const RxVerticalListWindow: (props: RxVerticalListWindowProps) => JSX.Element | null = liftReactList( - VerticalListWindow -) as any +export const RxVerticalListWindow: (props: RxVerticalListWindowProps) => React.ReactElement | null = + liftReactList(VerticalListWindow) as any type VerticalListWindowChildProps = VerticalListWindowProps & - WindowScrollerChildProps & { - height: number - } + WindowScrollerChildProps & { + height: number + } function VerticalListWindowChild(props: VerticalListWindowChildProps) { - const { height, rect, isScrolling, scrollTop, onChildScroll, listProps = {}, ...restProps } = props - const finalRect = useMemo(() => ({ ...rect, height }), [height, rect]) + const { height, rect, isScrolling, scrollTop, onChildScroll, listProps = {}, ...restProps } = props + const finalRect = useMemo(() => ({ ...rect, height }), [height, rect]) - const finalListProps = useMemo( - () => ({ - ...listProps, - autoHeight: true, - isScrolling, - scrollTop, - onScroll: onChildScroll, - }), - [isScrolling, scrollTop, onChildScroll, listProps] - ) + const finalListProps = useMemo( + () => ({ + ...listProps, + autoHeight: true, + isScrolling, + scrollTop, + onScroll: onChildScroll, + }), + [isScrolling, scrollTop, onChildScroll, listProps], + ) - return + return } export type VerticalListRowProps = ListRowProps & { - data: T[] - cellMeasurerCache: CellMeasurerCache - renderer: ListReactRenderer - gap: number - rowCount: number + data: T[] + cellMeasurerCache: CellMeasurerCache + renderer: ListReactRenderer + gap: number + rowCount: number } export function VerticalListRow(props: VerticalListRowProps) { - const { cellMeasurerCache, rowCount, renderer, parent, gap, data, index, style, isScrolling } = props - return ( - - {({ measure }) => ( -
- {renderer(data[index], measure, index, isScrolling)} -
- )} -
- ) + const { cellMeasurerCache, rowCount, renderer, parent, gap, data, index, style, isScrolling } = props + return ( + + {({ measure }) => ( +
+ {renderer(data[index], measure, index, isScrolling)} +
+ )} +
+ ) } export type RxVerticalListProps = RxReactListProps> -export const RxVerticalList: (props: RxVerticalListProps) => JSX.Element | null = liftReactList( - VerticalList +export const RxVerticalList: (props: RxVerticalListProps) => React.ReactElement | null = liftReactList( + VerticalList, ) as any const getStylesWithGap = (styles: Object, gap: number, row: number, rowCount: number) => { - return { - ...styles, - paddingBottom: row === rowCount - 1 ? 0 : gap, - } + return { + ...styles, + paddingBottom: row === rowCount - 1 ? 0 : gap, + } } diff --git a/packages/list/src/domain.ts b/packages/list/src/domain.ts index 74e1952..e062a08 100644 --- a/packages/list/src/domain.ts +++ b/packages/list/src/domain.ts @@ -1,33 +1,33 @@ -import React from "react" +import type React from "react" import type { OrReactChild } from "@rixio/react" -import { BaseInfiniteList } from "./infinite-list" +import type { BaseInfiniteList } from "./infinite-list" export type ListPartLoader = (size: number, continuation: C | null) => Promise<[T[], C | null]> type InfiniteListStateBase = - | { - status: "pending" | "fulfilled" | "idle" - } - | { - status: "rejected" - error: unknown - } + | { + status: "pending" | "fulfilled" | "idle" + } + | { + status: "rejected" + error: unknown + } export type InfiniteListState = InfiniteListStateBase & { - items: T[] - continuation: C | null - finished: boolean + items: T[] + continuation: C | null + finished: boolean } export const listStateIdle: InfiniteListState = { - status: "idle", - items: [], - continuation: null, - finished: false, + status: "idle", + items: [], + continuation: null, + finished: false, } export interface InfiniteListPropsShared { - list$: BaseInfiniteList - pending?: React.ReactNode - rejected?: OrReactChild<(error: any, reload: () => Promise) => React.ReactNode> + list$: BaseInfiniteList + pending?: React.ReactNode + rejected?: OrReactChild<(error: any, reload: () => Promise) => React.ReactNode> } diff --git a/packages/list/src/index.ts b/packages/list/src/index.ts index 9d3cd7a..e7b8f7d 100644 --- a/packages/list/src/index.ts +++ b/packages/list/src/index.ts @@ -1,12 +1,12 @@ export { InfiniteListState, ListPartLoader, listStateIdle, InfiniteListPropsShared } from "./domain" export { - BaseInfiniteList, - InfiniteList, - isFakeItem, - FakeItem, - InfiniteListMapper, - MapperFactoryProps, - mapperFactory, - ListItem, + BaseInfiniteList, + InfiniteList, + isFakeItem, + FakeItem, + InfiniteListMapper, + MapperFactoryProps, + mapperFactory, + ListItem, } from "./infinite-list" export { reactiveList } from "./reactive-list" diff --git a/packages/list/src/infinite-list.test.ts b/packages/list/src/infinite-list.test.ts index b7e9e66..9215b4e 100644 --- a/packages/list/src/infinite-list.test.ts +++ b/packages/list/src/infinite-list.test.ts @@ -1,126 +1,128 @@ import { Atom } from "@rixio/atom" -import { CacheState } from "@rixio/cache" -import { InfiniteList, ListItem, mapperFactory, MapperFactoryProps } from "./infinite-list" -import { InfiniteListState, listStateIdle } from "./domain" +import type { CacheState } from "@rixio/cache" +import type { ListItem, MapperFactoryProps } from "./infinite-list" +import { InfiniteList, mapperFactory } from "./infinite-list" +import type { InfiniteListState } from "./domain" +import { listStateIdle } from "./domain" describe("mapper", () => { - function init(props?: MapperFactoryProps) { - const mapper = mapperFactory(props) - const loadNextInvocations: boolean[] = [] - const fakeList$ = { - pageSize: 10, - loadNext(force?: boolean) { - loadNextInvocations.push(Boolean(force)) - }, - } as InfiniteList - return [mapper, loadNextInvocations, fakeList$] as const - } + function init(props?: MapperFactoryProps) { + const mapper = mapperFactory(props) + const loadNextInvocations: boolean[] = [] + const fakeList$ = { + pageSize: 10, + loadNext(force?: boolean) { + loadNextInvocations.push(Boolean(force)) + }, + } as InfiniteList + return [mapper, loadNextInvocations, fakeList$] as const + } - it("should emit Wrapped pending if nothing is loaded and initial = wrapped", () => { - const [mapper, loadNextInvocations, fakeList$] = init() + it("should emit Wrapped pending if nothing is loaded and initial = wrapped", () => { + const [mapper, loadNextInvocations, fakeList$] = init() - const pending = mapper({ status: "pending", finished: false, continuation: null, items: [] }, fakeList$) - expect(pending.status).toBe("pending") - expect(loadNextInvocations.length).toBe(0) - }) + const pending = mapper({ status: "pending", finished: false, continuation: null, items: [] }, fakeList$) + expect(pending.status).toBe("pending") + expect(loadNextInvocations.length).toBe(0) + }) - it("should emit Wrapped rejected if nothing is loaded and initial = wrapped", () => { - const [mapper, loadNextInvocations, fakeList$] = init() + it("should emit Wrapped rejected if nothing is loaded and initial = wrapped", () => { + const [mapper, loadNextInvocations, fakeList$] = init() - const rejected = mapper( - { status: "rejected", error: "error1", finished: false, continuation: null, items: [] }, - fakeList$ - ) - expect(rejected.status).toBe("rejected") - expect((rejected as any).error).toBe("error1") - ;(rejected as any).reload() - expect(loadNextInvocations.length).toBe(1) - expect(loadNextInvocations[0]).toBe(true) - }) + const rejected = mapper( + { status: "rejected", error: "error1", finished: false, continuation: null, items: [] }, + fakeList$, + ) + expect(rejected.status).toBe("rejected") + expect((rejected as any).error).toBe("error1") + ;(rejected as any).reload() + expect(loadNextInvocations.length).toBe(1) + expect(loadNextInvocations[0]).toBe(true) + }) - it("should create fake pending items if initial = fake", () => { - const [mapper, loadNextInvocations, fakeList$] = init({ initial: "fake" }) + it("should create fake pending items if initial = fake", () => { + const [mapper, loadNextInvocations, fakeList$] = init({ initial: "fake" }) - const pending = mapper({ status: "pending", finished: false, continuation: null, items: [] }, fakeList$) - expect(pending.status).toBe("fulfilled") - expect((pending as any).value.length).toBe(10) - expect((pending as any).value[0].type).toBe("pending") - expect(loadNextInvocations.length).toBe(0) - ;(pending as any).value[0].loadNext() - expect(loadNextInvocations.length).toBe(1) - expect(loadNextInvocations[0]).toBe(false) - }) + const pending = mapper({ status: "pending", finished: false, continuation: null, items: [] }, fakeList$) + expect(pending.status).toBe("fulfilled") + expect((pending as any).value.length).toBe(10) + expect((pending as any).value[0].type).toBe("pending") + expect(loadNextInvocations.length).toBe(0) + ;(pending as any).value[0].loadNext() + expect(loadNextInvocations.length).toBe(1) + expect(loadNextInvocations[0]).toBe(false) + }) - it("should create fake rejected item if initial = fake", () => { - const [mapper, loadNextInvocations, fakeList$] = init({ initial: "fake" }) + it("should create fake rejected item if initial = fake", () => { + const [mapper, loadNextInvocations, fakeList$] = init({ initial: "fake" }) - const rejected = mapper( - { status: "rejected", error: "error1", finished: false, continuation: null, items: [] }, - fakeList$ - ) - expect(rejected.status).toBe("fulfilled") - expect((rejected as any).value.length).toBe(1) - expect((rejected as any).value[0].type).toBe("rejected") - expect(loadNextInvocations.length).toBe(0) - ;(rejected as any).value[0].reload() - expect(loadNextInvocations.length).toBe(1) - expect(loadNextInvocations[0]).toBe(true) - }) + const rejected = mapper( + { status: "rejected", error: "error1", finished: false, continuation: null, items: [] }, + fakeList$, + ) + expect(rejected.status).toBe("fulfilled") + expect((rejected as any).value.length).toBe(1) + expect((rejected as any).value[0].type).toBe("rejected") + expect(loadNextInvocations.length).toBe(0) + ;(rejected as any).value[0].reload() + expect(loadNextInvocations.length).toBe(1) + expect(loadNextInvocations[0]).toBe(true) + }) - it("should create fake pending items for second page", () => { - const [mapper, loadNextInvocations, fakeList$] = init({ initial: "fake" }) + it("should create fake pending items for second page", () => { + const [mapper, loadNextInvocations, fakeList$] = init({ initial: "fake" }) - const pending = mapper({ status: "pending", finished: false, continuation: 1, items: [0] }, fakeList$) - expect(pending.status).toBe("fulfilled") - expect((pending as any).value.length).toBe(11) - expect((pending as any).value[0].type).toBe("item") - expect((pending as any).value[0].value).toBe(0) - expect((pending as any).value[1].type).toBe("pending") - expect(loadNextInvocations.length).toBe(0) - ;(pending as any).value[1].loadNext() - expect(loadNextInvocations.length).toBe(1) - expect(loadNextInvocations[0]).toBe(false) - }) + const pending = mapper({ status: "pending", finished: false, continuation: 1, items: [0] }, fakeList$) + expect(pending.status).toBe("fulfilled") + expect((pending as any).value.length).toBe(11) + expect((pending as any).value[0].type).toBe("item") + expect((pending as any).value[0].value).toBe(0) + expect((pending as any).value[1].type).toBe("pending") + expect(loadNextInvocations.length).toBe(0) + ;(pending as any).value[1].loadNext() + expect(loadNextInvocations.length).toBe(1) + expect(loadNextInvocations[0]).toBe(false) + }) - it("should create fake rejected item for second page", () => { - const [mapper, loadNextInvocations, fakeList$] = init({ initial: "fake" }) + it("should create fake rejected item for second page", () => { + const [mapper, loadNextInvocations, fakeList$] = init({ initial: "fake" }) - const rejected = mapper( - { status: "rejected", error: "error1", finished: false, continuation: 1, items: [0] }, - fakeList$ - ) - expect(rejected.status).toBe("fulfilled") - expect((rejected as any).value.length).toBe(2) - expect((rejected as any).value[0].type).toBe("item") - expect((rejected as any).value[0].value).toBe(0) - expect((rejected as any).value[1].type).toBe("rejected") - expect(loadNextInvocations.length).toBe(0) - ;(rejected as any).value[1].reload() - expect(loadNextInvocations.length).toBe(1) - expect(loadNextInvocations[0]).toBe(true) - }) + const rejected = mapper( + { status: "rejected", error: "error1", finished: false, continuation: 1, items: [0] }, + fakeList$, + ) + expect(rejected.status).toBe("fulfilled") + expect((rejected as any).value.length).toBe(2) + expect((rejected as any).value[0].type).toBe("item") + expect((rejected as any).value[0].value).toBe(0) + expect((rejected as any).value[1].type).toBe("rejected") + expect(loadNextInvocations.length).toBe(0) + ;(rejected as any).value[1].reload() + expect(loadNextInvocations.length).toBe(1) + expect(loadNextInvocations[0]).toBe(true) + }) }) describe("InfiniteList", () => { - it("should load first page when subscribed", async () => { - const state$ = Atom.create>(listStateIdle) - const requests: Array<[number, string | null]> = [] - const loader = jest.fn().mockImplementation(async (size, cont) => { - requests.push([size, cont]) - return [[0, 1, 2], "first"] - }) - const list$ = new InfiniteList(state$, loader, 10, { pendingPageSize: 0 }) - const statuses: CacheState["status"][] = [] - const values: ListItem[] = [] - list$.subscribe(next => { - if (next.status === "fulfilled") values.push(...next.value) - statuses.push(next.status) - }) + it("should load first page when subscribed", async () => { + const state$ = Atom.create>(listStateIdle) + const requests: Array<[number, string | null]> = [] + const loader = jest.fn().mockImplementation(async (size, cont) => { + requests.push([size, cont]) + return [[0, 1, 2], "first"] + }) + const list$ = new InfiniteList(state$, loader, 10, { pendingPageSize: 0 }) + const statuses: CacheState["status"][] = [] + const values: ListItem[] = [] + list$.subscribe(next => { + if (next.status === "fulfilled") values.push(...next.value) + statuses.push(next.status) + }) - expect(statuses).toEqual(["pending"]) - await list$.loadNext() - expect(loader.mock.calls).toHaveLength(1) - expect(statuses).toEqual(["pending", "fulfilled"]) - expect(values.map(x => (x.type === "item" ? x.value : undefined))).toEqual([0, 1, 2]) - }) + expect(statuses).toEqual(["pending"]) + await list$.loadNext() + expect(loader.mock.calls).toHaveLength(1) + expect(statuses).toEqual(["pending", "fulfilled"]) + expect(values.map(x => (x.type === "item" ? x.value : undefined))).toEqual([0, 1, 2]) + }) }) diff --git a/packages/list/src/infinite-list.ts b/packages/list/src/infinite-list.ts index 9f34b1a..3ac93bd 100644 --- a/packages/list/src/infinite-list.ts +++ b/packages/list/src/infinite-list.ts @@ -1,177 +1,173 @@ -import { Wrapped, WrappedFulfilled, WrappedPending, WrappedRejected } from "@rixio/wrapped" -import { Atom } from "@rixio/atom" -import { InfiniteListState, ListPartLoader, listStateIdle } from "./domain" +import type { Wrapped } from "@rixio/wrapped" +import { WrappedFulfilled, WrappedPending, WrappedRejected } from "@rixio/wrapped" +import type { Atom } from "@rixio/atom" +import type { InfiniteListState, ListPartLoader } from "./domain" +import { listStateIdle } from "./domain" import { MappedSubject } from "./utils" export type InfiniteListMapper = ( - state: InfiniteListState, - list$: BaseInfiniteList + state: InfiniteListState, + list$: BaseInfiniteList, ) => Wrapped export class BaseInfiniteList extends MappedSubject, Wrapped> { - constructor( - readonly state$: Atom>, - readonly loader: ListPartLoader, - readonly pageSize: number, - private readonly mapper: InfiniteListMapper - ) { - super(state$, WrappedPending.create()) - } - - clear = (): void => this.state$.set(listStateIdle) - loadPage = (continuation: C | null): Promise<[T[], C | null]> => this.loader(this.pageSize, continuation) - - loadNext = (force = false): Promise => { - const { status, finished } = this.state$.get() - if ((force || status === "fulfilled") && !finished) { - return this.loadNextInternal() - } - return Promise.resolve() - } - - protected _onValue(source: InfiniteListState): void { - if (source.status === "idle") { - this.loadNextInternal().then() - } - this.next(this.mapper(source, this)) - } - - private async loadNextInternal(): Promise { - const status$ = this.state$.lens("status") - - if (status$.get() === "pending") { - console.warn("List is updating") - return - } else if (this.state$.get().finished) { - console.warn("Loadable list already finished") - return - } - - const promise = this.loader(this.pageSize, this.state$.get().continuation) - status$.set("pending") - try { - const [items, continuation] = await promise - const finished = items.length === 0 || continuation === null - this.state$.modify(state => ({ - ...state, - finished, - items: state.items.concat(items), - continuation, - status: "fulfilled", - })) - } catch (error) { - this.state$.modify(state => ({ - ...state, - status: "rejected", - error, - })) - } - } + constructor( + readonly state$: Atom>, + readonly loader: ListPartLoader, + readonly pageSize: number, + private readonly mapper: InfiniteListMapper, + ) { + super(state$, WrappedPending.create()) + } + + clear = (): void => this.state$.set(listStateIdle) + loadPage = (continuation: C | null): Promise<[T[], C | null]> => this.loader(this.pageSize, continuation) + + loadNext = (force = false): Promise => { + const { status, finished } = this.state$.get() + if ((force || status === "fulfilled") && !finished) { + return this.loadNextInternal() + } + return Promise.resolve() + } + + protected _onValue(source: InfiniteListState): void { + if (source.status === "idle") { + this.loadNextInternal().then() + } + this.next(this.mapper(source, this)) + } + + private async loadNextInternal(): Promise { + const status$ = this.state$.lens("status") + + if (status$.get() === "pending") { + console.warn("List is updating") + return + } else if (this.state$.get().finished) { + console.warn("Loadable list already finished") + return + } + + const promise = this.loader(this.pageSize, this.state$.get().continuation) + status$.set("pending") + try { + const [items, continuation] = await promise + const finished = items.length === 0 || continuation === null + this.state$.modify(state => ({ + ...state, + finished, + items: state.items.concat(items), + continuation, + status: "fulfilled", + })) + } catch (error) { + this.state$.modify(state => ({ + ...state, + status: "rejected", + error, + })) + } + } } export class InfiniteList extends BaseInfiniteList[]> { - constructor( - state$: Atom>, - loader: ListPartLoader, - pageSize: number, - props?: MapperFactoryProps - ) { - super(state$, loader, pageSize, mapperFactory(props)) - } + constructor( + state$: Atom>, + loader: ListPartLoader, + pageSize: number, + props?: MapperFactoryProps, + ) { + super(state$, loader, pageSize, mapperFactory(props)) + } } const fakeItem = "fake_item" const symbol = Symbol.for(fakeItem) type PendingItem = { - type: "pending" - loadNext: () => Promise + type: "pending" + loadNext: () => Promise } type RejectedItem = { - type: "rejected" - error: any - reload: () => Promise + type: "rejected" + error: any + reload: () => Promise } export type FakeItem = { - [fakeItem]: typeof symbol + [fakeItem]: typeof symbol } & (PendingItem | RejectedItem) export type RealListItem = { - type: "item" - value: T + type: "item" + value: T } export type ListItem = RealListItem | FakeItem export type MapperFactoryProps = { - pendingPageSize?: number - initial?: "wrapped" | "fake" + pendingPageSize?: number + initial?: "wrapped" | "fake" } export function mapperFactory(props?: MapperFactoryProps): InfiniteListMapper[]> { - return (state, list$) => { - const pendingPageSize = props?.pendingPageSize === undefined ? list$.pageSize : props?.pendingPageSize - const initial = props?.initial || "wrapped" - - if (initial === "wrapped") { - if (state.continuation === null && !state.finished) { - if (state.status === "idle" || state.status === "pending") { - return WrappedPending.create() - } else if (state.status === "rejected") { - return WrappedRejected.create(state.error, () => list$.loadNext(true)) - } - } - } - switch (state.status) { - case "idle": - case "pending": - return WrappedFulfilled.create( - createRealListItems(state.items).concat(createPendingPage(pendingPageSize, list$)) - ) - case "rejected": - return WrappedFulfilled.create([ - ...createRealListItems(state.items), - createRejectedItem(state.error, () => list$.loadNext(true)), - ]) - } - if (state.finished) { - return WrappedFulfilled.create(createRealListItems(state.items)) - } - return WrappedFulfilled.create(createRealListItems(state.items).concat(createPendingPage(pendingPageSize, list$))) - } + return (state, list$) => { + const pendingPageSize = props?.pendingPageSize === undefined ? list$.pageSize : props?.pendingPageSize + const initial = props?.initial || "wrapped" + + if (initial === "wrapped") { + if (state.continuation === null && !state.finished) { + if (state.status === "idle" || state.status === "pending") { + return WrappedPending.create() + } else if (state.status === "rejected") { + return WrappedRejected.create(state.error, () => list$.loadNext(true)) + } + } + } + if (state.status === "rejected") { + return WrappedFulfilled.create([ + ...createRealListItems(state.items), + createRejectedItem(state.error, () => list$.loadNext(true)), + ]) + } + if (state.finished) { + return WrappedFulfilled.create(createRealListItems(state.items)) + } + return WrappedFulfilled.create(createRealListItems(state.items).concat(createPendingPage(pendingPageSize, list$))) + } } function createRealListItems(values: T[]) { - return values.map(createRealListItem) + return values.map(createRealListItem) } function createRealListItem(value: T): RealListItem { - return { - type: "item", - value, - } + return { + type: "item", + value, + } } function createPendingPage(pageSize: number, list: BaseInfiniteList) { - const pendingItem = createPendingItem(() => list.loadNext()) - return new Array(pageSize).fill(pendingItem) + const pendingItem = createPendingItem(() => list.loadNext()) + return new Array(pageSize).fill(pendingItem) } function createRejectedItem(error: any, reload: () => Promise): FakeItem { - return { - [fakeItem]: symbol, - type: "rejected", - error, - reload, - } + return { + [fakeItem]: symbol, + type: "rejected", + error, + reload, + } } export function isFakeItem(object: any): boolean { - return typeof object === "object" && object[fakeItem] === symbol + return typeof object === "object" && object[fakeItem] === symbol } function createPendingItem(loadNext: () => Promise): FakeItem { - return { - [fakeItem]: symbol, - type: "pending", - loadNext, - } + return { + [fakeItem]: symbol, + type: "pending", + loadNext, + } } diff --git a/packages/list/src/reactive-list.ts b/packages/list/src/reactive-list.ts index 827141e..96f7ee5 100644 --- a/packages/list/src/reactive-list.ts +++ b/packages/list/src/reactive-list.ts @@ -1,17 +1,15 @@ -import { Observable } from "rxjs" +import type { Observable } from "rxjs" import { map, scan } from "rxjs/operators" -// tslint:disable no-unused-vars export function reactiveList( - ids: Observable, - createListItem: (x: string) => TValue + ids: Observable, + createListItem: (x: string) => TValue, ): Observable export function reactiveList( - ids: Observable, - createListItem: (x: number) => TValue + ids: Observable, + createListItem: (x: number) => TValue, ): Observable -// tslint:enable no-unused-vars /** * Derive a reactive list from: @@ -19,30 +17,31 @@ export function reactiveList( * - a list item factory – a function that will create a list item based on item id. */ export function reactiveList( - ids: Observable, - createListItem: (x: TKey) => TValue + ids: Observable, + createListItem: (x: TKey) => TValue, ): Observable { - return ids.pipe( - scan( - ([oldIds, _], ids) => { - const newIds: { [k: string]: TValue } = {} - const newValues: TValue[] = Array(ids.length) - const n = ids.length + return ids.pipe( + scan( + ([oldIds], ids) => { + const nextIds: { [k: string]: TValue } = {} + const nextValues: TValue[] = new Array(ids.length) + const n = ids.length - for (let i = 0; i < n; ++i) { - const id = ids[i] - const k = id.toString() - if (k in newIds) { - newValues[i] = newIds[k] - } else { - // eslint-disable-next-line no-multi-assign - newIds[k] = newValues[i] = k in oldIds ? oldIds[k] : (createListItem as (_: string | number) => TValue)(id) - } - } - return [newIds, newValues] - }, - [{}, []] - ), - map(([, values]) => values) - ) + for (let i = 0; i < n; ++i) { + const id = ids[i] + const k = id.toString() + if (k in nextIds) { + nextValues[i] = nextIds[k] + } else { + // eslint-disable-next-line no-multi-assign + nextIds[k] = nextValues[i] = + k in oldIds ? oldIds[k] : (createListItem as (_: string | number) => TValue)(id) + } + } + return [nextIds, nextValues] + }, + [{}, []], + ), + map(([, values]) => values), + ) } diff --git a/packages/list/src/utils.ts b/packages/list/src/utils.ts index 2ddb1c9..63ca2dc 100644 --- a/packages/list/src/utils.ts +++ b/packages/list/src/utils.ts @@ -1,40 +1,41 @@ -import { BehaviorSubject, Observable, Subscriber, Subscription } from "rxjs" +import type { Observable, Subscriber } from "rxjs" +import { BehaviorSubject, Subscription } from "rxjs" export abstract class MappedSubject extends BehaviorSubject { - protected _subscription: Subscription | null = null - private _refCount = 0 + protected _subscription: Subscription | null = null + private _refCount = 0 - protected constructor(protected readonly _observable: Observable, initial: T) { - super(initial) - } + protected constructor(protected readonly _observable: Observable, initial: T) { + super(initial) + } - protected abstract _onValue(source: S): void + protected abstract _onValue(source: S): void - _subscribe(subscriber: Subscriber): Subscription { - // tslint:disable-line function-name - if (!this._subscription) { - this._subscription = this._observable.subscribe(v => this._onValue(v)) - } - this._refCount = this._refCount + 1 + _subscribe(subscriber: Subscriber): Subscription { + // tslint:disable-line function-name + if (!this._subscription) { + this._subscription = this._observable.subscribe(v => this._onValue(v)) + } + this._refCount = this._refCount + 1 - const sub = new Subscription(() => { - this._refCount = this._refCount - 1 - if (this._refCount <= 0 && this._subscription) { - this._subscription.unsubscribe() - this._subscription = null - } - }) - sub.add(super._subscribe(subscriber)) - return sub - } + const sub = new Subscription(() => { + this._refCount = this._refCount - 1 + if (this._refCount <= 0 && this._subscription) { + this._subscription.unsubscribe() + this._subscription = null + } + }) + sub.add(super._subscribe(subscriber)) + return sub + } - unsubscribe() { - if (this._subscription) { - this._subscription.unsubscribe() - this._subscription = null - } - this._refCount = 0 + unsubscribe() { + if (this._subscription) { + this._subscription.unsubscribe() + this._subscription = null + } + this._refCount = 0 - super.unsubscribe() - } + super.unsubscribe() + } } diff --git a/packages/list/test/utils/range.ts b/packages/list/test/utils/range.ts index 82594f6..5ec373d 100644 --- a/packages/list/test/utils/range.ts +++ b/packages/list/test/utils/range.ts @@ -1,5 +1,5 @@ export const range = (from: number, to: number) => { - return Array(to - from) + return new Array(to - from) .fill(0) .map((_, idx) => from + idx) } diff --git a/packages/react/src/base.ts b/packages/react/src/base.ts index e442bd8..44cf92a 100644 --- a/packages/react/src/base.ts +++ b/packages/react/src/base.ts @@ -1,174 +1,176 @@ import React from "react" -import { Observable, Subscription } from "rxjs" +import type { Subscription } from "rxjs" +import { Observable } from "rxjs" import { Lens } from "@rixio/lens" -import { Wrapped, Lifted, WrappedRejected, WrappedPending, toWrapped } from "@rixio/wrapped" +import type { Wrapped, Lifted } from "@rixio/wrapped" +import { WrappedRejected, WrappedPending, toWrapped } from "@rixio/wrapped" export type OrReactChild = React.ReactChild | React.ReactChild[] | T export type RxBaseProps = { - pending?: React.ReactNode - rejected?: OrReactChild<(error: any, reload: () => void) => React.ReactNode> + pending?: React.ReactNode + rejected?: OrReactChild<(error: any, reload: () => void) => React.ReactNode> } export abstract class RxWrapperBase

extends React.Component< - RProps, - Lifted

+ RProps, + Lifted

> { - private _state: Lifted

- private _mounted: boolean = false - private subscriptions: Map, [Subscription, Lens, any>]> = new Map() + private _state: Lifted

+ private _mounted: boolean = false + private subscriptions: Map, [Subscription, Lens, any>]> = new Map() - constructor(props: RProps) { - super(props) - this._state = this.extractProps(props) - this.doSubscribe({} as Lifted

, this._state) - this.state = this._state - } + constructor(props: RProps) { + super(props) + this._state = this.extractProps(props) + this.doSubscribe({} as Lifted

, this._state) + this.state = this._state + } - abstract extractRxBaseProps(props: RProps): RxBaseProps | undefined - abstract extractProps(props: RProps): Lifted

- abstract extractComponent(props: RProps): any + abstract extractRxBaseProps(props: RProps): RxBaseProps | undefined + abstract extractProps(props: RProps): Lifted

+ abstract extractComponent(props: RProps): any - componentDidMount() { - this._mounted = true - } + componentDidMount() { + this._mounted = true + } - componentWillUnmount() { - this.subscriptions.forEach(([subscription], key, map) => { - subscription.unsubscribe() - map.delete(key) - }) - this._mounted = false - } + componentWillUnmount() { + this.subscriptions.forEach(([subscription], key, map) => { + subscription.unsubscribe() + map.delete(key) + }) + this._mounted = false + } - shouldComponentUpdate(nextProps: RProps): boolean { - if (this.props !== nextProps) { - const oldProps = this.extractProps(this.props) - const newProps = this.extractProps(nextProps) - this.doUnsubscribe(oldProps, newProps) - this.doSubscribe(oldProps, newProps) - this.setState(this._state) - } - return true - } + shouldComponentUpdate(nextProps: RProps): boolean { + if (this.props !== nextProps) { + const oldProps = this.extractProps(this.props) + const finalProps = this.extractProps(nextProps) + this.doUnsubscribe(oldProps, finalProps) + this.doSubscribe(oldProps, finalProps) + this.setState(this._state) + } + return true + } - render() { - const result = toWrapped(this.checkObservables()) - if (result.status === "fulfilled") { - return React.createElement(this.extractComponent(this.props), result.value as P) - } - const props = this.extractRxBaseProps(this.props) - if (props) { - if (result.status === "pending" && props.pending) { - return props.pending - } - if (result.status === "rejected" && props.rejected) { - if (typeof props.rejected === "function") { - return props.rejected(result.error, result.reload) - } - return props.rejected - } - } - return null - } + render() { + const result = toWrapped(this.checkObservables()) + if (result.status === "fulfilled") { + return React.createElement(this.extractComponent(this.props), result.value as P) + } + const props = this.extractRxBaseProps(this.props) + if (props) { + if (result.status === "pending" && props.pending) { + return props.pending + } + if (result.status === "rejected" && props.rejected) { + if (typeof props.rejected === "function") { + return props.rejected(result.error, result.reload) + } + return props.rejected + } + } + return null + } - private doSubscribe(oldProps: Lifted

, props: Lifted

) { - walk(props, (value, lens) => { - if (value instanceof Observable) { - let oldValue: any - try { - oldValue = lens.get(oldProps) - } catch (e) { - oldValue = undefined - } - if (oldValue !== value) { - const s = value.subscribe( - v => this.handle(lens, toWrapped(v)), - e => this.handle(lens, WrappedRejected.create(e)) - ) - this.subscriptions.set(value, [s, lens]) - } - } else { - this._state = lens.set(value, this._state) - } - }) - } + private doSubscribe(oldProps: Lifted

, props: Lifted

) { + walk(props, (value, lens) => { + if (value instanceof Observable) { + let oldValue: any + try { + oldValue = lens.get(oldProps) + } catch (e) { + oldValue = undefined + } + if (oldValue !== value) { + const s = value.subscribe( + v => this.handle(lens, toWrapped(v)), + e => this.handle(lens, WrappedRejected.create(e)), + ) + this.subscriptions.set(value, [s, lens]) + } + } else { + this._state = lens.set(value, this._state) + } + }) + } - private doUnsubscribe(oldProps: Lifted

, newProps: Lifted

) { - walk(oldProps, (value, lens) => { - if (value instanceof Observable && lens.get(newProps) !== value) { - const [subscription] = this.subscriptions.get(value) || [] - if (subscription) { - subscription.unsubscribe() - } - this.subscriptions.delete(value) - } - }) - } + private doUnsubscribe(oldProps: Lifted

, nextProps: Lifted

) { + walk(oldProps, (value, lens) => { + if (value instanceof Observable && lens.get(nextProps) !== value) { + const [subscription] = this.subscriptions.get(value) || [] + if (subscription) { + subscription.unsubscribe() + } + this.subscriptions.delete(value) + } + }) + } - private handle(lens: Lens, any>, value: any) { - const newState = lens.set(value, this._state) - if (newState !== this._state) { - this._state = newState - if (this._mounted) { - this.setState(newState) - } - } - } + private handle(lens: Lens, any>, value: any) { + const nextState = lens.set(value, this._state) + if (nextState !== this._state) { + this._state = nextState + if (this._mounted) { + this.setState(nextState) + } + } + } - private checkObservables(): Lifted

| Wrapped { - let foundPending = false - const rejected: WrappedRejected[] = [] - let props = this._state - walk(this._state, (value, lens) => { - if (value instanceof Observable) { - foundPending = true - } - const wrapped = toWrapped(value) - if (wrapped.status === "rejected") { - rejected.push(wrapped) - } else if (wrapped.status === "pending") { - foundPending = true - } else { - props = lens.set(wrapped.value, props) - } - }) - if (foundPending) return WrappedPending.create() - if (rejected.length > 0) { - const reload = () => { - rejected.forEach(r => r.reload()) - this.subscriptions.forEach(([s, lens], obs) => { - s.unsubscribe() - const newSubscription = obs.subscribe( - plain => this.handle(lens, toWrapped(plain)), - error => this.handle(lens, WrappedRejected.create(error)) - ) - this.subscriptions.set(obs, [newSubscription, lens]) - }) - } - return WrappedRejected.create(rejected[0].error, reload) - } - return props - } + private checkObservables(): Lifted

| Wrapped { + let foundPending = false + const rejected: WrappedRejected[] = [] + let props = this._state + walk(this._state, (value, lens) => { + if (value instanceof Observable) { + foundPending = true + } + const wrapped = toWrapped(value) + if (wrapped.status === "rejected") { + rejected.push(wrapped) + } else if (wrapped.status === "pending") { + foundPending = true + } else { + props = lens.set(wrapped.value, props) + } + }) + if (foundPending) return WrappedPending.create() + if (rejected.length > 0) { + const reload = () => { + rejected.forEach(r => r.reload()) + this.subscriptions.forEach(([s, lens], obs) => { + s.unsubscribe() + const nextSubscription = obs.subscribe( + plain => this.handle(lens, toWrapped(plain)), + error => this.handle(lens, WrappedRejected.create(error)), + ) + this.subscriptions.set(obs, [nextSubscription, lens]) + }) + } + return WrappedRejected.create(rejected[0].error, reload) + } + return props + } } function walk(props: T, handler: (value: any, lens: Lens) => R | undefined) { - for (const key in props) { - if (props.hasOwnProperty(key)) { - const prop = props[key] as any - if (key === "children" && Array.isArray(prop)) { - for (let i = 0; i < prop.length; i++) { - const result = handler(prop[i], Lens.compose(Lens.key("children"), Lens.index(i)) as any) - if (result !== undefined) { - return result - } - } - } else { - const result = handler(prop, Lens.key(key) as any) - if (result !== undefined) { - return result - } - } - } - } + for (const key in props) { + if (props.hasOwnProperty(key)) { + const prop = props[key] as any + if (key === "children" && Array.isArray(prop)) { + for (let i = 0; i < prop.length; i++) { + const result = handler(prop[i], Lens.compose(Lens.key("children"), Lens.index(i)) as any) + if (result !== undefined) { + return result + } + } + } else { + const result = handler(prop, Lens.key(key) as any) + if (result !== undefined) { + return result + } + } + } + } } diff --git a/packages/react/src/lift.test.tsx b/packages/react/src/lift.test.tsx index 965338d..26c08a5 100644 --- a/packages/react/src/lift.test.tsx +++ b/packages/react/src/lift.test.tsx @@ -4,59 +4,59 @@ import React from "react" import { lift } from "./lift" function Div({ value }: { value: string }) { - return

{value}
+ return
{value}
} const LiftedDiv = lift(Div) const ExtendedLiftedDiv = lift(Div, { - pending: "BLABLABLA", + pending: "BLABLABLA", }) describe("lift", () => { - test("should observe reactive value using lifted html", () => { - const text = Math.random().toString() - const obs = new ReplaySubject(1) - obs.next(text) - const r = render( - - - - ) - expect(r.getByTestId("value")).toHaveTextContent(text) - const nextText = Math.random().toString() - act(() => obs.next(nextText)) - expect(r.getByTestId("value")).toHaveTextContent(nextText) - }) + test("should observe reactive value using lifted html", () => { + const text = Math.random().toString() + const obs = new ReplaySubject(1) + obs.next(text) + const r = render( + + + , + ) + expect(r.getByTestId("value")).toHaveTextContent(text) + const nextText = Math.random().toString() + act(() => obs.next(nextText)) + expect(r.getByTestId("value")).toHaveTextContent(nextText) + }) - test("should render nothing if observable doesn't emit value immediately", () => { - const obs = new ReplaySubject(1) - const r = render( - - - - ) - expect(r.getByTestId("value")).toBeEmpty() - const text = Math.random().toString() - obs.next(text) - expect(r.getByTestId("value")).toHaveTextContent(text) - const nextText = Math.random().toString() - act(() => obs.next(nextText)) - expect(r.getByTestId("value")).toHaveTextContent(nextText) - }) + test("should render nothing if observable doesn't emit value immediately", () => { + const obs = new ReplaySubject(1) + const r = render( + + + , + ) + expect(r.getByTestId("value")).toBeEmpty() + const text = Math.random().toString() + obs.next(text) + expect(r.getByTestId("value")).toHaveTextContent(text) + const nextText = Math.random().toString() + act(() => obs.next(nextText)) + expect(r.getByTestId("value")).toHaveTextContent(nextText) + }) - test("should render pending props if observable doesn't emit value", () => { - const obs = new ReplaySubject(1) - const r = render( - - - - ) - expect(r.getByTestId("value")).toHaveTextContent("BLABLABLA") - const text = Math.random().toString() - obs.next(text) - expect(r.getByTestId("value")).toHaveTextContent(text) - const nextText = Math.random().toString() - act(() => obs.next(nextText)) - expect(r.getByTestId("value")).toHaveTextContent(nextText) - }) + test("should render pending props if observable doesn't emit value", () => { + const obs = new ReplaySubject(1) + const r = render( + + + , + ) + expect(r.getByTestId("value")).toHaveTextContent("BLABLABLA") + const text = Math.random().toString() + obs.next(text) + expect(r.getByTestId("value")).toHaveTextContent(text) + const nextText = Math.random().toString() + act(() => obs.next(nextText)) + expect(r.getByTestId("value")).toHaveTextContent(nextText) + }) }) diff --git a/packages/react/src/lift.tsx b/packages/react/src/lift.tsx index ff82219..b9b1b2d 100644 --- a/packages/react/src/lift.tsx +++ b/packages/react/src/lift.tsx @@ -1,31 +1,33 @@ -import React, { ComponentType } from "react" -import { Lifted } from "@rixio/wrapped" -import { RxBaseProps, RxWrapperBase } from "./base" +import type { ComponentType } from "react" +import React from "react" +import type { Lifted } from "@rixio/wrapped" +import type { RxBaseProps } from "./base" +import { RxWrapperBase } from "./base" export type RxLiftProps

= RxBaseProps & { - component: ComponentType

- props: Lifted

+ component: ComponentType

+ props: Lifted

} export class RxLift

extends RxWrapperBase> { - extractRxBaseProps(props: RxLiftProps

): RxBaseProps | undefined { - return this.props - } + extractRxBaseProps(): RxBaseProps | undefined { + return this.props + } - extractProps({ props }: RxLiftProps

): Lifted

{ - return props - } + extractProps({ props }: RxLiftProps

): Lifted

{ + return props + } - extractComponent({ component }: RxLiftProps

): any { - return component - } + extractComponent({ component }: RxLiftProps

): any { + return component + } } export function lift

(component: ComponentType

, baseProps?: RxBaseProps): React.FC> { - function LiftedComponent(props: Lifted

) { - return - } + function LiftedComponent(props: Lifted

) { + return + } - LiftedComponent.displayName = `lifted(${component.displayName})` - return LiftedComponent + LiftedComponent.displayName = `lifted(${component.displayName})` + return LiftedComponent } diff --git a/packages/react/src/rx-html.test.tsx b/packages/react/src/rx-html.test.tsx index cc6f1f0..9005a12 100644 --- a/packages/react/src/rx-html.test.tsx +++ b/packages/react/src/rx-html.test.tsx @@ -1,4 +1,3 @@ -/* eslint-disable react/jsx-pascal-case */ import React from "react" import { act, render } from "@testing-library/react" import { WrappedFulfilled } from "@rixio/wrapped" @@ -6,61 +5,61 @@ import { of, ReplaySubject } from "rxjs" import { R } from "./rx-html" describe("RxHtml", () => { - test("should observe reactive value using lifted html", () => { - const text = Math.random().toString() - const obs = new ReplaySubject(1) - obs.next(text) - const r = render( - - {obs} - - ) - expect(r.getByTestId("value")).toHaveTextContent(text) - const nextText = Math.random().toString() - act(() => obs.next(nextText)) - expect(r.getByTestId("value")).toHaveTextContent(nextText) - }) + test("should observe reactive value using lifted html", () => { + const text = Math.random().toString() + const obs = new ReplaySubject(1) + obs.next(text) + const r = render( + + {obs} + , + ) + expect(r.getByTestId("value")).toHaveTextContent(text) + const nextText = Math.random().toString() + act(() => obs.next(nextText)) + expect(r.getByTestId("value")).toHaveTextContent(nextText) + }) - test("should observe wrapped reactive value using lifted html", () => { - const text = Math.random().toString() - const r = render( - - {of(WrappedFulfilled.create(text))} - - ) - expect(r.getByTestId("value")).toHaveTextContent(text) - }) + test("should observe wrapped reactive value using lifted html", () => { + const text = Math.random().toString() + const r = render( + + {of(WrappedFulfilled.create(text))} + , + ) + expect(r.getByTestId("value")).toHaveTextContent(text) + }) - test("should support some observables in children", () => { - const text = Math.random().toString() - const obs = new ReplaySubject(1) - obs.next(text) - const r = render( - - - {obs}-test-{obs} - - - ) - expect(r.getByTestId("value")).toHaveTextContent(`${text}-test-${text}`) - const nextText = Math.random().toString() - act(() => obs.next(nextText)) - expect(r.getByTestId("value")).toHaveTextContent(`${nextText}-test-${nextText}`) - }) + test("should support some observables in children", () => { + const text = Math.random().toString() + const obs = new ReplaySubject(1) + obs.next(text) + const r = render( + + + {obs}-test-{obs} + + , + ) + expect(r.getByTestId("value")).toHaveTextContent(`${text}-test-${text}`) + const nextText = Math.random().toString() + act(() => obs.next(nextText)) + expect(r.getByTestId("value")).toHaveTextContent(`${nextText}-test-${nextText}`) + }) - test("should not display anything if observable doesn't emit value", () => { - const text = Math.random().toString() - const obs = new ReplaySubject(1) - const r = render( - - {obs} - - ) - expect(r.getByTestId("value")).toBeEmpty() - obs.next(text) - expect(r.getByTestId("value")).toHaveTextContent(text) - const nextText = Math.random().toString() - act(() => obs.next(nextText)) - expect(r.getByTestId("value")).toHaveTextContent(nextText) - }) + test("should not display anything if observable doesn't emit value", () => { + const text = Math.random().toString() + const obs = new ReplaySubject(1) + const r = render( + + {obs} + , + ) + expect(r.getByTestId("value")).toBeEmpty() + obs.next(text) + expect(r.getByTestId("value")).toHaveTextContent(text) + const nextText = Math.random().toString() + act(() => obs.next(nextText)) + expect(r.getByTestId("value")).toHaveTextContent(nextText) + }) }) diff --git a/packages/react/src/rx-html.tsx b/packages/react/src/rx-html.tsx index f5d897f..8fdbd70 100644 --- a/packages/react/src/rx-html.tsx +++ b/packages/react/src/rx-html.tsx @@ -1,179 +1,181 @@ -import React, { LegacyRef, ReactHTML, DetailedHTMLFactory } from "react" -import { Lifted } from "@rixio/wrapped" -import { RxBaseProps, RxWrapperBase } from "./base" +import type { LegacyRef, ReactHTML, DetailedHTMLFactory } from "react" +import React from "react" +import type { Lifted } from "@rixio/wrapped" +import type { RxBaseProps } from "./base" +import { RxWrapperBase } from "./base" type InferHtmlProps = React.ReactHTML[K] extends DetailedHTMLFactory - ? P - : never + ? P + : never type InferHtmlElementType = React.ReactHTML[K] extends DetailedHTMLFactory< - any, - infer T + any, + infer T > - ? T - : never + ? T + : never type WithRef = { ref?: LegacyRef> } export type RxHtmlElementProps = Lifted> & WithRef export type RxHtmlProps = { - component: K - props?: RxHtmlElementProps + component: K + props?: RxHtmlElementProps } export class RxHtml extends RxWrapperBase, RxHtmlProps> { - extractRxBaseProps(props: RxHtmlProps): RxBaseProps | undefined { - return undefined - } + extractRxBaseProps(): RxBaseProps | undefined { + return undefined + } - extractProps({ props }: RxHtmlProps): Lifted> { - return props || ({} as any) - } + extractProps({ props }: RxHtmlProps): Lifted> { + return props || ({} as any) + } - extractComponent({ component }: RxHtmlProps): any { - return component - } + extractComponent({ component }: RxHtmlProps): any { + return component + } } export function liftHtml(key: K): React.FC> { - function LiftedComponent(props: RxHtmlElementProps) { - return - } + function LiftedComponent(props: RxHtmlElementProps) { + return + } - LiftedComponent.displayName = `lifted(${key})` - return LiftedComponent + LiftedComponent.displayName = `lifted(${key})` + return LiftedComponent } export type LiftedIntrinsicsHTML = { - readonly [K in keyof React.ReactHTML]: React.FunctionComponent> + readonly [K in keyof React.ReactHTML]: React.FunctionComponent> } function liftAll(html: Array): LiftedIntrinsicsHTML { - const r: { - -readonly [K in keyof ReactHTML]?: React.FunctionComponent> - } = {} + const r: { + -readonly [K in keyof ReactHTML]?: React.FunctionComponent> + } = {} - // @ts-ignore - html.forEach(e => (r[e] = liftHtml(e))) + // @ts-ignore + html.forEach(e => (r[e] = liftHtml(e))) - return r as LiftedIntrinsicsHTML + return r as LiftedIntrinsicsHTML } const html: (keyof ReactHTML)[] = [ - "a", - "abbr", - "address", - "area", - "article", - "aside", - "audio", - "b", - "base", - "bdi", - "bdo", - "big", - "blockquote", - "body", - "br", - "button", - "canvas", - "caption", - "cite", - "code", - "col", - "colgroup", - "data", - "datalist", - "dd", - "del", - "details", - "dfn", - "dialog", - "div", - "dl", - "dt", - "em", - "embed", - "fieldset", - "figcaption", - "figure", - "footer", - "form", - "h1", - "h2", - "h3", - "h4", - "h5", - "h6", - "head", - "header", - "hgroup", - "hr", - "html", - "i", - "iframe", - "img", - "input", - "ins", - "kbd", - "keygen", - "label", - "legend", - "li", - "link", - "main", - "map", - "mark", - "menu", - "menuitem", - "meta", - "meter", - "nav", - "noscript", - "object", - "ol", - "optgroup", - "option", - "output", - "p", - "param", - "picture", - "pre", - "progress", - "q", - "rp", - "rt", - "ruby", - "s", - "samp", - "script", - "section", - "select", - "small", - "source", - "span", - "strong", - "style", - "sub", - "summary", - "sup", - "table", - "tbody", - "td", - "textarea", - "tfoot", - "th", - "thead", - "time", - "title", - "tr", - "track", - "u", - "ul", - "var", - "video", - "wbr", + "a", + "abbr", + "address", + "area", + "article", + "aside", + "audio", + "b", + "base", + "bdi", + "bdo", + "big", + "blockquote", + "body", + "br", + "button", + "canvas", + "caption", + "cite", + "code", + "col", + "colgroup", + "data", + "datalist", + "dd", + "del", + "details", + "dfn", + "dialog", + "div", + "dl", + "dt", + "em", + "embed", + "fieldset", + "figcaption", + "figure", + "footer", + "form", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "head", + "header", + "hgroup", + "hr", + "html", + "i", + "iframe", + "img", + "input", + "ins", + "kbd", + "keygen", + "label", + "legend", + "li", + "link", + "main", + "map", + "mark", + "menu", + "menuitem", + "meta", + "meter", + "nav", + "noscript", + "object", + "ol", + "optgroup", + "option", + "output", + "p", + "param", + "picture", + "pre", + "progress", + "q", + "rp", + "rt", + "ruby", + "s", + "samp", + "script", + "section", + "select", + "small", + "source", + "span", + "strong", + "style", + "sub", + "summary", + "sup", + "table", + "tbody", + "td", + "textarea", + "tfoot", + "th", + "thead", + "time", + "title", + "tr", + "track", + "u", + "ul", + "var", + "video", + "wbr", ] export const R = liftAll(html) diff --git a/packages/react/src/rx-if.test.tsx b/packages/react/src/rx-if.test.tsx index e3cbe48..f336375 100644 --- a/packages/react/src/rx-if.test.tsx +++ b/packages/react/src/rx-if.test.tsx @@ -7,78 +7,78 @@ import { RxIf } from "./rx-if" let counter = 0 function Count() { - counter = counter + 1 - return text + counter = counter + 1 + return text } function Test({ value }: { value: string }) { - return {value} + return {value} } describe("RxIf", () => { - test("children are not rendered if test is not true", async () => { - expect.assertions(3) - const bool = Atom.create(false) - const r = render( - - - - ) - expect(() => r.getByTestId("value")).toThrow() - expect(counter).toBe(0) - act(() => bool.set(true)) - r.getByTestId("value") - expect(counter).toBe(1) - }) + test("children are not rendered if test is not true", async () => { + expect.assertions(3) + const bool = Atom.create(false) + const r = render( + + + , + ) + expect(() => r.getByTestId("value")).toThrow() + expect(counter).toBe(0) + act(() => bool.set(true)) + r.getByTestId("value") + expect(counter).toBe(1) + }) - test("should render children if truthy", async () => { - expect.assertions(1) - const bool = Atom.create("value") - const r = render( - - test string - - ) - r.getByTestId("value") - act(() => bool.set("")) - expect(() => r.getByTestId("value")).toThrow() - }) + test("should render children if truthy", async () => { + expect.assertions(1) + const bool = Atom.create("value") + const r = render( + + test string + , + ) + r.getByTestId("value") + act(() => bool.set("")) + expect(() => r.getByTestId("value")).toThrow() + }) - test("should render children if true", async () => { - expect.assertions(1) - const bool = Atom.create(true) - const r = render( - - test string - - ) - r.getByTestId("value") - act(() => bool.set(false)) - expect(() => r.getByTestId("value")).toThrow() - }) + test("should render children if true", async () => { + expect.assertions(1) + const bool = Atom.create(true) + const r = render( + + test string + , + ) + r.getByTestId("value") + act(() => bool.set(false)) + expect(() => r.getByTestId("value")).toThrow() + }) - test("should work with negate", async () => { - expect.assertions(1) - const bool = Atom.create(false) - const r = render( - - test string - - ) - r.getByTestId("value") - act(() => bool.set(true)) - expect(() => r.getByTestId("value")).toThrow() - }) + test("should work with negate", async () => { + expect.assertions(1) + const bool = Atom.create(false) + const r = render( + + test string + , + ) + r.getByTestId("value") + act(() => bool.set(true)) + expect(() => r.getByTestId("value")).toThrow() + }) - test("should render else part if not true", () => { - const bool = Atom.create(true) - const r = render( - }> - - - ) - expect(r.getByTestId("value")).toHaveTextContent("true") - act(() => bool.set(false)) - expect(r.getByTestId("value")).toHaveTextContent("false") - }) + test("should render else part if not true", () => { + const bool = Atom.create(true) + const r = render( + }> + + , + ) + expect(r.getByTestId("value")).toHaveTextContent("true") + act(() => bool.set(false)) + expect(r.getByTestId("value")).toHaveTextContent("false") + }) }) diff --git a/packages/react/src/rx-if.tsx b/packages/react/src/rx-if.tsx index 490513b..8da480a 100644 --- a/packages/react/src/rx-if.tsx +++ b/packages/react/src/rx-if.tsx @@ -1,28 +1,28 @@ import React from "react" -import { Observable } from "rxjs" +import type { Observable } from "rxjs" import { useRx } from "./use-rx" -import { OrReactChild } from "./base" +import type { OrReactChild } from "./base" export interface RxIfProps { - test$: Observable - else?: OrReactChild<() => React.ReactNode> - negate?: boolean - children: React.ReactNode + test$: Observable + else?: OrReactChild<() => React.ReactNode> + negate?: boolean + children: React.ReactNode } export function RxIf({ test$, children, negate, else: not }: RxIfProps): React.ReactElement | null { - const raw = useRx(test$) - const truthy = raw.status === "fulfilled" && Boolean(raw.value) + const raw = useRx(test$) + const truthy = raw.status === "fulfilled" && Boolean(raw.value) - if (negate && !truthy) { - return <>{children} - } else if (negate) { - if (typeof not === "function") return <>{not()} - else return <>{not} - } else if (truthy) { - return <>{children} - } else { - if (typeof not === "function") return <>{not()} - else return <>{not} - } + if (negate && !truthy) { + return <>{children} + } else if (negate) { + if (typeof not === "function") return <>{not()} + else return <>{not} + } else if (truthy) { + return <>{children} + } else { + if (typeof not === "function") return <>{not()} + else return <>{not} + } } diff --git a/packages/react/src/rx-wrapper.test.tsx b/packages/react/src/rx-wrapper.test.tsx index 6fc2b78..a0c0f47 100644 --- a/packages/react/src/rx-wrapper.test.tsx +++ b/packages/react/src/rx-wrapper.test.tsx @@ -1,151 +1,152 @@ import React from "react" import { act, render, fireEvent } from "@testing-library/react" import { Observable, ReplaySubject, defer } from "rxjs" -import { Wrapped, WrappedFulfilled, WrappedRejected } from "@rixio/wrapped" +import type { Wrapped } from "@rixio/wrapped" +import { WrappedFulfilled, WrappedRejected } from "@rixio/wrapped" import { RxWrapper } from "./rx-wrapper" type TestProps = { value1: string; value2: string } const Test = ({ value1, value2 }: TestProps) => { - return ( - - {value1} {value2} - - ) + return ( + + {value1} {value2} + + ) } const Testing = ({ text, reload }: { text?: any; reload?: () => void }) => { - return ( - <> - {text || "BLABLABLA"} - - - ) + return ( + <> + {text || "BLABLABLA"} + + + ) } describe("RxWrapper", () => { - test("should observe reactive value", () => { - const text = Math.random().toString() - const obs = new ReplaySubject(1) - obs.next(text) - const r = render() - expect(r.getByTestId("value")).toHaveTextContent(text) - const nextText = Math.random().toString() - act(() => obs.next(nextText)) - expect(r.getByTestId("value")).toHaveTextContent(nextText) - }) + test("should observe reactive value", () => { + const text = Math.random().toString() + const obs = new ReplaySubject(1) + obs.next(text) + const r = render() + expect(r.getByTestId("value")).toHaveTextContent(text) + const nextText = Math.random().toString() + act(() => obs.next(nextText)) + expect(r.getByTestId("value")).toHaveTextContent(nextText) + }) - test("should not render anything if observable doesn't emit value", () => { - const text = Math.random().toString() - const obs = new ReplaySubject(1) - const r = render() - expect(() => r.getByTestId("value")).toThrow() - act(() => obs.next(text)) - expect(r.getByTestId("value")).toHaveTextContent(text) - const nextText = Math.random().toString() - act(() => obs.next(nextText)) - expect(r.getByTestId("value")).toHaveTextContent(nextText) - }) + test("should not render anything if observable doesn't emit value", () => { + const text = Math.random().toString() + const obs = new ReplaySubject(1) + const r = render() + expect(() => r.getByTestId("value")).toThrow() + act(() => obs.next(text)) + expect(r.getByTestId("value")).toHaveTextContent(text) + const nextText = Math.random().toString() + act(() => obs.next(nextText)) + expect(r.getByTestId("value")).toHaveTextContent(nextText) + }) - test("should render pending prop if observable doesn't emit value", () => { - const text = Math.random().toString() - const obs = new ReplaySubject(1) - const r = render(} />) - expect(r.getByTestId("testing")).toHaveTextContent("BLABLABLA") - expect(() => r.getByTestId("value")).toThrow() - act(() => obs.next(text)) - expect(r.getByTestId("value")).toHaveTextContent(text) - const nextText = Math.random().toString() - act(() => obs.next(nextText)) - expect(r.getByTestId("value")).toHaveTextContent(nextText) - }) + test("should render pending prop if observable doesn't emit value", () => { + const text = Math.random().toString() + const obs = new ReplaySubject(1) + const r = render(} />) + expect(r.getByTestId("testing")).toHaveTextContent("BLABLABLA") + expect(() => r.getByTestId("value")).toThrow() + act(() => obs.next(text)) + expect(r.getByTestId("value")).toHaveTextContent(text) + const nextText = Math.random().toString() + act(() => obs.next(nextText)) + expect(r.getByTestId("value")).toHaveTextContent(nextText) + }) - test("should render rejected prop if observable emits error", () => { - const text = Math.random().toString() - const obs = new ReplaySubject>(1) - let reloaded = false - const reload = () => (reloaded = true) - const r = render( - - component={Test} - value1={obs} - value2="static" - rejected={(e, reload) => } - /> - ) - act(() => obs.next(WrappedRejected.create(text, reload))) - expect(r.getByTestId("testing")).toHaveTextContent(text) - expect(() => r.getByTestId("value")).toThrow() - expect(reloaded).toBeFalsy() - act(() => { - fireEvent.click(r.getByTestId("reload")) - }) - expect(reloaded).toBeTruthy() - act(() => obs.next(WrappedFulfilled.create(text))) - expect(r.getByTestId("value")).toHaveTextContent(text) - }) + test("should render rejected prop if observable emits error", () => { + const text = Math.random().toString() + const obs = new ReplaySubject>(1) + let reloaded = false + const reload = () => (reloaded = true) + const r = render( + + component={Test} + value1={obs} + value2="static" + rejected={(e, reload) => } + />, + ) + act(() => obs.next(WrappedRejected.create(text, reload))) + expect(r.getByTestId("testing")).toHaveTextContent(text) + expect(() => r.getByTestId("value")).toThrow() + expect(reloaded).toBeFalsy() + act(() => { + fireEvent.click(r.getByTestId("reload")) + }) + expect(reloaded).toBeTruthy() + act(() => obs.next(WrappedFulfilled.create(text))) + expect(r.getByTestId("value")).toHaveTextContent(text) + }) - test("should resubscribe failed observable", async () => { - const text = Math.random().toString() - let promise: Promise = Promise.reject(text) - const obs = defer(() => promise) + test("should resubscribe failed observable", async () => { + const text = Math.random().toString() + let promise: Promise = Promise.reject(text) + const obs = defer(() => promise) - const r = render( - - component={Test} - value1={obs} - value2="static" - rejected={(e, reload) => } - /> - ) - await delay(100) - expect(r.getByTestId("testing")).toHaveTextContent(text) - expect(() => r.getByTestId("value")).toThrow() - act(() => { - fireEvent.click(r.getByTestId("reload")) - }) - expect(r.getByTestId("testing")).toHaveTextContent(text) - const successText = Math.random().toString() - promise = Promise.resolve(successText) - act(() => { - fireEvent.click(r.getByTestId("reload")) - }) - await delay(100) - expect(r.getByTestId("value")).toHaveTextContent(successText) - }) + const r = render( + + component={Test} + value1={obs} + value2="static" + rejected={(e, reload) => } + />, + ) + await delay(100) + expect(r.getByTestId("testing")).toHaveTextContent(text) + expect(() => r.getByTestId("value")).toThrow() + act(() => { + fireEvent.click(r.getByTestId("reload")) + }) + expect(r.getByTestId("testing")).toHaveTextContent(text) + const successText = Math.random().toString() + promise = Promise.resolve(successText) + act(() => { + fireEvent.click(r.getByTestId("reload")) + }) + await delay(100) + expect(r.getByTestId("value")).toHaveTextContent(successText) + }) - test("should react to props changes", () => { - const text = Math.random().toString() - const r = render() - expect(r.getByTestId("value")).toHaveTextContent(text) - const nextText = Math.random().toString() - r.rerender() - expect(r.getByTestId("value")).toHaveTextContent(nextText) - }) + test("should react to props changes", () => { + const text = Math.random().toString() + const r = render() + expect(r.getByTestId("value")).toHaveTextContent(text) + const nextText = Math.random().toString() + r.rerender() + expect(r.getByTestId("value")).toHaveTextContent(nextText) + }) - test("should resubscribe if observable changes", () => { - const text = Math.random().toString() - let count = 0 - const obs = new Observable(s => { - count = count + 1 - s.next(text) - return () => (count = count - 1) - }) - const r = render() - expect(r.getByTestId("value")).toHaveTextContent(text) + test("should resubscribe if observable changes", () => { + const text = Math.random().toString() + let count = 0 + const obs = new Observable(s => { + count = count + 1 + s.next(text) + return () => (count = count - 1) + }) + const r = render() + expect(r.getByTestId("value")).toHaveTextContent(text) - const nextText = Math.random().toString() - const obs2 = new ReplaySubject(1) - obs2.next(nextText) - expect(count).toBe(1) + const nextText = Math.random().toString() + const obs2 = new ReplaySubject(1) + obs2.next(nextText) + expect(count).toBe(1) - r.rerender() - expect(r.getByTestId("value")).toHaveTextContent(nextText) - expect(count).toBe(0) - }) + r.rerender() + expect(r.getByTestId("value")).toHaveTextContent(nextText) + expect(count).toBe(0) + }) }) async function delay(ms: number) { - await new Promise(r => setTimeout(r, ms)) + await new Promise(r => setTimeout(r, ms)) } diff --git a/packages/react/src/rx-wrapper.ts b/packages/react/src/rx-wrapper.ts index 3ffb46d..ef32b2b 100644 --- a/packages/react/src/rx-wrapper.ts +++ b/packages/react/src/rx-wrapper.ts @@ -1,22 +1,24 @@ -import { ComponentType } from "react" -import { Lifted } from "@rixio/wrapped" -import { RxBaseProps, RxWrapperBase } from "./base" +import type { ComponentType } from "react" +import type { Lifted } from "@rixio/wrapped" +import type { RxBaseProps } from "./base" +import { RxWrapperBase } from "./base" export type RxWrapperProps

= Lifted

& - RxBaseProps & { - component: ComponentType

- } + RxBaseProps & { + component: ComponentType

+ } export class RxWrapper

extends RxWrapperBase> { - extractRxBaseProps(props: RxWrapperProps

): RxBaseProps | undefined { - return this.props - } + extractRxBaseProps(): RxBaseProps | undefined { + return this.props + } - extractProps({ component, ...rest }: RxWrapperProps

): Lifted

{ - return rest as any - } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + extractProps({ component, ...rest }: RxWrapperProps

): Lifted

{ + return rest as any + } - extractComponent({ component }: RxWrapperProps

): any { - return component - } + extractComponent({ component }: RxWrapperProps

): any { + return component + } } diff --git a/packages/react/src/rx.test.tsx b/packages/react/src/rx.test.tsx index bb88dc4..f534d97 100644 --- a/packages/react/src/rx.test.tsx +++ b/packages/react/src/rx.test.tsx @@ -1,251 +1,253 @@ -import React, { ReactElement } from "react" +import type { ReactElement } from "react" +import React from "react" import { act, render, waitFor, fireEvent } from "@testing-library/react" import { Atom } from "@rixio/atom" import { R } from "@rixio/react" -import { BehaviorSubject, Observable, ReplaySubject, defer, Subject } from "rxjs" -import { Wrapped, WrappedFulfilled, WrappedPending } from "@rixio/wrapped" +import type { Observable } from "rxjs" +import { BehaviorSubject, ReplaySubject, defer, Subject } from "rxjs" +import type { Wrapped } from "@rixio/wrapped" +import { WrappedFulfilled, WrappedPending } from "@rixio/wrapped" import { CacheFulfilled, CacheIdle, KeyMemoImpl, MemoImpl, toListLoader } from "@rixio/cache" import { Map as IM } from "immutable" import { Rx } from "./rx" const Testing = ({ text, reload }: { text?: any; reload?: () => void }) => { - return ( - <> - {text || "BLABLABLA"} - - - ) + return ( + <> + {text || "BLABLABLA"} + + + ) } describe("Rx", () => { - test("should display pending if is pending", async () => { - expect.assertions(2) - const state$ = Atom.create>(WrappedPending.create()) - const r = render( - - - {v => {v}} - - - ) - expect(r.getByTestId("test")).toHaveTextContent("pending") - expect(r.getByTestId("test")).not.toHaveTextContent("content") - }) + test("should display pending if is pending", async () => { + expect.assertions(2) + const state$ = Atom.create>(WrappedPending.create()) + const r = render( + + + {v => {v}} + + , + ) + expect(r.getByTestId("test")).toHaveTextContent("pending") + expect(r.getByTestId("test")).not.toHaveTextContent("content") + }) - test("should display content if loaded", async () => { - testCacheState(state$ => ( - - value$={state$} pending="pending"> - {value => {value}} - - - )) - }) + test("should display content if loaded", async () => { + testCacheState(state$ => ( + + value$={state$} pending="pending"> + {value => {value}} + + + )) + }) - test("should display content if simple observable is used", async () => { - const subj = new ReplaySubject(1) - const r = render( - - - - ) - expect(r.getByTestId("test")).toHaveTextContent("pending") - const number = Math.random() - act(() => { - subj.next(number) - }) - await waitFor(() => { - expect(r.getByTestId("test")).toHaveTextContent(number.toString()) - }) - }) + test("should display content if simple observable is used", async () => { + const subj = new ReplaySubject(1) + const r = render( + + + , + ) + expect(r.getByTestId("test")).toHaveTextContent("pending") + const number = Math.random() + act(() => { + subj.next(number) + }) + await waitFor(() => { + expect(r.getByTestId("test")).toHaveTextContent(number.toString()) + }) + }) - test("Rx should work with Memo", async () => { - let value: number = 10 - const cache = new MemoImpl(Atom.create(CacheIdle.create()), () => Promise.resolve(value)) - const r = render( - - - - ) - await waitFor(() => { - expect(r.getByTestId("test")).toHaveTextContent("10") - }) - act(() => { - value = 20 - cache.clear() - }) - await waitFor(() => { - expect(r.getByTestId("test")).toHaveTextContent("20") - }) - act(() => { - cache.atom.set(CacheFulfilled.create(30)) - }) - await waitFor(() => { - expect(r.getByTestId("test")).toHaveTextContent("30") - }) - }) + test("Rx should work with Memo", async () => { + let value: number = 10 + const cache = new MemoImpl(Atom.create(CacheIdle.create()), () => Promise.resolve(value)) + const r = render( + + + , + ) + await waitFor(() => { + expect(r.getByTestId("test")).toHaveTextContent("10") + }) + act(() => { + value = 20 + cache.clear() + }) + await waitFor(() => { + expect(r.getByTestId("test")).toHaveTextContent("20") + }) + act(() => { + cache.atom.set(CacheFulfilled.create(30)) + }) + await waitFor(() => { + expect(r.getByTestId("test")).toHaveTextContent("30") + }) + }) - test("Rx should work with Memo and reload", async () => { - let counter = 0 - const cache = new MemoImpl(Atom.create(CacheIdle.create()), () => { - counter = counter + 1 - return new Promise((resolve, reject) => { - setTimeout(() => (counter <= 1 ? reject("my-error") : resolve("resolved")), 0) - }) - }) - const r = render( - - } /> - - ) - await waitFor(() => { - expect(r.getByTestId("test")).toHaveTextContent("my-error") - }) - act(() => { - fireEvent.click(r.getByTestId("reload")) - }) - await waitFor(() => { - expect(r.getByTestId("test")).toHaveTextContent("resolved") - }) - }) + test("Rx should work with Memo and reload", async () => { + let counter = 0 + const cache = new MemoImpl(Atom.create(CacheIdle.create()), () => { + counter = counter + 1 + return new Promise((resolve, reject) => { + setTimeout(() => (counter <= 1 ? reject("my-error") : resolve("resolved")), 0) + }) + }) + const r = render( + + } /> + , + ) + await waitFor(() => { + expect(r.getByTestId("test")).toHaveTextContent("my-error") + }) + act(() => { + fireEvent.click(r.getByTestId("reload")) + }) + await waitFor(() => { + expect(r.getByTestId("test")).toHaveTextContent("resolved") + }) + }) - test("Rx should work with KeyMemo", async () => { - let value: number = 10 - const cache = new KeyMemoImpl( - Atom.create(IM()), - toListLoader(() => Promise.resolve(value)) - ) - const r = render( - - - - ) - await waitFor(() => { - expect(r.getByTestId("test")).toHaveTextContent("10") - }) - act(() => { - value = 20 - cache.single("key1").clear() - }) - await waitFor(() => { - expect(r.getByTestId("test")).toHaveTextContent("20") - }) - act(() => { - cache.single("key1").atom.set(WrappedFulfilled.create(30)) - }) - await waitFor(() => { - expect(r.getByTestId("test")).toHaveTextContent("30") - }) - }) + test("Rx should work with KeyMemo", async () => { + let value: number = 10 + const cache = new KeyMemoImpl( + Atom.create(IM()), + toListLoader(() => Promise.resolve(value)), + ) + const r = render( + + + , + ) + await waitFor(() => { + expect(r.getByTestId("test")).toHaveTextContent("10") + }) + act(() => { + value = 20 + cache.single("key1").clear() + }) + await waitFor(() => { + expect(r.getByTestId("test")).toHaveTextContent("20") + }) + act(() => { + cache.single("key1").atom.set(WrappedFulfilled.create(30)) + }) + await waitFor(() => { + expect(r.getByTestId("test")).toHaveTextContent("30") + }) + }) - test("should show rejected if error occured after success", async () => { - const subj = new Subject() - const r = render( - - x} /> - - ) - expect(r.getByTestId("test")).toHaveTextContent("pending") - act(() => { - subj.next(10) - }) - await waitFor(() => { - expect(r.getByTestId("test")).toHaveTextContent("10") - }) - act(() => { - subj.error("error1") - }) - await waitFor(() => { - expect(r.getByTestId("test")).toHaveTextContent("error1") - }) - }) + test("should show rejected if error occured after success", async () => { + const subj = new Subject() + const r = render( + + x} /> + , + ) + expect(r.getByTestId("test")).toHaveTextContent("pending") + act(() => { + subj.next(10) + }) + await waitFor(() => { + expect(r.getByTestId("test")).toHaveTextContent("10") + }) + act(() => { + subj.error("error1") + }) + await waitFor(() => { + expect(r.getByTestId("test")).toHaveTextContent("error1") + }) + }) - test("should display error if simple observable is used", async () => { - const subj = new ReplaySubject(1) - const r = render( - - x} /> - - ) - expect(r.getByTestId("test")).toHaveTextContent("pending") - const text = Math.random().toString() - act(() => { - subj.error(text) - }) - await waitFor(() => { - expect(r.getByTestId("test")).toHaveTextContent(text) - }) - }) + test("should display error if simple observable is used", async () => { + const subj = new ReplaySubject(1) + const r = render( + + x} /> + , + ) + expect(r.getByTestId("test")).toHaveTextContent("pending") + const text = Math.random().toString() + act(() => { + subj.error(text) + }) + await waitFor(() => { + expect(r.getByTestId("test")).toHaveTextContent(text) + }) + }) - test("should resubscribe to observable if reloaded", async () => { - const text = Math.random().toString() - let promise: Promise = Promise.reject(text) - const obs = defer(() => promise) - const r = render( - - } /> - - ) - await waitFor(() => { - expect(r.getByTestId("testing")).toHaveTextContent(text) - }) - act(() => { - fireEvent.click(r.getByTestId("reload")) - }) - await waitFor(() => { - expect(r.getByTestId("testing")).toHaveTextContent(text) - }) - const successText = Math.random().toString() - promise = Promise.resolve(successText) - act(() => { - fireEvent.click(r.getByTestId("reload")) - }) - await waitFor(() => { - expect(r.getByTestId("test")).toHaveTextContent(successText) - }) - }) + test("should resubscribe to observable if reloaded", async () => { + const text = Math.random().toString() + let promise: Promise = Promise.reject(text) + const obs = defer(() => promise) + const r = render( + + } /> + , + ) + await waitFor(() => { + expect(r.getByTestId("testing")).toHaveTextContent(text) + }) + act(() => { + fireEvent.click(r.getByTestId("reload")) + }) + await waitFor(() => { + expect(r.getByTestId("testing")).toHaveTextContent(text) + }) + const successText = Math.random().toString() + promise = Promise.resolve(successText) + act(() => { + fireEvent.click(r.getByTestId("reload")) + }) + await waitFor(() => { + expect(r.getByTestId("test")).toHaveTextContent(successText) + }) + }) - test("should display content if children empty", async () => { - testCacheState(state$ => ( - - - - )) - }) + test("should display content if children empty", async () => { + testCacheState(state$ => ( + + + + )) + }) - test("should work if render prop is not used", () => { - testCacheState(state$ => ( - - - simple text -

multiple elements
- {/* eslint-disable-next-line react/jsx-pascal-case */} - - - - )) - }) + test("should work if render prop is not used", () => { + testCacheState(state$ => ( + + + simple text +
multiple elements
+ +
+
+ )) + }) - test("should work with null atom's value", async () => { - const state$ = Atom.create(null) - const r = render( - - {v => {`${v}`}} - - ) - expect(r.getByTestId("test")).toHaveTextContent("null") - }) + test("should work with null atom's value", async () => { + const state$ = Atom.create(null) + const r = render( + + {v => {`${v}`}} + , + ) + expect(r.getByTestId("test")).toHaveTextContent("null") + }) }) function testCacheState(comp: (state: Observable>) => ReactElement) { - const state$ = new BehaviorSubject>(WrappedPending.create()) - const r = render(comp(state$)) - expect(r.getByTestId("test")).toHaveTextContent("pending") - const number = Math.random() - act(() => { - state$.next(WrappedFulfilled.create(number)) - }) - expect(r.getByTestId("test")).toHaveTextContent(number.toString()) + const state$ = new BehaviorSubject>(WrappedPending.create()) + const r = render(comp(state$)) + expect(r.getByTestId("test")).toHaveTextContent("pending") + const number = Math.random() + act(() => { + state$.next(WrappedFulfilled.create(number)) + }) + expect(r.getByTestId("test")).toHaveTextContent(number.toString()) } diff --git a/packages/react/src/rx.tsx b/packages/react/src/rx.tsx index 5680c09..1d276bc 100644 --- a/packages/react/src/rx.tsx +++ b/packages/react/src/rx.tsx @@ -1,36 +1,38 @@ -import React, { ReactNode, useMemo, useState } from "react" -import { combineLatest, OWLike } from "@rixio/wrapped" +import type { ReactNode } from "react" +import React, { useMemo, useState } from "react" +import type { OWLike } from "@rixio/wrapped" +import { combineLatest } from "@rixio/wrapped" import { useRx } from "./use-rx" -import { OrReactChild } from "./base" +import type { OrReactChild } from "./base" export interface RxPropsBase { - pending?: React.ReactNode - rejected?: OrReactChild<(error: any, reload: () => void) => React.ReactNode> + pending?: React.ReactNode + rejected?: OrReactChild<(error: any, reload: () => void) => React.ReactNode> } export interface RxProps extends RxPropsBase { - value$: any - children?: any + value$: any + children?: any } type Rx1Props = { - value$: OWLike - children?: OrReactChild<(value: T, reload: () => {}) => ReactNode> + value$: OWLike + children?: OrReactChild<(value: T, reload: () => {}) => ReactNode> } & RxPropsBase type Rx2Props = { - value$: [OWLike, OWLike] - children?: OrReactChild<(value: [T1, T2], reload: () => {}) => ReactNode> + value$: [OWLike, OWLike] + children?: OrReactChild<(value: [T1, T2], reload: () => {}) => ReactNode> } & RxPropsBase type Rx3Props = { - value$: [OWLike, OWLike, OWLike] - children?: OrReactChild<(value: [T1, T2, T3], reload: () => {}) => ReactNode> + value$: [OWLike, OWLike, OWLike] + children?: OrReactChild<(value: [T1, T2, T3], reload: () => {}) => ReactNode> } & RxPropsBase type Rx4Props = { - value$: [OWLike, OWLike, OWLike, OWLike] - children?: OrReactChild<(value: [T1, T2, T3, T4], reload: () => {}) => ReactNode> + value$: [OWLike, OWLike, OWLike, OWLike] + children?: OrReactChild<(value: [T1, T2, T3, T4], reload: () => {}) => ReactNode> } & RxPropsBase export function Rx(props: Rx1Props): React.ReactElement @@ -38,41 +40,40 @@ export function Rx(props: Rx2Props): React.ReactElement export function Rx(props: Rx3Props): React.ReactElement export function Rx(props: Rx4Props): React.ReactElement export function Rx({ pending, rejected, children, value$ }: RxProps): React.ReactElement | null { - const array = getObservables(value$) - const observables = useMemo(() => { - return combineLatest(array) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, array) - const [nonce, setNonce] = useState(0) - const value = useRx(observables, [observables, nonce]) + const array = getObservables(value$) + const observables = useMemo(() => combineLatest(array), array) + const [nonce, setNonce] = useState(0) + const value = useRx(observables, [observables, nonce]) - switch (value.status) { - case "pending": - return - case "rejected": - if (typeof rejected === "function") { - return ( - { - value.reload() - setNonce(n => n + 1) - })} - /> - ) - } - return - case "fulfilled": - const final = Array.isArray(value$) ? value.value : value.value[0] - if (children) { - if (typeof children === "function") { - return - } - return - } - return - } + switch (value.status) { + case "pending": + return + case "rejected": + if (typeof rejected === "function") { + return ( + { + value.reload() + setNonce(n => n + 1) + })} + /> + ) + } + return + case "fulfilled": + const final = Array.isArray(value$) ? value.value : value.value[0] + if (children) { + if (typeof children === "function") { + return + } + return + } + return + default: + return + } } function getObservables(observables: OWLike | OWLike[]): OWLike[] { - return Array.isArray(observables) ? observables : [observables] + return Array.isArray(observables) ? observables : [observables] } diff --git a/packages/react/src/use-rx.test.tsx b/packages/react/src/use-rx.test.tsx index 26f7d8b..31ed051 100644 --- a/packages/react/src/use-rx.test.tsx +++ b/packages/react/src/use-rx.test.tsx @@ -1,5 +1,6 @@ import React from "react" -import { Observable, of, ReplaySubject } from "rxjs" +import type { Observable } from "rxjs" +import { of, ReplaySubject } from "rxjs" import { render } from "@testing-library/react" import { Atom } from "@rixio/atom" import { act } from "react-dom/test-utils" @@ -7,57 +8,57 @@ import { delay, map, mergeMap } from "rxjs/operators" import { useRx } from "./use-rx" const RxText = ({ value, renders }: { value: Observable; renders: Atom }) => { - const simple = useRx(value) - renders.modify(x => x + 1) - return {simple.status === "fulfilled" ? simple.value : ""} + const simple = useRx(value) + renders.modify(x => x + 1) + return {simple.status === "fulfilled" ? simple.value : ""} } describe("useRx", () => { - test("should render atom 1 time", () => { - const text = Math.random().toString() - const renders = Atom.create(0) - const r = render() - expect(r.getByTestId("value")).toHaveTextContent(text) - expect(renders.get()).toStrictEqual(1) - }) - - test("should render ReplaySubject 1 time", () => { - const renders = Atom.create(0) - const subject = new ReplaySubject(1) - const text = Math.random().toString() - subject.next(text) - const r = render() - expect(r.getByTestId("value")).toHaveTextContent(text) - expect(renders.get()).toStrictEqual(1) - }) - - test("should listen to Atom changes", () => { - testSimple(a => a) - }) - - test("should work with simple map operator", () => { - testSimple(a => a.pipe(map(x => x))) - }) - - test("should work with simple mergeMap operator", () => { - testSimple(a => a.pipe(mergeMap(x => of(x)))) - }) - - test("should not work when there is no immediate value", () => { - expect(() => { - testSimple(a => a.pipe(delay(100))) - }).toThrow() - }) - - function testSimple(preprocessor: (o: Observable) => Observable) { - const renders = Atom.create(0) - const text = Math.random().toString() - const atom = Atom.create(text) - const r = render() - expect(r.getByTestId("value")).toHaveTextContent(text) - const nextText = Math.random().toString() - act(() => atom.set(nextText)) - expect(r.getByTestId("value")).toHaveTextContent(nextText) - expect(renders.get()).toStrictEqual(2) - } + test("should render atom 1 time", () => { + const text = Math.random().toString() + const renders = Atom.create(0) + const r = render() + expect(r.getByTestId("value")).toHaveTextContent(text) + expect(renders.get()).toStrictEqual(1) + }) + + test("should render ReplaySubject 1 time", () => { + const renders = Atom.create(0) + const subject = new ReplaySubject(1) + const text = Math.random().toString() + subject.next(text) + const r = render() + expect(r.getByTestId("value")).toHaveTextContent(text) + expect(renders.get()).toStrictEqual(1) + }) + + test("should listen to Atom changes", () => { + testSimple(a => a) + }) + + test("should work with simple map operator", () => { + testSimple(a => a.pipe(map(x => x))) + }) + + test("should work with simple mergeMap operator", () => { + testSimple(a => a.pipe(mergeMap(x => of(x)))) + }) + + test("should not work when there is no immediate value", () => { + expect(() => { + testSimple(a => a.pipe(delay(100))) + }).toThrow() + }) + + function testSimple(preprocessor: (o: Observable) => Observable) { + const renders = Atom.create(0) + const text = Math.random().toString() + const atom = Atom.create(text) + const r = render() + expect(r.getByTestId("value")).toHaveTextContent(text) + const nextText = Math.random().toString() + act(() => atom.set(nextText)) + expect(r.getByTestId("value")).toHaveTextContent(nextText) + expect(renders.get()).toStrictEqual(2) + } }) diff --git a/packages/react/src/use-rx.ts b/packages/react/src/use-rx.ts index 147c5f7..5b1a876 100644 --- a/packages/react/src/use-rx.ts +++ b/packages/react/src/use-rx.ts @@ -1,60 +1,61 @@ import { useState, useMemo, useRef, useEffect } from "react" -import { Observable } from "rxjs" +import type { Observable } from "rxjs" import { first } from "rxjs/operators" -import { Wrapped, toPlainOrThrow, WrappedFulfilled, WrappedRejected, WrappedPending, OWLike, OW } from "@rixio/wrapped" -import { ReadOnlyAtom } from "@rixio/atom" +import type { Wrapped, OWLike } from "@rixio/wrapped" +import { toPlainOrThrow, WrappedFulfilled, WrappedRejected, WrappedPending, OW } from "@rixio/wrapped" +import type { ReadOnlyAtom } from "@rixio/atom" import { useSubscription } from "./use-subscription" export function getImmediate(observable: Observable): Wrapped { - let immediate: Wrapped = WrappedPending.create() - observable.pipe(first()).subscribe( - value => (immediate = WrappedFulfilled.create(value)), - error => (immediate = WrappedRejected.create(error)) - ) - return immediate + let immediate: Wrapped = WrappedPending.create() + observable.pipe(first()).subscribe( + value => (immediate = WrappedFulfilled.create(value)), + error => (immediate = WrappedRejected.create(error)), + ) + return immediate } export function getImmediateOrThrow(observable: Observable): T { - const immediate = getImmediate(observable) - if (immediate.status === "rejected") throw immediate.error - if (immediate.status !== "fulfilled") throw new Error("Observable doesn't immediately emits value") - return immediate.value + const immediate = getImmediate(observable) + if (immediate.status === "rejected") throw immediate.error + if (immediate.status !== "fulfilled") throw new Error("Observable doesn't immediately emits value") + return immediate.value } -export function useRx(observable: OWLike, deps: any[] = [observable]): Wrapped { - const [, setCount] = useState(0) - const value = useRef>(WrappedPending.create()) - const initial = useRef(true) - const memoized = useMemo(() => new OW(observable), [observable]) +export function useRx(observable: OWLike, customDeps: any[] = [observable]): Wrapped { + const [, setCount] = useState(0) + const value = useRef>(WrappedPending.create()) + const initial = useRef(true) + const memoized = useMemo(() => new OW(observable), [observable]) - const sub = useMemo( - () => - memoized.subscribe(next => { - const current = value.current - value.current = next - if (!initial.current) { - if ( - current.status !== next.status || - (current.status === "fulfilled" && next.status === "fulfilled" && current.value !== next.value) - ) { - setCount(c => c + 1) - } - } - }), - // eslint-disable-next-line react-hooks/exhaustive-deps - deps - ) - useEffect(() => () => sub.unsubscribe(), [sub]) - initial.current = false - return value.current + const sub = useMemo( + () => + memoized.subscribe(next => { + const current = value.current + value.current = next + if (!initial.current) { + if ( + current.status !== next.status || + (current.status === "fulfilled" && next.status === "fulfilled" && current.value !== next.value) + ) { + setCount(c => c + 1) + } + } + }), + customDeps, + ) + useEffect(() => () => sub.unsubscribe(), [sub]) + initial.current = false + + return value.current } export function useRxOrThrow(observable: OWLike): T { - return toPlainOrThrow(useRx(observable)) + return toPlainOrThrow(useRx(observable)) } export function useAtom(atom: ReadOnlyAtom): T { - const [state, setState] = useState(() => atom.get()) - useSubscription(atom, setState) - return state + const [state, setState] = useState(() => atom.get()) + useSubscription(atom, setState) + return state } diff --git a/packages/react/src/use-subscription.ts b/packages/react/src/use-subscription.ts index df78133..e96d42e 100644 --- a/packages/react/src/use-subscription.ts +++ b/packages/react/src/use-subscription.ts @@ -1,23 +1,18 @@ -import { Observable, PartialObserver } from "rxjs" +import type { Observable, PartialObserver } from "rxjs" import { useEffect } from "react" export function useSubscription( - observable: Observable, - observer?: PartialObserver | ((value: T) => void), - deps: any[] = [observable] + observable: Observable, + observer?: PartialObserver | ((value: T) => void), + customDeps: any[] = [observable], ) { - useEffect(() => { - if (typeof observer === "function") { - const s = observable.subscribe(observer) - return () => { - s.unsubscribe() - } - } else { - const s = observable.subscribe(observer) - return () => { - s.unsubscribe() - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, deps) + useEffect(() => { + if (typeof observer === "function") { + const s = observable.subscribe(observer) + return () => s.unsubscribe() + } else { + const s = observable.subscribe(observer) + return () => s.unsubscribe() + } + }, customDeps) } diff --git a/packages/wrapped/src/domain.ts b/packages/wrapped/src/domain.ts index 602c57e..356fa95 100644 --- a/packages/wrapped/src/domain.ts +++ b/packages/wrapped/src/domain.ts @@ -1,36 +1,37 @@ -import { noop, Observable } from "rxjs" +import type { Observable } from "rxjs" +import { noop } from "rxjs" export class WrappedBase {} export class WrappedFulfilled extends WrappedBase { - readonly status = "fulfilled" - constructor(public readonly value: T) { - super() - } - static create = (value: T) => new WrappedFulfilled(value) + readonly status = "fulfilled" + constructor(public readonly value: T) { + super() + } + static create = (value: T) => new WrappedFulfilled(value) } export class WrappedPending extends WrappedBase { - readonly status = "pending" - static create = () => new WrappedPending() + readonly status = "pending" + static create = () => new WrappedPending() } export class WrappedRejected extends WrappedBase { - readonly status = "rejected" - constructor(public readonly error: unknown, public readonly reload: () => void = noop) { - super() - } + readonly status = "rejected" + constructor(public readonly error: unknown, public readonly reload: () => void = noop) { + super() + } - static create = (error: unknown, reload: () => void = noop) => new WrappedRejected(error, reload) + static create = (error: unknown, reload: () => void = noop) => new WrappedRejected(error, reload) } export type Wrapped = WrappedFulfilled | WrappedPending | WrappedRejected export type ObservableLike = T | Observable | Observable> export function isWrapped(value: unknown): value is Wrapped { - return value instanceof WrappedBase + return value instanceof WrappedBase } export type Lifted = { - [K in keyof T]: ObservableLike + [K in keyof T]: ObservableLike } diff --git a/packages/wrapped/src/operators/index.test.ts b/packages/wrapped/src/operators/index.test.ts index 7e436b2..13c7c79 100644 --- a/packages/wrapped/src/operators/index.test.ts +++ b/packages/wrapped/src/operators/index.test.ts @@ -1,331 +1,332 @@ import { identity, of, Subject, Subscription, throwError } from "rxjs" -import { Wrapped, WrappedFulfilled, WrappedRejected } from "../domain" +import type { Wrapped } from "../domain" +import { WrappedFulfilled, WrappedRejected } from "../domain" import { catchError, combineLatest, filter, flatMap, map, switchMap, unwrap, from, defer } from "./index" describe("operators", () => { - test("map should work with plain observables", () => { - const s = new Subject() - const emitted: Wrapped[] = [] - const sub = s.pipe(map(x => `${x}`)).subscribe(v => emitted.push(v)) - expect(emitted.length).toBe(1) - expect(emitted[0].status).toBe("pending") - - s.next(1) - expect(emitted.length).toBe(2) - expectValue(emitted[1], "1") - - sub.unsubscribe() - }) - - test("map should catch error in mapper function", () => { - const s = new Subject() - const ERROR = "error" - const emitted: Wrapped[] = [] - const sub = s - .pipe( - map(() => { - throw new Error(ERROR) - }) - ) - .subscribe(v => emitted.push(v)) - expect(emitted.length).toBe(1) - expect(emitted[0].status).toBe("pending") - - s.next(1) - expect(emitted.length).toBe(2) - expect(emitted[1].status).toBe("rejected") - expect((emitted[1] as any).error.message).toBe(ERROR) - - sub.unsubscribe() - }) - - test("combineLatest should lift plain observables", () => { - const n = new Subject() - const s = new Subject() - - const emitted: Wrapped<[number, string]>[] = [] - const sub = combineLatest([n, s]).subscribe(v => emitted.push(v)) - expect(emitted.length).toBe(1) - expect(emitted[0].status).toBe("pending") - - n.next(1) - expect(emitted.length).toBe(2) - expect(emitted[1].status).toBe("pending") - - s.next("s1") - expect(emitted.length).toBe(3) - expect(emitted[2].status).toBe("fulfilled") - expect((emitted[2] as any).value).toStrictEqual([1, "s1"]) - - s.error("reason-string") - expect(emitted.length).toBe(4) - expect(emitted[3].status).toBe("rejected") - expect((emitted[3] as any).error).toBe("reason-string") - - sub.unsubscribe() - }) - - test("combineLatest should work with empty arrays", async () => { - const emitted: Wrapped<[]>[] = [] - const sub = combineLatest([]).subscribe(v => emitted.push(v)) - expect(emitted.length).toBe(1) - expect(emitted[0].status).toBe("fulfilled") - expect((emitted[0] as any).value).toStrictEqual([]) - - sub.unsubscribe() - }) - - test("flatMap should work with Promises", async () => { - const s = new Subject() - - const emitted: Wrapped[] = [] - const sub = s.pipe(flatMap(x => delay(100).then(() => parseInt(x)))).subscribe(v => emitted.push(v)) - expect(emitted.length).toBe(1) - expect(emitted[0].status).toBe("pending") - - s.next("1") - expect(emitted.length).toBe(1) - - await delay(120) - expect(emitted.length).toBe(2) - expectValue(emitted[1], 1) - sub.unsubscribe() - }) - - test("flatMap should save reload function", async () => { - const s = new Subject>() - - const emitted: Wrapped[] = [] - const sub = s.pipe(flatMap(x => delay(100).then(() => parseInt(x)))).subscribe(v => emitted.push(v)) - expect(emitted.length).toBe(1) - expect(emitted[0].status).toBe("pending") - - const reload = jest.fn() - s.next(WrappedRejected.create("reason1", reload)) - expect(emitted.length).toBe(2) - expect(emitted[1].status).toBe("rejected") - const rej = emitted[1] as WrappedRejected - expect(rej.error).toBe("reason1") - expect(reload.mock.calls.length).toEqual(0) - rej.reload() - expect(reload.mock.calls.length).toEqual(1) - - sub.unsubscribe() - }) - - test("flatMap shouldn't cancel previous emits", async () => { - const s = new Subject>() - - let index = 0 - const emitted: Wrapped[] = [] - const sub = s - .pipe( - flatMap(async x => { - if (index === 0) { - index = index + 1 - await delay(100) - } - return parseInt(x) - }) - ) - .subscribe(v => emitted.push(v)) - expect(emitted.length).toBe(1) - expect(emitted[0].status).toBe("pending") - - s.next(WrappedFulfilled.create("10")) - s.next(WrappedFulfilled.create("15")) - - await delay(16) - expect(emitted.length).toBe(2) - expectValue(emitted[1], 15) - - await delay(100) - expect(emitted.length).toBe(3) - expectValue(emitted[2], 10) - expect(emitted[2].status).toBe("fulfilled") - expect((emitted[2] as any).value).toBe(10) - - sub.unsubscribe() - }) - - test("switchMap should save reload function", () => { - const s = new Subject>() - - const emitted: Wrapped[] = [] - const sub = s.pipe(switchMap(x => delay(100).then(() => parseInt(x)))).subscribe(v => emitted.push(v)) - expect(emitted.length).toBe(1) - expect(emitted[0].status).toBe("pending") - - const reload = jest.fn() - s.next(WrappedRejected.create("reason1", reload)) - expect(emitted.length).toBe(2) - expect(emitted[1].status).toBe("rejected") - const rej = emitted[1] as WrappedRejected - expect(rej.error).toBe("reason1") - expect(reload.mock.calls.length).toEqual(0) - rej.reload() - expect(reload.mock.calls.length).toEqual(1) - - sub.unsubscribe() - }) - - test("switchMap should cancel previous emits", async () => { - const s = new Subject>() - - let index = 0 - const emitted: Wrapped[] = [] - const sub = s - .pipe( - switchMap(async x => { - if (index === 0) { - index = index + 1 - await delay(100) - } - return parseInt(x) - }) - ) - .subscribe(v => emitted.push(v)) - expect(emitted.length).toBe(1) - expect(emitted[0].status).toBe("pending") - - s.next(WrappedFulfilled.create("10")) - s.next(WrappedFulfilled.create("15")) - - await delay(120) - expect(emitted.length).toBe(2) - expectValue(emitted[1], 15) - - sub.unsubscribe() - }) - - test("filter should filter out even numbers", () => { - const s = new Subject>() - - const emitted: Wrapped[] = [] - const sub = s.pipe(filter(x => x % 2 === 0)).subscribe(v => emitted.push(v)) - expect(emitted.length).toBe(1) - expect(emitted[0].status).toBe("pending") - - s.next(WrappedFulfilled.create(5)) - expect(emitted.length).toBe(1) - - s.next(WrappedFulfilled.create(10)) - expect(emitted.length).toBe(2) - expectValue(emitted[1], 10) - - sub.unsubscribe() - }) - - test("catchError should map rejected status of Wrapped", () => { - const catchMapper = (x: unknown) => { - if (typeof x === "number") return of(x + 2) - return throwError("Non-parasable value") - } - const s = new Subject>() - const values: Wrapped[] = [] - const sub = s.pipe(catchError(catchMapper)).subscribe(x => values.push(x)) - expect(values.length).toBe(1) - expect(values[0].status).toBe("pending") - - s.next(WrappedFulfilled.create(1)) - expect(values.length).toBe(2) - expectValue(values[1], 1) - s.next(WrappedRejected.create(1)) - expect(values.length).toBe(3) - expectValue(values[2], 3) - - sub.unsubscribe() - }) - - test("unwrap should accept WrappedObservable and return Observable", () => { - const s = new Subject() - const values: number[] = [] - const errors: Array = [] - const sub = s.pipe(map(identity), unwrap()).subscribe( - x => values.push(x), - x => errors.push(x) - ) - expect(values.length).toBe(0) - - s.next(1) - expect(values.length).toBe(1) - expect(values[0]).toBe(1) - - sub.unsubscribe() - }) - - test("unwrap should throw error if receive rejected status", () => { - const s = new Subject>() - const original = s.pipe(map(identity)) - const unwrapped = original.pipe(unwrap()) - const originalValues: Wrapped[] = [] - const values: number[] = [] - const unwrappedErrors: any[] = [] - const sub = new Subscription() - sub.add( - unwrapped.subscribe( - x => values.push(x), - x => unwrappedErrors.push(x) - ) - ) - sub.add(original.subscribe(x => originalValues.push(x))) - expect(values.length).toBe(0) - expect(originalValues.length).toBe(1) - expect(originalValues[0].status).toBe("pending") - - const ERROR = "error" - s.next(WrappedRejected.create(ERROR)) - expect(unwrappedErrors.length).toBe(1) - expect(unwrappedErrors[0]).toBe(ERROR) - - sub.unsubscribe() - }) - - test("from should receive value after promise fulfill", async () => { - const promise = delay(100).then(() => 10) - const emitted: Wrapped[] = [] - const sub = from(promise).subscribe(x => emitted.push(x)) - expect(emitted.length).toEqual(1) - expect(emitted[0].status).toEqual("pending") - - await delay(120) - expect(emitted.length).toEqual(2) - expectValue(emitted[1], 10) - - sub.unsubscribe() - }) - - test("defer should receive value only after subscribe", async () => { - let called = false - const getSomething = async () => { - called = true - await delay(100) - return 10 - } - const emitted: Wrapped[] = [] - const observable = defer(() => getSomething()) - - await delay(120) - expect(called).toEqual(false) - - const sub = observable.subscribe(x => emitted.push(x)) - expect(called).toEqual(true) - expect(emitted.length).toEqual(1) - expect(emitted[0].status).toEqual("pending") - - await delay(120) - expect(emitted.length).toEqual(2) - expectValue(emitted[1], 10) - - sub.unsubscribe() - }) + test("map should work with plain observables", () => { + const s = new Subject() + const emitted: Wrapped[] = [] + const sub = s.pipe(map(x => `${x}`)).subscribe(v => emitted.push(v)) + expect(emitted.length).toBe(1) + expect(emitted[0].status).toBe("pending") + + s.next(1) + expect(emitted.length).toBe(2) + expectValue(emitted[1], "1") + + sub.unsubscribe() + }) + + test("map should catch error in mapper function", () => { + const s = new Subject() + const ERROR = "error" + const emitted: Wrapped[] = [] + const sub = s + .pipe( + map(() => { + throw new Error(ERROR) + }), + ) + .subscribe(v => emitted.push(v)) + expect(emitted.length).toBe(1) + expect(emitted[0].status).toBe("pending") + + s.next(1) + expect(emitted.length).toBe(2) + expect(emitted[1].status).toBe("rejected") + expect((emitted[1] as any).error.message).toBe(ERROR) + + sub.unsubscribe() + }) + + test("combineLatest should lift plain observables", () => { + const n = new Subject() + const s = new Subject() + + const emitted: Wrapped<[number, string]>[] = [] + const sub = combineLatest([n, s]).subscribe(v => emitted.push(v)) + expect(emitted.length).toBe(1) + expect(emitted[0].status).toBe("pending") + + n.next(1) + expect(emitted.length).toBe(2) + expect(emitted[1].status).toBe("pending") + + s.next("s1") + expect(emitted.length).toBe(3) + expect(emitted[2].status).toBe("fulfilled") + expect((emitted[2] as any).value).toStrictEqual([1, "s1"]) + + s.error("reason-string") + expect(emitted.length).toBe(4) + expect(emitted[3].status).toBe("rejected") + expect((emitted[3] as any).error).toBe("reason-string") + + sub.unsubscribe() + }) + + test("combineLatest should work with empty arrays", async () => { + const emitted: Wrapped<[]>[] = [] + const sub = combineLatest([]).subscribe(v => emitted.push(v)) + expect(emitted.length).toBe(1) + expect(emitted[0].status).toBe("fulfilled") + expect((emitted[0] as any).value).toStrictEqual([]) + + sub.unsubscribe() + }) + + test("flatMap should work with Promises", async () => { + const s = new Subject() + + const emitted: Wrapped[] = [] + const sub = s.pipe(flatMap(x => delay(100).then(() => parseInt(x)))).subscribe(v => emitted.push(v)) + expect(emitted.length).toBe(1) + expect(emitted[0].status).toBe("pending") + + s.next("1") + expect(emitted.length).toBe(1) + + await delay(120) + expect(emitted.length).toBe(2) + expectValue(emitted[1], 1) + sub.unsubscribe() + }) + + test("flatMap should save reload function", async () => { + const s = new Subject>() + + const emitted: Wrapped[] = [] + const sub = s.pipe(flatMap(x => delay(100).then(() => parseInt(x)))).subscribe(v => emitted.push(v)) + expect(emitted.length).toBe(1) + expect(emitted[0].status).toBe("pending") + + const reload = jest.fn() + s.next(WrappedRejected.create("reason1", reload)) + expect(emitted.length).toBe(2) + expect(emitted[1].status).toBe("rejected") + const rej = emitted[1] as WrappedRejected + expect(rej.error).toBe("reason1") + expect(reload.mock.calls.length).toEqual(0) + rej.reload() + expect(reload.mock.calls.length).toEqual(1) + + sub.unsubscribe() + }) + + test("flatMap shouldn't cancel previous emits", async () => { + const s = new Subject>() + + let index = 0 + const emitted: Wrapped[] = [] + const sub = s + .pipe( + flatMap(async x => { + if (index === 0) { + index = index + 1 + await delay(100) + } + return parseInt(x) + }), + ) + .subscribe(v => emitted.push(v)) + expect(emitted.length).toBe(1) + expect(emitted[0].status).toBe("pending") + + s.next(WrappedFulfilled.create("10")) + s.next(WrappedFulfilled.create("15")) + + await delay(16) + expect(emitted.length).toBe(2) + expectValue(emitted[1], 15) + + await delay(100) + expect(emitted.length).toBe(3) + expectValue(emitted[2], 10) + expect(emitted[2].status).toBe("fulfilled") + expect((emitted[2] as any).value).toBe(10) + + sub.unsubscribe() + }) + + test("switchMap should save reload function", () => { + const s = new Subject>() + + const emitted: Wrapped[] = [] + const sub = s.pipe(switchMap(x => delay(100).then(() => parseInt(x)))).subscribe(v => emitted.push(v)) + expect(emitted.length).toBe(1) + expect(emitted[0].status).toBe("pending") + + const reload = jest.fn() + s.next(WrappedRejected.create("reason1", reload)) + expect(emitted.length).toBe(2) + expect(emitted[1].status).toBe("rejected") + const rej = emitted[1] as WrappedRejected + expect(rej.error).toBe("reason1") + expect(reload.mock.calls.length).toEqual(0) + rej.reload() + expect(reload.mock.calls.length).toEqual(1) + + sub.unsubscribe() + }) + + test("switchMap should cancel previous emits", async () => { + const s = new Subject>() + + let index = 0 + const emitted: Wrapped[] = [] + const sub = s + .pipe( + switchMap(async x => { + if (index === 0) { + index = index + 1 + await delay(100) + } + return parseInt(x) + }), + ) + .subscribe(v => emitted.push(v)) + expect(emitted.length).toBe(1) + expect(emitted[0].status).toBe("pending") + + s.next(WrappedFulfilled.create("10")) + s.next(WrappedFulfilled.create("15")) + + await delay(120) + expect(emitted.length).toBe(2) + expectValue(emitted[1], 15) + + sub.unsubscribe() + }) + + test("filter should filter out even numbers", () => { + const s = new Subject>() + + const emitted: Wrapped[] = [] + const sub = s.pipe(filter(x => x % 2 === 0)).subscribe(v => emitted.push(v)) + expect(emitted.length).toBe(1) + expect(emitted[0].status).toBe("pending") + + s.next(WrappedFulfilled.create(5)) + expect(emitted.length).toBe(1) + + s.next(WrappedFulfilled.create(10)) + expect(emitted.length).toBe(2) + expectValue(emitted[1], 10) + + sub.unsubscribe() + }) + + test("catchError should map rejected status of Wrapped", () => { + const catchMapper = (x: unknown) => { + if (typeof x === "number") return of(x + 2) + return throwError("Non-parasable value") + } + const s = new Subject>() + const values: Wrapped[] = [] + const sub = s.pipe(catchError(catchMapper)).subscribe(x => values.push(x)) + expect(values.length).toBe(1) + expect(values[0].status).toBe("pending") + + s.next(WrappedFulfilled.create(1)) + expect(values.length).toBe(2) + expectValue(values[1], 1) + s.next(WrappedRejected.create(1)) + expect(values.length).toBe(3) + expectValue(values[2], 3) + + sub.unsubscribe() + }) + + test("unwrap should accept WrappedObservable and return Observable", () => { + const s = new Subject() + const values: number[] = [] + const errors: Array = [] + const sub = s.pipe(map(identity), unwrap()).subscribe( + x => values.push(x), + x => errors.push(x), + ) + expect(values.length).toBe(0) + + s.next(1) + expect(values.length).toBe(1) + expect(values[0]).toBe(1) + + sub.unsubscribe() + }) + + test("unwrap should throw error if receive rejected status", () => { + const s = new Subject>() + const original = s.pipe(map(identity)) + const unwrapped = original.pipe(unwrap()) + const originalValues: Wrapped[] = [] + const values: number[] = [] + const unwrappedErrors: any[] = [] + const sub = new Subscription() + sub.add( + unwrapped.subscribe( + x => values.push(x), + x => unwrappedErrors.push(x), + ), + ) + sub.add(original.subscribe(x => originalValues.push(x))) + expect(values.length).toBe(0) + expect(originalValues.length).toBe(1) + expect(originalValues[0].status).toBe("pending") + + const ERROR = "error" + s.next(WrappedRejected.create(ERROR)) + expect(unwrappedErrors.length).toBe(1) + expect(unwrappedErrors[0]).toBe(ERROR) + + sub.unsubscribe() + }) + + test("from should receive value after promise fulfill", async () => { + const promise = delay(100).then(() => 10) + const emitted: Wrapped[] = [] + const sub = from(promise).subscribe(x => emitted.push(x)) + expect(emitted.length).toEqual(1) + expect(emitted[0].status).toEqual("pending") + + await delay(120) + expect(emitted.length).toEqual(2) + expectValue(emitted[1], 10) + + sub.unsubscribe() + }) + + test("defer should receive value only after subscribe", async () => { + let called = false + const getSomething = async () => { + called = true + await delay(100) + return 10 + } + const emitted: Wrapped[] = [] + const observable = defer(() => getSomething()) + + await delay(120) + expect(called).toEqual(false) + + const sub = observable.subscribe(x => emitted.push(x)) + expect(called).toEqual(true) + expect(emitted.length).toEqual(1) + expect(emitted[0].status).toEqual("pending") + + await delay(120) + expect(emitted.length).toEqual(2) + expectValue(emitted[1], 10) + + sub.unsubscribe() + }) }) function delay(timeout: number): Promise { - return new Promise(resolve => setTimeout(resolve, timeout)) + return new Promise(resolve => setTimeout(resolve, timeout)) } function expectValue(value: Wrapped, expectedValue: T) { - expect(value.status).toEqual("fulfilled") - expect((value as WrappedFulfilled).value).toEqual(expectedValue) + expect(value.status).toEqual("fulfilled") + expect((value as WrappedFulfilled).value).toEqual(expectedValue) } diff --git a/packages/wrapped/src/operators/index.ts b/packages/wrapped/src/operators/index.ts index 6964076..3a9f8ea 100644 --- a/packages/wrapped/src/operators/index.ts +++ b/packages/wrapped/src/operators/index.ts @@ -1,143 +1,161 @@ import * as rxjs from "rxjs" import * as operators from "rxjs/operators" -import { WrappedFulfilled, WrappedRejected, WrappedPending, Wrapped } from "../domain" -import { OW, OWLike } from "../ow" +import type { Wrapped } from "../domain" +import { WrappedFulfilled, WrappedRejected, WrappedPending } from "../domain" +import type { OWLike } from "../ow" +import { OW } from "../ow" import { toWrapped } from "../utils" type F = (value: T) => R export function map(mapper: (value: T) => R): F, OW> { - return x => - new OW(x).pipe( - operators.map(v => { - if (v.status === "fulfilled") { - try { - return WrappedFulfilled.create(mapper(v.value)) - } catch (error) { - return WrappedRejected.create(error) - } - } - return v - }) - ) + return x => + new OW(x).pipe( + operators.map(v => { + if (v.status === "fulfilled") { + try { + return WrappedFulfilled.create(mapper(v.value)) + } catch (error) { + return WrappedRejected.create(error) + } + } + return v + }), + ) } type InferFromTuple = { - [I in keyof T]: T[I] extends OWLike ? T : unknown + [I in keyof T]: T[I] extends OWLike ? T : unknown } export function combineLatest[]]>(array: [...Ts]): OW> { - if (array.length === 0) { - return (new OW([]) as unknown) as OW> - } + if (array.length === 0) { + return new OW([]) as unknown as OW> + } - return rxjs.combineLatest(array.map(x => new OW(x))).pipe( - operators.map(results => { - let pending = false - const rejected: WrappedRejected[] = [] - const combined = new Array(results.length) as InferFromTuple - results.forEach((w, i) => { - switch (w.status) { - case "pending": - pending = true - break - case "rejected": - rejected.push(w) - break - case "fulfilled": - combined[i] = w.value - break - } - }) - if (rejected.length > 0) return WrappedRejected.create(rejected[0].error, () => rejected.forEach(r => r.reload())) - if (pending) return WrappedPending.create() - return WrappedFulfilled.create(combined) - }) - ) + return rxjs.combineLatest(array.map(x => new OW(x))).pipe( + operators.map(results => { + let pending = false + const rejected: WrappedRejected[] = [] + const combined = new Array(results.length) as InferFromTuple + results.forEach((w, i) => { + switch (w.status) { + case "pending": + pending = true + break + case "rejected": + rejected.push(w) + break + case "fulfilled": + combined[i] = w.value + break + default: + break + } + }) + if (rejected.length > 0) return WrappedRejected.create(rejected[0].error, () => rejected.forEach(r => r.reload())) + if (pending) return WrappedPending.create() + return WrappedFulfilled.create(combined) + }), + ) } export function flatMap(mapper: (value: T) => rxjs.ObservableInput | R>): F, OW> { - return x => - new OW(x).pipe( - operators.mergeMap(v => { - switch (v.status) { - case "pending": - case "rejected": - return rxjs.of(v) - case "fulfilled": - return rxjs.from(mapper(v.value)).pipe(operators.map(toWrapped)) - } - }) - ) + return x => + new OW(x).pipe( + operators.mergeMap(v => { + switch (v.status) { + case "pending": + case "rejected": + return rxjs.of(v) + case "fulfilled": + return rxjs.from(mapper(v.value)).pipe(operators.map(toWrapped)) + default: + return rxjs.throwError(new UnknownWrappedStatus()) + } + }), + ) } export function switchMap(mapper: (value: T) => rxjs.ObservableInput | R>): F, OW> { - return x => - new OW(x).pipe( - operators.switchMap(v => { - switch (v.status) { - case "pending": - case "rejected": - return rxjs.of(v) - case "fulfilled": - return rxjs.from(mapper(v.value)).pipe(operators.map(toWrapped)) - } - }) - ) + return x => + new OW(x).pipe( + operators.switchMap(v => { + switch (v.status) { + case "pending": + case "rejected": + return rxjs.of(v) + case "fulfilled": + return rxjs.from(mapper(v.value)).pipe(operators.map(toWrapped)) + default: + return rxjs.throwError(new UnknownWrappedStatus()) + } + }), + ) } export function filter(predicate: (value: T, index: number) => boolean): F, OW> { - return x => { - let index = 0 - return new OW(x).pipe( - flatMap(x => { - if (predicate(x, index)) { - index = index + 1 - return rxjs.of(x) - } - return rxjs.EMPTY - }) - ) - } + return x => { + let index = 0 + return new OW(x).pipe( + flatMap(x => { + if (predicate(x, index)) { + index = index + 1 + return rxjs.of(x) + } + return rxjs.EMPTY + }), + ) + } } export function catchError( - mapper: (error: unknown) => rxjs.ObservableInput | O> + mapper: (error: unknown) => rxjs.ObservableInput | O>, ): F, OW> { - return x => - new OW(x).pipe( - operators.mergeMap(v => { - switch (v.status) { - case "fulfilled": - case "pending": - return rxjs.of(v) - case "rejected": - return rxjs.from(mapper(v.error)).pipe(operators.map(toWrapped)) - } - }) - ) + return x => + new OW(x).pipe( + operators.mergeMap(v => { + switch (v.status) { + case "fulfilled": + case "pending": + return rxjs.of(v) + case "rejected": + return rxjs.from(mapper(v.error)).pipe(operators.map(toWrapped)) + default: + return rxjs.throwError(new UnknownWrappedStatus()) + } + }), + ) } export function unwrap(): F, rxjs.Observable> { - return x => - x.pipe( - operators.mergeMap(v => { - switch (v.status) { - case "fulfilled": - return rxjs.of(v.value) - case "pending": - return rxjs.NEVER - case "rejected": { - return rxjs.throwError(v.error) - } - } - }) - ) + return x => + x.pipe( + operators.mergeMap(v => { + switch (v.status) { + case "fulfilled": + return rxjs.of(v.value) + case "pending": + return rxjs.NEVER + case "rejected": + return rxjs.throwError(v.error) + default: + return rxjs.throwError(new UnknownWrappedStatus()) + } + }), + ) } export function from(input: rxjs.ObservableInput): OW { - return new OW(rxjs.from(input)) + return new OW(rxjs.from(input)) } export function defer(factory: () => rxjs.ObservableInput): OW { - return new OW(rxjs.defer(factory)) + return new OW(rxjs.defer(factory)) +} + +class UnknownWrappedStatus extends Error { + constructor() { + super("Unknown Wrapped status") + this.name = "UnknownWrappedStatus" + } } diff --git a/packages/wrapped/src/ow/index.test.ts b/packages/wrapped/src/ow/index.test.ts index 635cf7a..ee4a568 100644 --- a/packages/wrapped/src/ow/index.test.ts +++ b/packages/wrapped/src/ow/index.test.ts @@ -1,102 +1,103 @@ import { defer, noop, Subject, Subscription } from "rxjs" import { shareReplay } from "rxjs/operators" -import { WrappedFulfilled, WrappedRejected, Wrapped } from "../domain" +import type { Wrapped } from "../domain" +import { WrappedFulfilled, WrappedRejected } from "../domain" import { OW } from "./index" describe("OW", () => { - test("should wrap plain observable", () => { - const s = new Subject() - const wrapped = new OW(s) - const emitted: Array> = [] - const sub = wrapped.subscribe(v => emitted.push(v)) - expect(emitted.length).toBe(1) - expect(emitted[0].status).toBe("pending") - - s.next(1) - expect(emitted.length).toBe(2) - const em2 = emitted[1] - expect(em2.status).toBe("fulfilled") - expect((em2 as any).value).toBe(1) - - s.error("reason") - expect(emitted.length).toBe(3) - const em3 = emitted[2] - expect(em3.status).toBe("rejected") - expect((em3 as any).error).toBe("reason") - - sub.unsubscribe() - }) - - test("should skip double pending emit", async () => { - const loader = jest.fn().mockImplementation(() => delay(100).then(() => true)) - const wrapped = new OW(defer(() => loader()).pipe(shareReplay(1))) - expect(loader.mock.calls.length).toEqual(0) - - const emitted: Array> = [] - const sub = new Subscription() - sub.add(wrapped.subscribe(noop)) - sub.add(wrapped.subscribe(x => emitted.push(x as any))) - expect(loader.mock.calls.length).toEqual(1) - expect(emitted.length).toEqual(1) - expect(emitted[0].status).toEqual("pending") - await delay(120) - expect(emitted.length).toEqual(2) - expect(emitted[1].status).toEqual("fulfilled") - - sub.unsubscribe() - }) - - test("should reject with error", async () => { - const err = new Error("My error") - const loader = jest.fn().mockImplementation(() => delay(100).then(() => Promise.reject(err))) - const wrapped = new OW(defer(() => loader()).pipe(shareReplay(1))) - expect(loader.mock.calls.length).toEqual(0) - - const emitted: Array> = [] - const sub = new Subscription() - sub.add(wrapped.subscribe(noop)) - sub.add(wrapped.subscribe(x => emitted.push(x as any))) - expect(loader.mock.calls.length).toEqual(1) - expect(emitted.length).toEqual(1) - expect(emitted[0].status).toEqual("pending") - await delay(120) - expect(emitted.length).toEqual(2) - expect(emitted[1].status).toEqual("rejected") - expect((emitted[1] as WrappedRejected).error).toEqual(err) - - sub.unsubscribe() - }) - - test("should do nothing if it's already wrapped", () => { - const s = new Subject>() - const wrapped = new OW(s) - const emitted: Array> = [] - const sub = wrapped.subscribe(v => emitted.push(v)) - expect(emitted.length).toBe(1) - expect(emitted[0].status).toBe("pending") - - s.next(WrappedFulfilled.create(1)) - expect(emitted.length).toBe(2) - const em2 = emitted[1] - expect(em2.status).toBe("fulfilled") - expect((em2 as any).value).toBe(1) - - s.next(WrappedRejected.create("reason")) - expect(emitted.length).toBe(3) - const em3 = emitted[2] - expect(em3.status).toBe("rejected") - expect((em3 as any).error).toBe("reason") - - s.next(WrappedRejected.create("reason2")) - expect(emitted.length).toBe(4) - const em4 = emitted[3] - expect(em4.status).toBe("rejected") - expect((em4 as any).error).toBe("reason2") - - sub.unsubscribe() - }) + test("should wrap plain observable", () => { + const s = new Subject() + const wrapped = new OW(s) + const emitted: Array> = [] + const sub = wrapped.subscribe(v => emitted.push(v)) + expect(emitted.length).toBe(1) + expect(emitted[0].status).toBe("pending") + + s.next(1) + expect(emitted.length).toBe(2) + const em2 = emitted[1] + expect(em2.status).toBe("fulfilled") + expect((em2 as any).value).toBe(1) + + s.error("reason") + expect(emitted.length).toBe(3) + const em3 = emitted[2] + expect(em3.status).toBe("rejected") + expect((em3 as any).error).toBe("reason") + + sub.unsubscribe() + }) + + test("should skip double pending emit", async () => { + const loader = jest.fn().mockImplementation(() => delay(100).then(() => true)) + const wrapped = new OW(defer(() => loader()).pipe(shareReplay(1))) + expect(loader.mock.calls.length).toEqual(0) + + const emitted: Array> = [] + const sub = new Subscription() + sub.add(wrapped.subscribe(noop)) + sub.add(wrapped.subscribe(x => emitted.push(x as any))) + expect(loader.mock.calls.length).toEqual(1) + expect(emitted.length).toEqual(1) + expect(emitted[0].status).toEqual("pending") + await delay(120) + expect(emitted.length).toEqual(2) + expect(emitted[1].status).toEqual("fulfilled") + + sub.unsubscribe() + }) + + test("should reject with error", async () => { + const err = new Error("My error") + const loader = jest.fn().mockImplementation(() => delay(100).then(() => Promise.reject(err))) + const wrapped = new OW(defer(() => loader()).pipe(shareReplay(1))) + expect(loader.mock.calls.length).toEqual(0) + + const emitted: Array> = [] + const sub = new Subscription() + sub.add(wrapped.subscribe(noop)) + sub.add(wrapped.subscribe(x => emitted.push(x as any))) + expect(loader.mock.calls.length).toEqual(1) + expect(emitted.length).toEqual(1) + expect(emitted[0].status).toEqual("pending") + await delay(120) + expect(emitted.length).toEqual(2) + expect(emitted[1].status).toEqual("rejected") + expect((emitted[1] as WrappedRejected).error).toEqual(err) + + sub.unsubscribe() + }) + + test("should do nothing if it's already wrapped", () => { + const s = new Subject>() + const wrapped = new OW(s) + const emitted: Array> = [] + const sub = wrapped.subscribe(v => emitted.push(v)) + expect(emitted.length).toBe(1) + expect(emitted[0].status).toBe("pending") + + s.next(WrappedFulfilled.create(1)) + expect(emitted.length).toBe(2) + const em2 = emitted[1] + expect(em2.status).toBe("fulfilled") + expect((em2 as any).value).toBe(1) + + s.next(WrappedRejected.create("reason")) + expect(emitted.length).toBe(3) + const em3 = emitted[2] + expect(em3.status).toBe("rejected") + expect((em3 as any).error).toBe("reason") + + s.next(WrappedRejected.create("reason2")) + expect(emitted.length).toBe(4) + const em4 = emitted[3] + expect(em4.status).toBe("rejected") + expect((em4 as any).error).toBe("reason2") + + sub.unsubscribe() + }) }) function delay(ms: number) { - return new Promise(r => setTimeout(r, ms)) + return new Promise(r => setTimeout(r, ms)) } diff --git a/packages/wrapped/src/ow/index.ts b/packages/wrapped/src/ow/index.ts index 554ccc0..7e19e36 100644 --- a/packages/wrapped/src/ow/index.ts +++ b/packages/wrapped/src/ow/index.ts @@ -1,38 +1,39 @@ import { Observable } from "rxjs" -import { Wrapped, WrappedBase, WrappedFulfilled, WrappedPending, WrappedRejected } from "../domain" +import type { Wrapped } from "../domain" +import { WrappedBase, WrappedFulfilled, WrappedPending, WrappedRejected } from "../domain" export type OWLike = T | Observable | T> export class OW extends Observable> { - constructor(original: OWLike) { - super(s => { - if (original instanceof Observable) { - let got = false - const subscription = original.subscribe( - value => { - got = true - if (value instanceof WrappedBase) { - s.next(value) - } else { - s.next(WrappedFulfilled.create(value)) - } - }, - error => { - got = true - s.next(WrappedRejected.create(error)) - }, - () => { - got = true - s.complete() - } - ) - if (!got) { - s.next(WrappedPending.create()) - } - s.add(subscription) - } else { - s.next(WrappedFulfilled.create(original)) - } - }) - } + constructor(original: OWLike) { + super(s => { + if (original instanceof Observable) { + let got = false + const subscription = original.subscribe( + value => { + got = true + if (value instanceof WrappedBase) { + s.next(value) + } else { + s.next(WrappedFulfilled.create(value)) + } + }, + error => { + got = true + s.next(WrappedRejected.create(error)) + }, + () => { + got = true + s.complete() + }, + ) + if (!got) { + s.next(WrappedPending.create()) + } + s.add(subscription) + } else { + s.next(WrappedFulfilled.create(original)) + } + }) + } } diff --git a/packages/wrapped/src/rx-object/index.test.ts b/packages/wrapped/src/rx-object/index.test.ts index 4350c4b..617fb61 100644 --- a/packages/wrapped/src/rx-object/index.test.ts +++ b/packages/wrapped/src/rx-object/index.test.ts @@ -3,31 +3,31 @@ import { of } from "rxjs" import { rxObject } from "../rx-object" describe("rxObject", () => { - test("should work with plain objects", async () => { - const observable = rxObject({ key: "value" }) - const value = await observable.pipe(first()).toPromise() - expect(value.status).toBe("fulfilled") - expect((value as any).value.key).toBe("value") - }) + test("should work with plain objects", async () => { + const observable = rxObject({ key: "value" }) + const value = await observable.pipe(first()).toPromise() + expect(value.status).toBe("fulfilled") + expect((value as any).value.key).toBe("value") + }) - test("should work with one observable", async () => { - const num = Math.random() - const obs = rxObject({ key: "value", obs: of(num) }) - const value = await obs.pipe(first()).toPromise() - expect(value.status).toBe("fulfilled") - expect((value as any).value.key).toBe("value") - expect((value as any).value.obs).toBe(num) - }) + test("should work with one observable", async () => { + const num = Math.random() + const obs = rxObject({ key: "value", obs: of(num) }) + const value = await obs.pipe(first()).toPromise() + expect(value.status).toBe("fulfilled") + expect((value as any).value.key).toBe("value") + expect((value as any).value.obs).toBe(num) + }) - test("should work with some observables", async () => { - const num = Math.random() - const num2 = Math.random() - const value = await rxObject({ key: "value", obs: of(num), obs2: of(num2) }) - .pipe(first()) - .toPromise() - expect(value.status).toBe("fulfilled") - expect((value as any).value.key).toBe("value") - expect((value as any).value.obs).toBe(num) - expect((value as any).value.obs2).toBe(num2) - }) + test("should work with some observables", async () => { + const num = Math.random() + const num2 = Math.random() + const value = await rxObject({ key: "value", obs: of(num), obs2: of(num2) }) + .pipe(first()) + .toPromise() + expect(value.status).toBe("fulfilled") + expect((value as any).value.key).toBe("value") + expect((value as any).value.obs).toBe(num) + expect((value as any).value.obs2).toBe(num2) + }) }) diff --git a/packages/wrapped/src/rx-object/index.ts b/packages/wrapped/src/rx-object/index.ts index 8325c90..43b0fd0 100644 --- a/packages/wrapped/src/rx-object/index.ts +++ b/packages/wrapped/src/rx-object/index.ts @@ -1,34 +1,34 @@ import { Observable } from "rxjs" import { Lens } from "@rixio/lens" -import { Lifted, Wrapped } from "../domain" +import type { Lifted, Wrapped } from "../domain" import { combineLatest, map } from "../operators" import { OW } from "../ow" type InferObservableInTuple = { - [I in keyof T]: T[I] extends Observable ? T : T[I] + [I in keyof T]: T[I] extends Observable ? T : T[I] } export function rxObject(lifted: [...T]): Observable>> export function rxObject(lifted: Lifted): Observable> export function rxObject(lifted: any): Observable { - const observables: Observable[] = [] - const lenses: Lens, any>[] = [] - walk(lifted, (value, lens) => { - if (value instanceof Observable) { - observables.push(value) - lenses.push(lens) - } - }) - if (observables.length === 0) { - return new OW(lifted) - } - return combineLatest(observables).pipe(map(values => lenses.reduce((acc, l, idx) => l.set(values[idx], acc), lifted))) + const observables: Observable[] = [] + const lenses: Lens, any>[] = [] + walk(lifted, (value, lens) => { + if (value instanceof Observable) { + observables.push(value) + lenses.push(lens) + } + }) + if (observables.length === 0) { + return new OW(lifted) + } + return combineLatest(observables).pipe(map(values => lenses.reduce((acc, l, idx) => l.set(values[idx], acc), lifted))) } function walk(props: T, handler: (value: any, lens: Lens) => void) { - for (const key in props) { - if (props.hasOwnProperty(key)) { - const prop = props[key] as any - handler(prop, Lens.key(key) as any) - } - } + for (const key in props) { + if (props.hasOwnProperty(key)) { + const prop = props[key] as any + handler(prop, Lens.key(key) as any) + } + } } diff --git a/packages/wrapped/src/utils.ts b/packages/wrapped/src/utils.ts index 1dbf6d9..dc8b143 100644 --- a/packages/wrapped/src/utils.ts +++ b/packages/wrapped/src/utils.ts @@ -1,15 +1,16 @@ -import { isWrapped, Wrapped, WrappedFulfilled } from "./domain" +import type { Wrapped } from "./domain" +import { isWrapped, WrappedFulfilled } from "./domain" /** * @deprecated please use it on your own risk */ export function toPlainOrThrow(value: Wrapped): T { - if (value.status === "fulfilled") return value.value - throw new Error("not fulfilled") + if (value.status === "fulfilled") return value.value + throw new Error("not fulfilled") } export function toWrapped(value: T | Wrapped): Wrapped { - if (isWrapped(value)) return value - return WrappedFulfilled.create(value) + if (isWrapped(value)) return value + return WrappedFulfilled.create(value) } diff --git a/yarn.lock b/yarn.lock index ebd62be..1c2b322 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,11 @@ # yarn lockfile v1 +"@aashutoshrathi/word-wrap@^1.2.3": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" + integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== + "@adobe/css-tools@^4.0.1": version "4.2.0" resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.2.0.tgz#e1a84fca468f4b337816fcb7f0964beb620ba855" @@ -22,13 +27,6 @@ dependencies: default-browser-id "3.0.0" -"@babel/code-frame@7.12.11": - version "7.12.11" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f" - integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw== - dependencies: - "@babel/highlight" "^7.10.4" - "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" @@ -102,15 +100,6 @@ json5 "^2.2.1" semver "^6.3.0" -"@babel/generator@^7.11.5": - version "7.11.6" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.11.6.tgz#b868900f81b163b4d464ea24545c61cbac4dc620" - integrity sha512-DWtQ1PV3r+cLbySoHrwn9RWEgKMBLLma4OBQloPRyDYvc5msJM9kvTLo1YnlJd1P/ZuKbdli3ijr5q3FvAF3uA== - dependencies: - "@babel/types" "^7.11.5" - jsesc "^2.5.1" - source-map "^0.5.0" - "@babel/generator@^7.12.11", "@babel/generator@^7.21.4", "@babel/generator@^7.7.2", "@babel/generator@~7.21.1": version "7.21.4" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.21.4.tgz#64a94b7448989f421f919d5239ef553b37bb26bc" @@ -212,15 +201,6 @@ dependencies: "@babel/types" "^7.18.6" -"@babel/helper-function-name@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz#d2d3b20c59ad8c47112fa7d2a94bc09d5ef82f1a" - integrity sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ== - dependencies: - "@babel/helper-get-function-arity" "^7.10.4" - "@babel/template" "^7.10.4" - "@babel/types" "^7.10.4" - "@babel/helper-function-name@^7.18.9", "@babel/helper-function-name@^7.21.0": version "7.21.0" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz#d552829b10ea9f120969304023cd0645fa00b1b4" @@ -237,13 +217,6 @@ "@babel/template" "^7.18.10" "@babel/types" "^7.19.0" -"@babel/helper-get-function-arity@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2" - integrity sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A== - dependencies: - "@babel/types" "^7.10.4" - "@babel/helper-hoist-variables@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678" @@ -358,13 +331,6 @@ dependencies: "@babel/types" "^7.20.0" -"@babel/helper-split-export-declaration@^7.11.0": - version "7.11.0" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz#f8a491244acf6a676158ac42072911ba83ad099f" - integrity sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg== - dependencies: - "@babel/types" "^7.11.0" - "@babel/helper-split-export-declaration@^7.18.6": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz#7367949bc75b20c6d5a5d4a97bba2824ae8ef075" @@ -448,7 +414,7 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.10.4", "@babel/parser@^7.11.5", "@babel/parser@^7.7.0": +"@babel/parser@^7.1.0", "@babel/parser@^7.10.4": version "7.11.5" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.5.tgz#c7ff6303df71080ec7a4f5b8c003c58f1cf51037" integrity sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q== @@ -1197,21 +1163,6 @@ resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== -"@babel/runtime-corejs3@^7.10.2": - version "7.11.2" - resolved "https://registry.yarnpkg.com/@babel/runtime-corejs3/-/runtime-corejs3-7.11.2.tgz#02c3029743150188edeb66541195f54600278419" - integrity sha512-qh5IR+8VgFz83VBa6OkaET6uN/mJOhHONuy3m1sgF0CV6mXdPSEBdA7e1eUbVvyNtANjMbg22JUv71BaDXLY6A== - dependencies: - core-js-pure "^3.0.0" - regenerator-runtime "^0.13.4" - -"@babel/runtime@^7.10.2", "@babel/runtime@^7.9.2": - version "7.11.2" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736" - integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw== - dependencies: - regenerator-runtime "^0.13.4" - "@babel/runtime@^7.12.5", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4": version "7.21.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.0.tgz#5b55c9d394e5fcf304909a8b00c07dc217b56673" @@ -1226,14 +1177,12 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/template@^7.10.4", "@babel/template@^7.3.3": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" - integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA== +"@babel/runtime@^7.9.2": + version "7.11.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736" + integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw== dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/parser" "^7.10.4" - "@babel/types" "^7.10.4" + regenerator-runtime "^0.13.4" "@babel/template@^7.18.10": version "7.18.10" @@ -1253,6 +1202,15 @@ "@babel/parser" "^7.20.7" "@babel/types" "^7.20.7" +"@babel/template@^7.3.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.4.tgz#3251996c4200ebc71d1a8fc405fba940f36ba278" + integrity sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/parser" "^7.10.4" + "@babel/types" "^7.10.4" + "@babel/traverse@^7.1.6", "@babel/traverse@^7.20.5", "@babel/traverse@^7.20.7", "@babel/traverse@^7.21.0", "@babel/traverse@^7.21.2", "@babel/traverse@^7.21.4", "@babel/traverse@^7.7.2", "@babel/traverse@~7.21.2": version "7.21.4" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.21.4.tgz#a836aca7b116634e97a6ed99976236b3282c9d36" @@ -1285,22 +1243,7 @@ debug "^4.1.0" globals "^11.1.0" -"@babel/traverse@^7.7.0": - version "7.11.5" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.11.5.tgz#be777b93b518eb6d76ee2e1ea1d143daa11e61c3" - integrity sha512-EjiPXt+r7LiCZXEfRpSJd+jUMnBd4/9OUv7Nx3+0u9+eimMwJmG0Q98lw4/289JCoxSE8OolDMNZaaF/JZ69WQ== - dependencies: - "@babel/code-frame" "^7.10.4" - "@babel/generator" "^7.11.5" - "@babel/helper-function-name" "^7.10.4" - "@babel/helper-split-export-declaration" "^7.11.0" - "@babel/parser" "^7.11.5" - "@babel/types" "^7.11.5" - debug "^4.1.0" - globals "^11.1.0" - lodash "^4.17.19" - -"@babel/types@^7.0.0", "@babel/types@^7.10.4", "@babel/types@^7.11.0", "@babel/types@^7.11.5", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.7.0": +"@babel/types@^7.0.0", "@babel/types@^7.10.4", "@babel/types@^7.3.0", "@babel/types@^7.3.3": version "7.11.5" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.11.5.tgz#d9de577d01252d77c6800cee039ee64faf75662d" integrity sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q== @@ -1462,21 +1405,38 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.15.tgz#c2e737f3a201ebff8e2ac2b8e9f246b397ad19b8" integrity sha512-DjDa9ywLUUmjhV2Y9wUTIF+1XsmuFGvZoCmOWkli1XcNAh5t25cc7fgsCx4Zi/Uurep3TTLyDiKATgGEg61pkA== -"@eslint/eslintrc@^0.4.3": - version "0.4.3" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-0.4.3.tgz#9e42981ef035beb3dd49add17acb96e8ff6f394c" - integrity sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw== +"@eslint-community/eslint-utils@^4.1.2", "@eslint-community/eslint-utils@^4.2.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" + integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== + dependencies: + eslint-visitor-keys "^3.3.0" + +"@eslint-community/regexpp@^4.4.0", "@eslint-community/regexpp@^4.6.1": + version "4.6.2" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.6.2.tgz#1816b5f6948029c5eaacb0703b850ee0cb37d8f8" + integrity sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw== + +"@eslint/eslintrc@^2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.2.tgz#c6936b4b328c64496692f76944e755738be62396" + integrity sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g== dependencies: ajv "^6.12.4" - debug "^4.1.1" - espree "^7.3.0" - globals "^13.9.0" - ignore "^4.0.6" + debug "^4.3.2" + espree "^9.6.0" + globals "^13.19.0" + ignore "^5.2.0" import-fresh "^3.2.1" - js-yaml "^3.13.1" - minimatch "^3.0.4" + js-yaml "^4.1.0" + minimatch "^3.1.2" strip-json-comments "^3.1.1" +"@eslint/js@^8.47.0": + version "8.47.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.47.0.tgz#5478fdf443ff8158f9de171c704ae45308696c7d" + integrity sha512-P6omY1zv5MItm93kLM8s2vr1HICJH8v0dvddDhysbIuZ+vcjOHg5Zbkf1mTkcmi2JA9oBG2anOkRnW8WJTS8Og== + "@fal-works/esbuild-plugin-global-externals@^2.1.2": version "2.1.2" resolved "https://registry.yarnpkg.com/@fal-works/esbuild-plugin-global-externals/-/esbuild-plugin-global-externals-2.1.2.tgz#c05ed35ad82df8e6ac616c68b92c2282bd083ba4" @@ -1499,16 +1459,21 @@ dependencies: "@hapi/hoek" "^9.0.0" -"@humanwhocodes/config-array@^0.5.0": - version "0.5.0" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.5.0.tgz#1407967d4c6eecd7388f83acf1eaf4d0c6e58ef9" - integrity sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg== +"@humanwhocodes/config-array@^0.11.10": + version "0.11.10" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz#5a3ffe32cc9306365fb3fd572596cd602d5e12d2" + integrity sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ== dependencies: - "@humanwhocodes/object-schema" "^1.2.0" + "@humanwhocodes/object-schema" "^1.2.1" debug "^4.1.1" - minimatch "^3.0.4" + minimatch "^3.0.5" -"@humanwhocodes/object-schema@^1.2.0": +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/object-schema@^1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== @@ -1925,7 +1890,7 @@ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== -"@nodelib/fs.walk@^1.2.3": +"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": version "1.2.8" resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== @@ -2359,22 +2324,22 @@ schema-utils "^3.0.0" source-map "^0.7.3" -"@roborox/eslint-config-default@^2.2.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@roborox/eslint-config-default/-/eslint-config-default-2.2.0.tgz#697be781ce62a9f016952e8ee4f1b72f7ec53afb" - integrity sha512-Pa4DgwLV22ZeINkN4SxrbTI06TSVxkikfJPJfRpk5HnaeHL2OZfqZ12NbCe8fjnxdR9umv8WRxW4w1hujcKN6g== - dependencies: - "@typescript-eslint/eslint-plugin" "3.10.0" - "@typescript-eslint/parser" "3.10.0" - babel-eslint "10.1.0" - eslint-config-react-app "6.0.0-next.77" - eslint-plugin-flowtype "5.2.0" - eslint-plugin-import "2.22.0" - eslint-plugin-jest "23.20.0" - eslint-plugin-jsx-a11y "6.3.1" - eslint-plugin-react "7.20.6" - eslint-plugin-react-hooks "4.1.0" - eslint-plugin-unicorn "21.0.0" +"@rarible/eslint-config-ts@~0.10.0-alpha.10": + version "0.10.0-alpha.10" + resolved "https://registry.yarnpkg.com/@rarible/eslint-config-ts/-/eslint-config-ts-0.10.0-alpha.10.tgz#77621205391b57af1bbd2958b30f9828461ee534" + integrity sha512-Y8y+l8kFAD2m3ppbaeaeiDtsPDo604Wnk7+hK6iMTUwClxR83La3xw1WVOxr1TDIW4XlJnWzIoPlO8ycoTsfwg== + dependencies: + "@typescript-eslint/eslint-plugin" "^5.57.1" + "@typescript-eslint/parser" "^5.57.1" + confusing-browser-globals "^1.0.11" + eslint-plugin-import "^2.27.5" + eslint-plugin-jest "^27.2.1" + eslint-plugin-unicorn "^46.0.0" + +"@rarible/prettier@^0.10.0-alpha.10": + version "0.10.0-alpha.10" + resolved "https://registry.yarnpkg.com/@rarible/prettier/-/prettier-0.10.0-alpha.10.tgz#301683321e3b2c85eaba0cf0be9f5cb391de8a19" + integrity sha512-l06ve7cXejdpEsWTYsgtGW7fzY9dow7YzTQcYZeZcMh3dP5oTKyc8k117mbgC4e4ZdeQyLP/H1HmedfG590z7Q== "@sideway/address@^4.1.3": version "4.1.4" @@ -3088,11 +3053,6 @@ "@types/eslint" "*" "@types/estree" "*" -"@types/eslint-visitor-keys@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" - integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag== - "@types/eslint@*": version "8.37.0" resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.37.0.tgz#29cebc6c2a3ac7fea7113207bf5a828fdf4d7ef1" @@ -3220,11 +3180,6 @@ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== -"@types/json-schema@^7.0.3": - version "7.0.6" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.6.tgz#f4c7ec43e81b319a9815115031709f26987891f0" - integrity sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw== - "@types/json5@^0.0.29": version "0.0.29" resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" @@ -3371,6 +3326,11 @@ resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5" integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ== +"@types/semver@^7.3.12": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.0.tgz#591c1ce3a702c45ee15f47a42ade72c2fd78978a" + integrity sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw== + "@types/semver@^7.3.4": version "7.3.13" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91" @@ -3420,88 +3380,89 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@3.10.0": - version "3.10.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.10.0.tgz#40fd53e81639c0d1a515b44e5fdf4c03dfd3cd39" - integrity sha512-Bbeg9JAnSzZ85Y0gpInZscSpifA6SbEgRryaKdP5ZlUjhTKsvZS4GUIE6xAZCjhNTrf4zXXsySo83ZdHL7it0w== +"@typescript-eslint/eslint-plugin@^5.57.1": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz#aeef0328d172b9e37d9bab6dbc13b87ed88977db" + integrity sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag== dependencies: - "@typescript-eslint/experimental-utils" "3.10.0" - debug "^4.1.1" - functional-red-black-tree "^1.0.1" - regexpp "^3.0.0" - semver "^7.3.2" - tsutils "^3.17.1" - -"@typescript-eslint/experimental-utils@3.10.0": - version "3.10.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.10.0.tgz#f97a669a84a78319ab324cd51169d0c52853a360" - integrity sha512-e5ZLSTuXgqC/Gq3QzK2orjlhTZVXzwxDujQmTBOM1NIVBZgW3wiIZjaXuVutk9R4UltFlwC9UD2+bdxsA7yyNg== - dependencies: - "@types/json-schema" "^7.0.3" - "@typescript-eslint/types" "3.10.0" - "@typescript-eslint/typescript-estree" "3.10.0" - eslint-scope "^5.0.0" - eslint-utils "^2.0.0" - -"@typescript-eslint/experimental-utils@^2.5.0": - version "2.34.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.34.0.tgz#d3524b644cdb40eebceca67f8cf3e4cc9c8f980f" - integrity sha512-eS6FTkq+wuMJ+sgtuNTtcqavWXqsflWcfBnlYhg/nS4aZ1leewkXGbvBhaapn1q6qf4M71bsR1tez5JTRMuqwA== - dependencies: - "@types/json-schema" "^7.0.3" - "@typescript-eslint/typescript-estree" "2.34.0" - eslint-scope "^5.0.0" - eslint-utils "^2.0.0" - -"@typescript-eslint/parser@3.10.0": - version "3.10.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-3.10.0.tgz#820322d990a82265a78f4c1fc9aae03ce95b76ac" - integrity sha512-iJyf3f2HVwscvJR7ySGMXw2DJgIAPKEz8TeU17XVKzgJRV4/VgCeDFcqLzueRe7iFI2gv+Tln4AV88ZOnsCNXg== - dependencies: - "@types/eslint-visitor-keys" "^1.0.0" - "@typescript-eslint/experimental-utils" "3.10.0" - "@typescript-eslint/types" "3.10.0" - "@typescript-eslint/typescript-estree" "3.10.0" - eslint-visitor-keys "^1.1.0" - -"@typescript-eslint/types@3.10.0": - version "3.10.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-3.10.0.tgz#b81906674eca94a884345ba0bc1aaf6cd4da912a" - integrity sha512-ktUWSa75heQNwH85GRM7qP/UUrXqx9d6yIdw0iLO9/uE1LILW+i+3+B64dUodUS2WFWLzKTlwfi9giqrODibWg== - -"@typescript-eslint/typescript-estree@2.34.0": - version "2.34.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz#14aeb6353b39ef0732cc7f1b8285294937cf37d5" - integrity sha512-OMAr+nJWKdlVM9LOqCqh3pQQPwxHAN7Du8DR6dmwCrAmxtiXQnhHJ6tBNtf+cggqfo51SG/FCwnKhXCIM7hnVg== + "@eslint-community/regexpp" "^4.4.0" + "@typescript-eslint/scope-manager" "5.62.0" + "@typescript-eslint/type-utils" "5.62.0" + "@typescript-eslint/utils" "5.62.0" + debug "^4.3.4" + graphemer "^1.4.0" + ignore "^5.2.0" + natural-compare-lite "^1.4.0" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/parser@^5.57.1": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.62.0.tgz#1b63d082d849a2fcae8a569248fbe2ee1b8a56c7" + integrity sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA== dependencies: - debug "^4.1.1" - eslint-visitor-keys "^1.1.0" - glob "^7.1.6" - is-glob "^4.0.1" - lodash "^4.17.15" - semver "^7.3.2" - tsutils "^3.17.1" + "@typescript-eslint/scope-manager" "5.62.0" + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/typescript-estree" "5.62.0" + debug "^4.3.4" -"@typescript-eslint/typescript-estree@3.10.0": - version "3.10.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.10.0.tgz#65df13579a5e53c12afb4f1c5309589e3855a5de" - integrity sha512-yjuY6rmVHRhcUKgXaSPNVloRueGWpFNhxR5EQLzxXfiFSl1U/+FBqHhbaGwtPPEgCSt61QNhZgiFjWT27bgAyw== +"@typescript-eslint/scope-manager@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz#d9457ccc6a0b8d6b37d0eb252a23022478c5460c" + integrity sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w== dependencies: - "@typescript-eslint/types" "3.10.0" - "@typescript-eslint/visitor-keys" "3.10.0" - debug "^4.1.1" - glob "^7.1.6" - is-glob "^4.0.1" - lodash "^4.17.15" - semver "^7.3.2" - tsutils "^3.17.1" + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/visitor-keys" "5.62.0" -"@typescript-eslint/visitor-keys@3.10.0": - version "3.10.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-3.10.0.tgz#6c0cac867e705a42e2c71b359bf6a10a88a28985" - integrity sha512-g4qftk8lWb/rHZe9uEp8oZSvsJhUvR2cfp7F7qE6DyUD2SsovEs8JDQTRP1xHzsD+pERsEpYNqkDgQXW6+ob5A== +"@typescript-eslint/type-utils@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz#286f0389c41681376cdad96b309cedd17d70346a" + integrity sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew== dependencies: - eslint-visitor-keys "^1.1.0" + "@typescript-eslint/typescript-estree" "5.62.0" + "@typescript-eslint/utils" "5.62.0" + debug "^4.3.4" + tsutils "^3.21.0" + +"@typescript-eslint/types@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.62.0.tgz#258607e60effa309f067608931c3df6fed41fd2f" + integrity sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ== + +"@typescript-eslint/typescript-estree@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz#7d17794b77fabcac615d6a48fb143330d962eb9b" + integrity sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA== + dependencies: + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/visitor-keys" "5.62.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + semver "^7.3.7" + tsutils "^3.21.0" + +"@typescript-eslint/utils@5.62.0", "@typescript-eslint/utils@^5.10.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.62.0.tgz#141e809c71636e4a75daa39faed2fb5f4b10df86" + integrity sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@types/json-schema" "^7.0.9" + "@types/semver" "^7.3.12" + "@typescript-eslint/scope-manager" "5.62.0" + "@typescript-eslint/types" "5.62.0" + "@typescript-eslint/typescript-estree" "5.62.0" + eslint-scope "^5.1.1" + semver "^7.3.7" + +"@typescript-eslint/visitor-keys@5.62.0": + version "5.62.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz#2174011917ce582875954ffe2f6912d5931e353e" + integrity sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw== + dependencies: + "@typescript-eslint/types" "5.62.0" + eslint-visitor-keys "^3.3.0" "@webassemblyjs/ast@1.11.1": version "1.11.1" @@ -3712,12 +3673,7 @@ acorn-import-assertions@^1.7.6: resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz#ba2b5939ce62c238db6d93d81c9b111b29b855e9" integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw== -acorn-jsx@^5.2.0: - version "5.3.1" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.1.tgz#fc8661e11b7ac1539c47dbfea2e72b3af34d267b" - integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== - -acorn-jsx@^5.3.1: +acorn-jsx@^5.3.1, acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== @@ -3732,7 +3688,7 @@ acorn-walk@^8.0.2: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== -acorn@^7.4.0, acorn@^7.4.1: +acorn@^7.4.1: version "7.4.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== @@ -3742,6 +3698,11 @@ acorn@^8.1.0, acorn@^8.5.0, acorn@^8.7.1, acorn@^8.8.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== +acorn@^8.4.1, acorn@^8.9.0: + version "8.10.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.10.0.tgz#8be5b3907a67221a81ab23c7889c4c5526b62ec5" + integrity sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw== + add-stream@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/add-stream/-/add-stream-1.0.0.tgz#6a7990437ca736d5e1288db92bd3266d5f5cb2aa" @@ -3800,7 +3761,7 @@ ajv-keywords@^5.0.0: dependencies: fast-deep-equal "^3.1.3" -ajv@^6.10.0, ajv@^6.12.4: +ajv@^6.12.4: version "6.12.5" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da" integrity sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag== @@ -3820,7 +3781,7 @@ ajv@^6.12.5: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ajv@^8.0.0, ajv@^8.0.1, ajv@^8.8.0: +ajv@^8.0.0, ajv@^8.8.0: version "8.12.0" resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== @@ -3945,14 +3906,6 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -aria-query@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b" - integrity sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA== - dependencies: - "@babel/runtime" "^7.10.2" - "@babel/runtime-corejs3" "^7.10.2" - aria-query@^5.0.0: version "5.1.3" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.1.3.tgz#19db27cd101152773631396f7a95a3b58c22c35e" @@ -3960,6 +3913,14 @@ aria-query@^5.0.0: dependencies: deep-equal "^2.0.5" +array-buffer-byte-length@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz#fabe8bc193fea865f317fe7807085ee0dee5aead" + integrity sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A== + dependencies: + call-bind "^1.0.2" + is-array-buffer "^3.0.1" + array-differ@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/array-differ/-/array-differ-3.0.0.tgz#3cbb3d0f316810eafcc47624734237d6aee4ae6b" @@ -3975,36 +3936,64 @@ array-ify@^1.0.0: resolved "https://registry.yarnpkg.com/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece" integrity sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4= -array-includes@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.1.tgz#cdd67e6852bdf9c1215460786732255ed2459348" - integrity sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ== +array-includes@^3.1.6: + version "3.1.6" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.6.tgz#9e9e720e194f198266ba9e18c29e6a9b0e4b225f" + integrity sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw== dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.0" - is-string "^1.0.5" + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + get-intrinsic "^1.1.3" + is-string "^1.0.7" array-union@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== -array.prototype.flat@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz#0de82b426b0318dbfdb940089e38b043d37f6c7b" - integrity sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ== +array.prototype.findlastindex@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.2.tgz#bc229aef98f6bd0533a2bc61ff95209875526c9b" + integrity sha512-tb5thFFlUcp7NdNF6/MpDk/1r/4awWG1FIz3YqDf+/zJSTezBb+/5WViH41obXULHVpDzoiCLpJ/ZO9YbJMsdw== dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + es-shim-unscopables "^1.0.0" + get-intrinsic "^1.1.3" -array.prototype.flatmap@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.2.3.tgz#1c13f84a178566042dd63de4414440db9222e443" - integrity sha512-OOEk+lkePcg+ODXIpvuU9PAryCikCJyo7GlDG1upleEpQRx6mzL9puEBkozQ5iAx20KV0l3DbyQwqciJtqe5Pg== +array.prototype.flat@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.1.tgz#ffc6576a7ca3efc2f46a143b9d1dda9b4b3cf5e2" + integrity sha512-roTU0KWIOmJ4DRLmwKd19Otg0/mT3qPNt0Qb3GWW8iObuZXxrjB/pzn0R3hqpRSWg4HCwqx+0vwOnWnvlOyeIA== dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - function-bind "^1.1.1" + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + es-shim-unscopables "^1.0.0" + +array.prototype.flatmap@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz#1aae7903c2100433cb8261cd4ed310aab5c4a183" + integrity sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" + es-shim-unscopables "^1.0.0" + +arraybuffer.prototype.slice@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.1.tgz#9b5ea3868a6eebc30273da577eb888381c0044bb" + integrity sha512-09x0ZWFEjj4WD8PDbykUwo3t9arLn8NIzmmYEJFpYekOAQjpkGSyrQhNoRTcwwcFRu+ycWF78QZ63oWTqSjBcw== + dependencies: + array-buffer-byte-length "^1.0.0" + call-bind "^1.0.2" + define-properties "^1.2.0" + get-intrinsic "^1.2.1" + is-array-buffer "^3.0.2" + is-shared-array-buffer "^1.0.2" arrify@^1.0.1: version "1.0.1" @@ -4026,11 +4015,6 @@ assert@^2.0.0: object-is "^1.0.1" util "^0.12.0" -ast-types-flow@^0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad" - integrity sha1-9wtzXGvKGlycItmCw+Oef+ujva0= - ast-types@0.15.2: version "0.15.2" resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.15.2.tgz#39ae4809393c4b16df751ee563411423e85fb49d" @@ -4082,11 +4066,6 @@ available-typed-arrays@^1.0.5: resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== -axe-core@^3.5.4: - version "3.5.5" - resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-3.5.5.tgz#84315073b53fa3c0c51676c588d59da09a192227" - integrity sha512-5P0QZ6J5xGikH780pghEdbEKijCTrruK9KxtPZCFWUpef0f6GipO+xEZ5GKCb020mmqgbiNO6TcA55CriL784Q== - axios@^1.0.0: version "1.3.5" resolved "https://registry.yarnpkg.com/axios/-/axios-1.3.5.tgz#e07209b39a0d11848e3e341fa087acd71dadc542" @@ -4096,28 +4075,11 @@ axios@^1.0.0: form-data "^4.0.0" proxy-from-env "^1.1.0" -axobject-query@^2.1.2: - version "2.2.0" - resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" - integrity sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA== - babel-core@^7.0.0-bridge.0: version "7.0.0-bridge.0" resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-7.0.0-bridge.0.tgz#95a492ddd90f9b4e9a4a1da14eb335b87b634ece" integrity sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg== -babel-eslint@10.1.0, babel-eslint@^10.1.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.1.0.tgz#6968e568a910b78fb3779cdd8b6ac2f479943232" - integrity sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg== - dependencies: - "@babel/code-frame" "^7.0.0" - "@babel/parser" "^7.7.0" - "@babel/traverse" "^7.7.0" - "@babel/types" "^7.7.0" - eslint-visitor-keys "^1.0.0" - resolve "^1.12.0" - babel-jest@^29.5.0: version "29.5.0" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.5.0.tgz#3fe3ddb109198e78b1c88f9ebdecd5e4fc2f50a5" @@ -4423,6 +4385,11 @@ buffer@^6.0.3: base64-js "^1.3.1" ieee754 "^1.2.1" +builtin-modules@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" + integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== + builtins@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/builtins/-/builtins-1.0.3.tgz#cb94faeb61c8696451db36534e1422f94f0aee88" @@ -4652,7 +4619,7 @@ ci-info@^2.0.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== -ci-info@^3.2.0: +ci-info@^3.2.0, ci-info@^3.6.1: version "3.8.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.8.0.tgz#81408265a5380c929f0bc665d62256628ce9ef91" integrity sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw== @@ -4924,21 +4891,16 @@ config-chain@1.1.12: ini "^1.3.4" proto-list "~1.2.1" -confusing-browser-globals@2.0.0-next.260+3d74b79d: - version "2.0.0-next.260" - resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-2.0.0-next.260.tgz#8e6671dcd84c0baff030acd8abdeb9600c225569" - integrity sha512-32gzYnwYaiszOPZK6Ior0Y+Y9+mXGQsvtq6ecg4zfkhbPVvY4DmQEaAGgmQjz4K3OulM5EhVM0i62edbrga4BQ== +confusing-browser-globals@^1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz#ae40e9b57cdd3915408a2805ebd3a5585608dc81" + integrity sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA== console-control-strings@^1.0.0, console-control-strings@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= -contains-path@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" - integrity sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo= - content-disposition@0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" @@ -5062,11 +5024,6 @@ core-js-compat@^3.25.1: dependencies: browserslist "^4.21.5" -core-js-pure@^3.0.0: - version "3.6.5" - resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813" - integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA== - core-js-pure@^3.23.3: version "3.30.0" resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.30.0.tgz#41b6c42e5f363bd53d79999bd35093b17e42e1bf" @@ -5175,11 +5132,6 @@ csstype@^3.0.2: resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.3.tgz#2b410bbeba38ba9633353aff34b05d9755d065f8" integrity sha512-jPl+wbWPOWJ7SXsWyqGRk3lGecbar0Cb0OvZF/r/ZU011R4YqiRehgkQ9p4eQfo9DSDLqLL3wHwfxeJiuIsNag== -damerau-levenshtein@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz#143c1641cb3d85c60c32329e26899adea8701791" - integrity sha512-JVrozIeElnj3QzfUIt8tB8YMluBJom4Vw9qTPpjGYQ9fYlB3D/rb6OordUxf3xeFB35LKWs0xqcO5U6ySvBtug== - dargs@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/dargs/-/dargs-7.0.0.tgz#04015c41de0bcb69ec84050f3d9be0caf8d6d5cc" @@ -5206,14 +5158,21 @@ debug@2.6.9, debug@^2.6.9: dependencies: ms "2.0.0" -debug@4, debug@^4.2.0, debug@^4.3.3, debug@^4.3.4: +debug@4, debug@^4.2.0, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: ms "2.1.2" -debug@^4.0.1, debug@^4.1.0, debug@^4.1.1: +debug@^3.2.7: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + +debug@^4.1.0, debug@^4.1.1: version "4.2.0" resolved "https://registry.yarnpkg.com/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1" integrity sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg== @@ -5303,7 +5262,7 @@ define-properties@^1.1.3: dependencies: object-keys "^1.0.12" -define-properties@^1.1.4: +define-properties@^1.1.4, define-properties@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.0.tgz#52988570670c9eacedd8064f4a990f2405849bd5" integrity sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA== @@ -5407,14 +5366,6 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" -doctrine@1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" - integrity sha1-N53Ocw9hZvds76TmcHoVmwLFpvo= - dependencies: - esutils "^2.0.2" - isarray "^1.0.0" - doctrine@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" @@ -5570,11 +5521,6 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== -emoji-regex@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.0.0.tgz#48a2309cc8a1d2e9d23bc6a67c39b63032e76ea4" - integrity sha512-6p1NII1Vm62wni/VR/cUMauVQoxmLVb9csqQlvLz+hO2gk8U2UYDfXHQSUYIBKmZwAKz867IDqG7B+u0mj+M6w== - emojis-list@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" @@ -5616,7 +5562,15 @@ enhanced-resolve@^5.0.0, enhanced-resolve@^5.10.0: graceful-fs "^4.2.4" tapable "^2.2.0" -enquirer@^2.3.5, enquirer@^2.3.6, enquirer@~2.3.6: +enhanced-resolve@^5.9.3: + version "5.15.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz#1af946c7d93603eb88e9896cee4904dc012e9c35" + integrity sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +enquirer@^2.3.6, enquirer@~2.3.6: version "2.3.6" resolved "https://registry.yarnpkg.com/enquirer/-/enquirer-2.3.6.tgz#2a7fe5dd634a1e4125a975ec994ff5456dc3734d" integrity sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg== @@ -5648,7 +5602,7 @@ err-code@^2.0.2: resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== -error-ex@^1.2.0, error-ex@^1.3.1: +error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== @@ -5662,40 +5616,50 @@ error-stack-parser@^2.0.6: dependencies: stackframe "^1.3.4" -es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.5: - version "1.17.7" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.7.tgz#a4de61b2f66989fc7421676c1cb9787573ace54c" - integrity sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g== - dependencies: - es-to-primitive "^1.2.1" - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.1" - is-callable "^1.2.2" - is-regex "^1.1.1" - object-inspect "^1.8.0" - object-keys "^1.1.1" - object.assign "^4.1.1" - string.prototype.trimend "^1.0.1" - string.prototype.trimstart "^1.0.1" - -es-abstract@^1.18.0-next.0: - version "1.18.0-next.1" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.18.0-next.1.tgz#6e3a0a4bda717e5023ab3b8e90bec36108d22c68" - integrity sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA== +es-abstract@^1.19.0, es-abstract@^1.20.4, es-abstract@^1.21.2: + version "1.22.1" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.22.1.tgz#8b4e5fc5cefd7f1660f0f8e1a52900dfbc9d9ccc" + integrity sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw== dependencies: + array-buffer-byte-length "^1.0.0" + arraybuffer.prototype.slice "^1.0.1" + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + es-set-tostringtag "^2.0.1" es-to-primitive "^1.2.1" - function-bind "^1.1.1" + function.prototype.name "^1.1.5" + get-intrinsic "^1.2.1" + get-symbol-description "^1.0.0" + globalthis "^1.0.3" + gopd "^1.0.1" has "^1.0.3" - has-symbols "^1.0.1" - is-callable "^1.2.2" - is-negative-zero "^2.0.0" - is-regex "^1.1.1" - object-inspect "^1.8.0" + has-property-descriptors "^1.0.0" + has-proto "^1.0.1" + has-symbols "^1.0.3" + internal-slot "^1.0.5" + is-array-buffer "^3.0.2" + is-callable "^1.2.7" + is-negative-zero "^2.0.2" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + is-string "^1.0.7" + is-typed-array "^1.1.10" + is-weakref "^1.0.2" + object-inspect "^1.12.3" object-keys "^1.1.1" - object.assign "^4.1.1" - string.prototype.trimend "^1.0.1" - string.prototype.trimstart "^1.0.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.5.0" + safe-array-concat "^1.0.0" + safe-regex-test "^1.0.0" + string.prototype.trim "^1.2.7" + string.prototype.trimend "^1.0.6" + string.prototype.trimstart "^1.0.6" + typed-array-buffer "^1.0.0" + typed-array-byte-length "^1.0.0" + typed-array-byte-offset "^1.0.0" + typed-array-length "^1.0.4" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.10" es-get-iterator@^1.1.2: version "1.1.3" @@ -5717,6 +5681,22 @@ es-module-lexer@^0.9.0: resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19" integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== +es-set-tostringtag@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz#338d502f6f674301d710b80c8592de8a15f09cd8" + integrity sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg== + dependencies: + get-intrinsic "^1.1.3" + has "^1.0.3" + has-tostringtag "^1.0.0" + +es-shim-unscopables@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241" + integrity sha512-Jm6GPcCdC30eMLbZ2x8z2WuRwAws3zTBBKuusffYVUrNj/GVSUAZ+xKMaUpfNDR5IbyNA5LJbaecoUVbmUcB1w== + dependencies: + has "^1.0.3" + es-to-primitive@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" @@ -5808,135 +5788,88 @@ escodegen@^2.0.0: optionalDependencies: source-map "~0.6.1" -eslint-ast-utils@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/eslint-ast-utils/-/eslint-ast-utils-1.1.0.tgz#3d58ba557801cfb1c941d68131ee9f8c34bd1586" - integrity sha512-otzzTim2/1+lVrlH19EfQQJEhVJSu0zOb9ygb3iapN6UlyaDtyRq4b5U1FuW0v1lRa9Fp/GJyHkSwm6NqABgCA== - dependencies: - lodash.get "^4.4.2" - lodash.zip "^4.2.0" - -eslint-config-prettier@^7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-7.2.0.tgz#f4a4bd2832e810e8cc7c1411ec85b3e85c0c53f9" - integrity sha512-rV4Qu0C3nfJKPOAhFujFxB7RMP+URFyQqqOZW9DMRD7ZDTFyjaIlETU3xzHELt++4ugC0+Jm084HQYkkJe+Ivg== - -eslint-config-react-app@6.0.0-next.77: - version "6.0.0-next.77" - resolved "https://registry.yarnpkg.com/eslint-config-react-app/-/eslint-config-react-app-6.0.0-next.77.tgz#fd4f396526698058be83681aa586c0d5afa8ec6a" - integrity sha512-wSYHAHIxBy8ETFGUz5o58Va+HPlV8W2sZc3zf0alZWOlUqmk8AO84FT5L5pnlCsJMZLWHvs0oSwU2CqgKxM92g== - dependencies: - confusing-browser-globals "2.0.0-next.260+3d74b79d" +eslint-config-prettier@^8.8.0: + version "8.10.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.10.0.tgz#3a06a662130807e2502fc3ff8b4143d8a0658e11" + integrity sha512-SM8AMJdeQqRYT9O9zguiruQZaN7+z+E4eAP9oiLNGKMtomwaB1E9dcgUD6ZAn/eQAb52USbvezbiljfZUhbJcg== -eslint-import-resolver-node@^0.3.3: - version "0.3.4" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717" - integrity sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA== +eslint-import-resolver-node@^0.3.7: + version "0.3.9" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac" + integrity sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g== dependencies: - debug "^2.6.9" - resolve "^1.13.1" + debug "^3.2.7" + is-core-module "^2.13.0" + resolve "^1.22.4" -eslint-module-utils@^2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz#579ebd094f56af7797d19c9866c9c9486629bfa6" - integrity sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA== - dependencies: - debug "^2.6.9" - pkg-dir "^2.0.0" - -eslint-plugin-flowtype@5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-flowtype/-/eslint-plugin-flowtype-5.2.0.tgz#a4bef5dc18f9b2bdb41569a4ab05d73805a3d261" - integrity sha512-z7ULdTxuhlRJcEe1MVljePXricuPOrsWfScRXFhNzVD5dmTHWjIF57AxD0e7AbEoLSbjSsaA5S+hCg43WvpXJQ== - dependencies: - lodash "^4.17.15" - string-natural-compare "^3.0.1" - -eslint-plugin-import@2.22.0: - version "2.22.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.22.0.tgz#92f7736fe1fde3e2de77623c838dd992ff5ffb7e" - integrity sha512-66Fpf1Ln6aIS5Gr/55ts19eUuoDhAbZgnr6UxK5hbDx6l/QgQgx61AePq+BV4PP2uXQFClgMVzep5zZ94qqsxg== - dependencies: - array-includes "^3.1.1" - array.prototype.flat "^1.2.3" - contains-path "^0.1.0" - debug "^2.6.9" - doctrine "1.5.0" - eslint-import-resolver-node "^0.3.3" - eslint-module-utils "^2.6.0" - has "^1.0.3" - minimatch "^3.0.4" - object.values "^1.1.1" - read-pkg-up "^2.0.0" - resolve "^1.17.0" - tsconfig-paths "^3.9.0" - -eslint-plugin-jest@23.20.0: - version "23.20.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-23.20.0.tgz#e1d69c75f639e99d836642453c4e75ed22da4099" - integrity sha512-+6BGQt85OREevBDWCvhqj1yYA4+BFK4XnRZSGJionuEYmcglMZYLNNBBemwzbqUAckURaHdJSBcjHPyrtypZOw== +eslint-module-utils@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz#e439fee65fc33f6bba630ff621efc38ec0375c49" + integrity sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw== dependencies: - "@typescript-eslint/experimental-utils" "^2.5.0" + debug "^3.2.7" -eslint-plugin-jsx-a11y@6.3.1: - version "6.3.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.3.1.tgz#99ef7e97f567cc6a5b8dd5ab95a94a67058a2660" - integrity sha512-i1S+P+c3HOlBJzMFORRbC58tHa65Kbo8b52/TwCwSKLohwvpfT5rm2GjGWzOHTEuq4xxf2aRlHHTtmExDQOP+g== - dependencies: - "@babel/runtime" "^7.10.2" - aria-query "^4.2.2" - array-includes "^3.1.1" - ast-types-flow "^0.0.7" - axe-core "^3.5.4" - axobject-query "^2.1.2" - damerau-levenshtein "^1.0.6" - emoji-regex "^9.0.0" - has "^1.0.3" - jsx-ast-utils "^2.4.1" - language-tags "^1.0.5" - -eslint-plugin-react-hooks@4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.1.0.tgz#6323fbd5e650e84b2987ba76370523a60f4e7925" - integrity sha512-36zilUcDwDReiORXmcmTc6rRumu9JIM3WjSvV0nclHoUQ0CNrX866EwONvLR/UqaeqFutbAnVu8PEmctdo2SRQ== - -eslint-plugin-react@7.20.6: - version "7.20.6" - resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.20.6.tgz#4d7845311a93c463493ccfa0a19c9c5d0fd69f60" - integrity sha512-kidMTE5HAEBSLu23CUDvj8dc3LdBU0ri1scwHBZjI41oDv4tjsWZKU7MQccFzH1QYPYhsnTF2ovh7JlcIcmxgg== +eslint-plugin-import@^2.27.5: + version "2.28.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.28.0.tgz#8d66d6925117b06c4018d491ae84469eb3cb1005" + integrity sha512-B8s/n+ZluN7sxj9eUf7/pRFERX0r5bnFA2dCaLHy2ZeaQEAz0k+ZZkFWRFHJAqxfxQDx6KLv9LeIki7cFdwW+Q== dependencies: - array-includes "^3.1.1" - array.prototype.flatmap "^1.2.3" + array-includes "^3.1.6" + array.prototype.findlastindex "^1.2.2" + array.prototype.flat "^1.3.1" + array.prototype.flatmap "^1.3.1" + debug "^3.2.7" doctrine "^2.1.0" + eslint-import-resolver-node "^0.3.7" + eslint-module-utils "^2.8.0" has "^1.0.3" - jsx-ast-utils "^2.4.1" - object.entries "^1.1.2" - object.fromentries "^2.0.2" - object.values "^1.1.1" - prop-types "^15.7.2" - resolve "^1.17.0" - string.prototype.matchall "^4.0.2" + is-core-module "^2.12.1" + is-glob "^4.0.3" + minimatch "^3.1.2" + object.fromentries "^2.0.6" + object.groupby "^1.0.0" + object.values "^1.1.6" + resolve "^1.22.3" + semver "^6.3.1" + tsconfig-paths "^3.14.2" + +eslint-plugin-jest@^27.2.1: + version "27.2.3" + resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-27.2.3.tgz#6f8a4bb2ca82c0c5d481d1b3be256ab001f5a3ec" + integrity sha512-sRLlSCpICzWuje66Gl9zvdF6mwD5X86I4u55hJyFBsxYOsBCmT5+kSUjf+fkFWVMMgpzNEupjW8WzUqi83hJAQ== + dependencies: + "@typescript-eslint/utils" "^5.10.0" + +eslint-plugin-prettier@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz#651cbb88b1dab98bfd42f017a12fa6b2d993f94b" + integrity sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ== + dependencies: + prettier-linter-helpers "^1.0.0" -eslint-plugin-unicorn@21.0.0: - version "21.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-unicorn/-/eslint-plugin-unicorn-21.0.0.tgz#7e3a8b0f725f003619e1f40d769939ecd8d708d0" - integrity sha512-S8v7+v4gZTQPj4pKKvexhgSUaLQSyItvxW2SVZDaX9Iu5IjlAmF2eni+L6w8a2aqshxgU8Lle4FIAVDtuejSKQ== +eslint-plugin-unicorn@^46.0.0: + version "46.0.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-unicorn/-/eslint-plugin-unicorn-46.0.1.tgz#222ff65b30b2d9ed6f90de908ceb6a05dd0514d9" + integrity sha512-setGhMTiLAddg1asdwjZ3hekIN5zLznNa5zll7pBPwFOka6greCKDQydfqy4fqyUhndi74wpDzClSQMEcmOaew== dependencies: - ci-info "^2.0.0" + "@babel/helper-validator-identifier" "^7.19.1" + "@eslint-community/eslint-utils" "^4.1.2" + ci-info "^3.6.1" clean-regexp "^1.0.0" - eslint-ast-utils "^1.1.0" - eslint-template-visitor "^2.0.0" - eslint-utils "^2.1.0" - import-modules "^2.0.0" - lodash "^4.17.15" + esquery "^1.4.0" + indent-string "^4.0.0" + is-builtin-module "^3.2.0" + jsesc "^3.0.2" + lodash "^4.17.21" pluralize "^8.0.0" read-pkg-up "^7.0.1" - regexp-tree "^0.1.21" - reserved-words "^0.1.2" + regexp-tree "^0.1.24" + regjsparser "^0.9.1" safe-regex "^2.1.1" - semver "^7.3.2" + semver "^7.3.8" + strip-indent "^3.0.0" -eslint-scope@5.1.1, eslint-scope@^5.0.0, eslint-scope@^5.1.1: +eslint-scope@5.1.1, eslint-scope@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== @@ -5944,110 +5877,77 @@ eslint-scope@5.1.1, eslint-scope@^5.0.0, eslint-scope@^5.1.1: esrecurse "^4.3.0" estraverse "^4.1.1" -eslint-template-visitor@^2.0.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/eslint-template-visitor/-/eslint-template-visitor-2.2.1.tgz#2dccb1ab28fa7429e56ba6dd0144def2d89bc2d6" - integrity sha512-q3SxoBXz0XjPGkUpwGVAwIwIPIxzCAJX1uwfVc8tW3v7u/zS7WXNH3I2Mu2MDz2NgSITAyKLRaQFPHu/iyKxDQ== +eslint-scope@^7.2.2: + version "7.2.2" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" + integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== dependencies: - babel-eslint "^10.1.0" - eslint-visitor-keys "^1.3.0" - esquery "^1.3.1" - multimap "^1.1.0" - -eslint-utils@^2.0.0, eslint-utils@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" - integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== - dependencies: - eslint-visitor-keys "^1.1.0" - -eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0, eslint-visitor-keys@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" - integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== + esrecurse "^4.3.0" + estraverse "^5.2.0" -eslint-visitor-keys@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" - integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== - -eslint@^7.10.0: - version "7.32.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.32.0.tgz#c6d328a14be3fb08c8d1d21e12c02fdb7a2a812d" - integrity sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA== - dependencies: - "@babel/code-frame" "7.12.11" - "@eslint/eslintrc" "^0.4.3" - "@humanwhocodes/config-array" "^0.5.0" - ajv "^6.10.0" +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +eslint@^8.47.0: + version "8.47.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.47.0.tgz#c95f9b935463fb4fad7005e626c7621052e90806" + integrity sha512-spUQWrdPt+pRVP1TTJLmfRNJJHHZryFmptzcafwSvHsceV81djHOdnEeDmkdotZyLNjDhrOasNK8nikkoG1O8Q== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.6.1" + "@eslint/eslintrc" "^2.1.2" + "@eslint/js" "^8.47.0" + "@humanwhocodes/config-array" "^0.11.10" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + ajv "^6.12.4" chalk "^4.0.0" cross-spawn "^7.0.2" - debug "^4.0.1" + debug "^4.3.2" doctrine "^3.0.0" - enquirer "^2.3.5" escape-string-regexp "^4.0.0" - eslint-scope "^5.1.1" - eslint-utils "^2.1.0" - eslint-visitor-keys "^2.0.0" - espree "^7.3.1" - esquery "^1.4.0" + eslint-scope "^7.2.2" + eslint-visitor-keys "^3.4.3" + espree "^9.6.1" + esquery "^1.4.2" esutils "^2.0.2" fast-deep-equal "^3.1.3" file-entry-cache "^6.0.1" - functional-red-black-tree "^1.0.1" - glob-parent "^5.1.2" - globals "^13.6.0" - ignore "^4.0.6" - import-fresh "^3.0.0" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.19.0" + graphemer "^1.4.0" + ignore "^5.2.0" imurmurhash "^0.1.4" is-glob "^4.0.0" - js-yaml "^3.13.1" + is-path-inside "^3.0.3" + js-yaml "^4.1.0" json-stable-stringify-without-jsonify "^1.0.1" levn "^0.4.1" lodash.merge "^4.6.2" - minimatch "^3.0.4" + minimatch "^3.1.2" natural-compare "^1.4.0" - optionator "^0.9.1" - progress "^2.0.0" - regexpp "^3.1.0" - semver "^7.2.1" - strip-ansi "^6.0.0" - strip-json-comments "^3.1.0" - table "^6.0.9" + optionator "^0.9.3" + strip-ansi "^6.0.1" text-table "^0.2.0" - v8-compile-cache "^2.0.3" - -espree@^7.3.0: - version "7.3.0" - resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.0.tgz#dc30437cf67947cf576121ebd780f15eeac72348" - integrity sha512-dksIWsvKCixn1yrEXO8UosNSxaDoSYpq9reEjZSbHLpT5hpaCAKTLBwq0RHtLrIr+c0ByiYzWT8KTMRzoRCNlw== - dependencies: - acorn "^7.4.0" - acorn-jsx "^5.2.0" - eslint-visitor-keys "^1.3.0" -espree@^7.3.1: - version "7.3.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6" - integrity sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g== +espree@^9.6.0, espree@^9.6.1: + version "9.6.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" + integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== dependencies: - acorn "^7.4.0" - acorn-jsx "^5.3.1" - eslint-visitor-keys "^1.3.0" + acorn "^8.9.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.4.1" esprima@^4.0.0, esprima@^4.0.1, esprima@~4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -esquery@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.3.1.tgz#b78b5828aa8e214e29fb74c4d5b752e1c033da57" - integrity sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ== - dependencies: - estraverse "^5.1.0" - -esquery@^1.4.0: +esquery@^1.4.0, esquery@^1.4.2: version "1.5.0" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== @@ -6227,6 +6127,11 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== +fast-diff@^1.1.2: + version "1.3.0" + resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0" + integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== + fast-glob@3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1" @@ -6381,7 +6286,7 @@ find-up@5.0.0, find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" -find-up@^2.0.0, find-up@^2.1.0: +find-up@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= @@ -6559,12 +6464,17 @@ function-bind@^1.1.1: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== -functional-red-black-tree@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" - integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= +function.prototype.name@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.5.tgz#cce0505fe1ffb80503e6f9e46cc64e46a12a9621" + integrity sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.0" + functions-have-names "^1.2.2" -functions-have-names@^1.2.2: +functions-have-names@^1.2.2, functions-have-names@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== @@ -6649,6 +6559,16 @@ get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@^1.2.0: has "^1.0.3" has-symbols "^1.0.3" +get-intrinsic@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" + integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-proto "^1.0.1" + has-symbols "^1.0.3" + get-npm-tarball-url@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/get-npm-tarball-url/-/get-npm-tarball-url-2.0.3.tgz#67dff908d699e9e2182530ae6e939a93e5f8dfdb" @@ -6696,6 +6616,14 @@ get-stream@^6.0.0: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== +get-symbol-description@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" + integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + giget@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/giget/-/giget-1.1.2.tgz#f99a49cb0ff85479c8c3612cdc7ca27f2066e818" @@ -6770,6 +6698,13 @@ glob-parent@5.1.2, glob-parent@^5.1.2, glob-parent@~5.1.2: dependencies: is-glob "^4.0.1" +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + glob-promise@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/glob-promise/-/glob-promise-6.0.2.tgz#7c7f2a223e3aaa8f7bd7ff5f24d0ab2352724b31" @@ -6806,7 +6741,7 @@ glob@^7.0.0: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: +glob@^7.1.3, glob@^7.1.4: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== @@ -6844,13 +6779,20 @@ globals@^11.1.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -globals@^13.6.0, globals@^13.9.0: - version "13.20.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.20.0.tgz#ea276a1e508ffd4f1612888f9d1bad1e2717bf82" - integrity sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ== +globals@^13.19.0: + version "13.21.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.21.0.tgz#163aae12f34ef502f5153cfbdd3600f36c63c571" + integrity sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg== dependencies: type-fest "^0.20.2" +globalthis@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf" + integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA== + dependencies: + define-properties "^1.1.3" + globby@10.0.0: version "10.0.0" resolved "https://registry.yarnpkg.com/globby/-/globby-10.0.0.tgz#abfcd0630037ae174a88590132c2f6804e291072" @@ -6865,7 +6807,7 @@ globby@10.0.0: merge2 "^1.2.3" slash "^3.0.0" -globby@11.1.0, globby@^11.0.1, globby@^11.0.2: +globby@11.1.0, globby@^11.0.1, globby@^11.0.2, globby@^11.1.0: version "11.1.0" resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== @@ -6899,6 +6841,11 @@ graceful-fs@^4.2.6: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + gunzip-maybe@^1.4.2: version "1.4.2" resolved "https://registry.yarnpkg.com/gunzip-maybe/-/gunzip-maybe-1.4.2.tgz#b913564ae3be0eda6f3de36464837a9cd94b98ac" @@ -6928,7 +6875,7 @@ hard-rejection@^2.1.0: resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA== -has-bigints@^1.0.1: +has-bigints@^1.0.1, has-bigints@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== @@ -6950,6 +6897,11 @@ has-property-descriptors@^1.0.0: dependencies: get-intrinsic "^1.1.1" +has-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.1.tgz#1885c1305538958aff469fef37937c22795408e0" + integrity sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg== + has-symbols@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" @@ -7181,11 +7133,6 @@ ignore-walk@^6.0.0: dependencies: minimatch "^7.4.2" -ignore@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" - integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== - ignore@^5.0.4: version "5.2.4" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" @@ -7201,7 +7148,7 @@ immutable@^4.0.0: resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.3.0.tgz#eb1738f14ffb39fd068b1dbe1296117484dd34be" integrity sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg== -import-fresh@^3.0.0, import-fresh@^3.2.1: +import-fresh@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.2.1.tgz#633ff618506e793af5ac91bf48b72677e15cbe66" integrity sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ== @@ -7217,11 +7164,6 @@ import-local@^3.0.2: pkg-dir "^4.2.0" resolve-cwd "^3.0.0" -import-modules@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/import-modules/-/import-modules-2.0.0.tgz#9c1e13b4e7a15682f70a6e3fa29534e4540cfc5d" - integrity sha512-iczM/v9drffdNnABOKwj0f9G3cFDon99VcG1mxeBsdqnbd+vnQ5c2uAiCHNQITqFTOPaEvwg3VjoWCur0uHLEw== - imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" @@ -7310,16 +7252,7 @@ inquirer@^8.2.4: through "^2.3.6" wrap-ansi "^7.0.0" -internal-slot@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.2.tgz#9c2e9fb3cd8e5e4256c6f45fe310067fcfa378a3" - integrity sha512-2cQNfwhAfJIkU4KZPkDI+Gj5yNNnbqi40W9Gge6dfnk4TocEVm00B3bdiL+JINrbGJil2TeHvM4rETGzk/f/0g== - dependencies: - es-abstract "^1.17.0-next.1" - has "^1.0.3" - side-channel "^1.0.2" - -internal-slot@^1.0.4: +internal-slot@^1.0.4, internal-slot@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.5.tgz#f2a2ee21f668f8627a4667f309dc0f4fb6674986" integrity sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ== @@ -7351,7 +7284,7 @@ is-arguments@^1.0.4, is-arguments@^1.1.1: call-bind "^1.0.2" has-tostringtag "^1.0.0" -is-array-buffer@^3.0.1: +is-array-buffer@^3.0.1, is-array-buffer@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.2.tgz#f2653ced8412081638ecb0ebbd0c41c6e0aecbbe" integrity sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w== @@ -7387,12 +7320,19 @@ is-boolean-object@^1.1.0: call-bind "^1.0.2" has-tostringtag "^1.0.0" -is-callable@^1.1.3: +is-builtin-module@^3.2.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-3.2.1.tgz#f03271717d8654cfcaf07ab0463faa3571581169" + integrity sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A== + dependencies: + builtin-modules "^3.3.0" + +is-callable@^1.1.3, is-callable@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== -is-callable@^1.1.4, is-callable@^1.2.2: +is-callable@^1.1.4: version "1.2.2" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.2.tgz#c7c6715cd22d4ddb48d3e19970223aceabb080d9" integrity sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA== @@ -7411,6 +7351,13 @@ is-core-module@^2.11.0, is-core-module@^2.5.0, is-core-module@^2.8.1: dependencies: has "^1.0.3" +is-core-module@^2.12.1, is-core-module@^2.13.0: + version "2.13.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.0.tgz#bb52aa6e2cbd49a30c2ba68c42bf3435ba6072db" + integrity sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ== + dependencies: + has "^1.0.3" + is-date-object@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" @@ -7467,7 +7414,7 @@ is-glob@^4.0.0, is-glob@^4.0.1: dependencies: is-extglob "^2.1.1" -is-glob@~4.0.1: +is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== @@ -7502,10 +7449,10 @@ is-nan@^1.2.1: call-bind "^1.0.0" define-properties "^1.1.3" -is-negative-zero@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.0.tgz#9553b121b0fac28869da9ed459e20c7543788461" - integrity sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE= +is-negative-zero@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" + integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== is-number-object@^1.0.4: version "1.0.7" @@ -7534,7 +7481,7 @@ is-path-cwd@^2.2.0: resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-2.2.0.tgz#67d43b82664a7b5191fd9119127eb300048a9fdb" integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== -is-path-inside@^3.0.2: +is-path-inside@^3.0.2, is-path-inside@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== @@ -7566,13 +7513,6 @@ is-potential-custom-element-name@^1.0.1: resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz#171ed6f19e3ac554394edf78caa05784a45bebb5" integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== -is-regex@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" - integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg== - dependencies: - has-symbols "^1.0.1" - is-regex@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" @@ -7654,6 +7594,13 @@ is-typed-array@^1.1.10, is-typed-array@^1.1.3: gopd "^1.0.1" has-tostringtag "^1.0.0" +is-typed-array@^1.1.9: + version "1.1.12" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.12.tgz#d0bab5686ef4a76f7a73097b95470ab199c57d4a" + integrity sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg== + dependencies: + which-typed-array "^1.1.11" + is-unicode-supported@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" @@ -7664,6 +7611,13 @@ is-weakmap@^2.0.1: resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2" integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA== +is-weakref@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + is-weakset@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.2.tgz#4569d67a747a1ce5a994dfd4ef6dcea76e7c0a1d" @@ -7679,16 +7633,16 @@ is-wsl@^2.1.1, is-wsl@^2.2.0: dependencies: is-docker "^2.0.0" -isarray@^1.0.0, isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= - isarray@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -8264,6 +8218,11 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== +jsesc@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e" + integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g== + jsesc@~0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" @@ -8309,10 +8268,10 @@ json-stringify-safe@^5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= -json5@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" - integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== +json5@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" + integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== dependencies: minimist "^1.2.0" @@ -8345,14 +8304,6 @@ jsonparse@^1.2.0, jsonparse@^1.3.1: resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= -jsx-ast-utils@^2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.4.1.tgz#1114a4c1209481db06c690c2b4f488cc665f657e" - integrity sha512-z1xSldJ6imESSzOjd3NNkieVJKRlKYSOtMG8SFyCj2FIrvSaSuli/WjpBkEzCBoR9bYYYFgqJw61Xhu7Lcgk+w== - dependencies: - array-includes "^3.1.1" - object.assign "^4.1.0" - just-diff-apply@^5.2.0: version "5.5.0" resolved "https://registry.yarnpkg.com/just-diff-apply/-/just-diff-apply-5.5.0.tgz#771c2ca9fa69f3d2b54e7c3f5c1dfcbcc47f9f0f" @@ -8373,18 +8324,6 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== -language-subtag-registry@~0.3.2: - version "0.3.20" - resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.20.tgz#a00a37121894f224f763268e431c55556b0c0755" - integrity sha512-KPMwROklF4tEx283Xw0pNKtfTj1gZ4UByp4EsIFWLgBavJltF4TiYPc39k06zSTsLzxTVXXDSpbwaQXaFB4Qeg== - -language-tags@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.5.tgz#d321dbc4da30ba8bf3024e040fa5c14661f9193a" - integrity sha1-0yHbxNowuovzAk4ED6XBRmH5GTo= - dependencies: - language-subtag-registry "~0.3.2" - lazy-universal-dotenv@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/lazy-universal-dotenv/-/lazy-universal-dotenv-4.0.0.tgz#0b220c264e89a042a37181a4928cdd298af73422" @@ -8573,16 +8512,6 @@ load-json-file@6.2.0: strip-bom "^4.0.0" type-fest "^0.6.0" -load-json-file@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" - integrity sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg= - dependencies: - graceful-fs "^4.1.2" - parse-json "^2.2.0" - pify "^2.0.0" - strip-bom "^3.0.0" - load-json-file@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" @@ -8642,11 +8571,6 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== -lodash.get@^4.4.2: - version "4.4.2" - resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" - integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= - lodash.ismatch@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz#756cb5150ca3ba6f11085a78849645f188f85f37" @@ -8662,16 +8586,6 @@ lodash.merge@^4.6.2: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== -lodash.truncate@^4.4.2: - version "4.4.2" - resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" - integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw== - -lodash.zip@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.zip/-/lodash.zip-4.2.0.tgz#ec6662e4896408ed4ab6c542a3990b72cc080020" - integrity sha1-7GZi5IlkCO1KtsVCo5kLcswIACA= - lodash@^4.17.15, lodash@^4.17.19: version "4.17.20" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" @@ -8961,7 +8875,7 @@ minimatch@3.0.5: dependencies: brace-expansion "^1.1.7" -minimatch@^3.0.2, minimatch@^3.1.1: +minimatch@^3.0.2, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -9158,16 +9072,11 @@ ms@2.1.2, ms@^2.0.0: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3: +ms@2.1.3, ms@^2.1.1: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -multimap@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/multimap/-/multimap-1.1.0.tgz#5263febc085a1791c33b59bb3afc6a76a2a10ca8" - integrity sha512-0ZIR9PasPxGXmRsEF8jsDzndzHDj7tIav+JUmvIFB/WHswliFnquxECT/De7GR4yg99ky/NlRKJT82G1y271bw== - multimatch@5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/multimatch/-/multimatch-5.0.0.tgz#932b800963cea7a31a033328fa1e0c3a1874dbe6" @@ -9189,6 +9098,11 @@ nanoid@^3.3.1, nanoid@^3.3.4: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== +natural-compare-lite@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" + integrity sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g== + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -9600,10 +9514,10 @@ object-assign@^4.1.1: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= -object-inspect@^1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" - integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA== +object-inspect@^1.12.3: + version "1.12.3" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" + integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g== object-inspect@^1.9.0: version "1.12.2" @@ -9623,16 +9537,6 @@ object-keys@^1.0.12, object-keys@^1.1.1: resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== -object.assign@^4.1.0, object.assign@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.1.tgz#303867a666cdd41936ecdedfb1f8f3e32a478cdd" - integrity sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.18.0-next.0" - has-symbols "^1.0.1" - object-keys "^1.1.1" - object.assign@^4.1.4: version "4.1.4" resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.4.tgz#9673c7c7c351ab8c4d0b516f4343ebf4dfb7799f" @@ -9643,34 +9547,33 @@ object.assign@^4.1.4: has-symbols "^1.0.3" object-keys "^1.1.1" -object.entries@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.2.tgz#bc73f00acb6b6bb16c203434b10f9a7e797d3add" - integrity sha512-BQdB9qKmb/HyNdMNWVr7O3+z5MUIx3aiegEIJqjMBbBf0YT9RRxTJSim4mzFqtyr7PDAHigq0N9dO0m0tRakQA== +object.fromentries@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.6.tgz#cdb04da08c539cffa912dcd368b886e0904bfa73" + integrity sha512-VciD13dswC4j1Xt5394WR4MzmAQmlgN72phd/riNp9vtD7tp4QQWJ0R4wvclXcafgcYK8veHRed2W6XeGBvcfg== dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.5" - has "^1.0.3" + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" -object.fromentries@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.2.tgz#4a09c9b9bb3843dd0f89acdb517a794d4f355ac9" - integrity sha512-r3ZiBH7MQppDJVLx6fhD618GKNG40CZYH9wgwdhKxBDDbQgjeWGGd4AtkZad84d291YxvWe7bJGuE65Anh0dxQ== +object.groupby@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.0.tgz#cb29259cf90f37e7bac6437686c1ea8c916d12a9" + integrity sha512-70MWG6NfRH9GnbZOikuhPPYzpUpof9iW2J9E4dW7FXTqPNb6rllE6u39SKwwiNh8lCwX3DDb5OgcKGiEBrTTyw== dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - function-bind "^1.1.1" - has "^1.0.3" + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.21.2" + get-intrinsic "^1.2.1" -object.values@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.1.tgz#68a99ecde356b7e9295a3c5e0ce31dc8c953de5e" - integrity sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA== +object.values@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.6.tgz#4abbaa71eba47d63589d402856f908243eea9b1d" + integrity sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw== dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - function-bind "^1.1.1" - has "^1.0.3" + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" objectorarray@^1.0.5: version "1.0.5" @@ -9732,17 +9635,17 @@ optionator@^0.8.1: type-check "~0.3.2" word-wrap "~1.2.3" -optionator@^0.9.1: - version "0.9.1" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" - integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== +optionator@^0.9.3: + version "0.9.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" + integrity sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg== dependencies: + "@aashutoshrathi/word-wrap" "^1.2.3" deep-is "^0.1.3" fast-levenshtein "^2.0.6" levn "^0.4.1" prelude-ls "^1.2.1" type-check "^0.4.0" - word-wrap "^1.2.3" ora@^5.4.1: version "5.4.1" @@ -9959,13 +9862,6 @@ parse-conflict-json@^3.0.0: just-diff "^6.0.0" just-diff-apply "^5.2.0" -parse-json@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" - integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= - dependencies: - error-ex "^1.2.0" - parse-json@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" @@ -10076,13 +9972,6 @@ path-to-regexp@0.1.7: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== -path-type@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" - integrity sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM= - dependencies: - pify "^2.0.0" - path-type@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" @@ -10134,7 +10023,7 @@ pify@5.0.0, pify@^5.0.0: resolved "https://registry.yarnpkg.com/pify/-/pify-5.0.0.tgz#1f5eca3f5e87ebec28cc6d54a0e4aaf00acc127f" integrity sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA== -pify@^2.0.0, pify@^2.3.0: +pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= @@ -10154,13 +10043,6 @@ pirates@^4.0.4, pirates@^4.0.5: resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== -pkg-dir@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" - integrity sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s= - dependencies: - find-up "^2.1.0" - pkg-dir@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" @@ -10254,7 +10136,19 @@ prelude-ls@~1.1.2: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= -prettier@^2.1.2, prettier@^2.8.0: +prettier-linter-helpers@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" + integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== + dependencies: + fast-diff "^1.1.2" + +prettier@^2.4.1: + version "2.8.8" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" + integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== + +prettier@^2.8.0: version "2.8.7" resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.7.tgz#bb79fc8729308549d28fe3a98fce73d2c0656450" integrity sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw== @@ -10329,7 +10223,7 @@ process@^0.11.10: resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= -progress@^2.0.0, progress@^2.0.1: +progress@^2.0.1: version "2.0.3" resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== @@ -10674,14 +10568,6 @@ read-package-json@^6.0.0: normalize-package-data "^5.0.0" npm-normalize-package-bin "^3.0.0" -read-pkg-up@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" - integrity sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4= - dependencies: - find-up "^2.0.0" - read-pkg "^2.0.0" - read-pkg-up@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07" @@ -10699,15 +10585,6 @@ read-pkg-up@^7.0.1: read-pkg "^5.2.0" type-fest "^0.8.1" -read-pkg@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" - integrity sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg= - dependencies: - load-json-file "^2.0.0" - normalize-package-data "^2.3.2" - path-type "^2.0.0" - read-pkg@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" @@ -10860,19 +10737,16 @@ regenerator-transform@^0.15.1: dependencies: "@babel/runtime" "^7.8.4" -regexp-tree@^0.1.21, regexp-tree@~0.1.1: +regexp-tree@^0.1.24: + version "0.1.27" + resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.27.tgz#2198f0ef54518ffa743fe74d983b56ffd631b6cd" + integrity sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA== + +regexp-tree@~0.1.1: version "0.1.21" resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.21.tgz#55e2246b7f7d36f1b461490942fa780299c400d7" integrity sha512-kUUXjX4AnqnR8KRTCrayAo9PzYMRKmVoGgaz2tBuz0MF3g1ZbGebmtW0yFHfFK9CmBjQKeYIgoL22pFLBJY7sw== -regexp.prototype.flags@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75" - integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - regexp.prototype.flags@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz#87cab30f80f66660181a3bb7bf5981a872b367ac" @@ -10882,10 +10756,14 @@ regexp.prototype.flags@^1.4.3: define-properties "^1.1.3" functions-have-names "^1.2.2" -regexpp@^3.0.0, regexpp@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.1.0.tgz#206d0ad0a5648cffbdb8ae46438f3dc51c9f78e2" - integrity sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q== +regexp.prototype.flags@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz#fe7ce25e7e4cca8db37b6634c8a2c7009199b9cb" + integrity sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + functions-have-names "^1.2.3" regexpu-core@^5.3.1: version "5.3.2" @@ -10937,11 +10815,6 @@ requires-port@^1.0.0: resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== -reserved-words@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/reserved-words/-/reserved-words-0.1.2.tgz#00a0940f98cd501aeaaac316411d9adc52b31ab1" - integrity sha1-AKCUD5jNUBrqqsMWQR2a3FKzGrE= - resolve-cwd@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" @@ -10973,13 +10846,22 @@ resolve@^1.1.6, resolve@^1.14.2, resolve@^1.20.0: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.17.0: +resolve@^1.10.0: version "1.17.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== dependencies: path-parse "^1.0.6" +resolve@^1.22.3, resolve@^1.22.4: + version "1.22.4" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.4.tgz#1dc40df46554cdaf8948a486a10f6ba1e2026c34" + integrity sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + restore-cursor@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" @@ -11057,6 +10939,16 @@ rxjs@^7.5.1, rxjs@^7.5.5: dependencies: tslib "^2.1.0" +safe-array-concat@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.0.0.tgz#2064223cba3c08d2ee05148eedbc563cd6d84060" + integrity sha512-9dVEFruWIsnie89yym+xWTAYASdpw3CJV7Li/6zBewGf9z2i1j31rP6jnY0pHEO4QZh6N0K11bFjWmdR8UGdPQ== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.0" + has-symbols "^1.0.3" + isarray "^2.0.5" + safe-buffer@5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" @@ -11072,6 +10964,15 @@ safe-buffer@5.2.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +safe-regex-test@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.0.tgz#793b874d524eb3640d1873aad03596db2d4f2295" + integrity sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.3" + is-regex "^1.1.4" + safe-regex@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-2.1.1.tgz#f7128f00d056e2fe5c11e81a1324dd974aadced2" @@ -11142,7 +11043,7 @@ semver@7.3.8, semver@^7.0.0, semver@^7.1.1, semver@^7.3.5, semver@^7.3.7, semver dependencies: lru-cache "^6.0.0" -semver@7.x, semver@^7.2.1, semver@^7.3.2: +semver@7.x: version "7.3.2" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== @@ -11152,6 +11053,11 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + semver@^7.3.4: version "7.3.7" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" @@ -11249,14 +11155,6 @@ shelljs@^0.8.5: interpret "^1.0.0" rechoir "^0.6.2" -side-channel@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.3.tgz#cdc46b057550bbab63706210838df5d4c19519c3" - integrity sha512-A6+ByhlLkksFoUepsGxfj5x1gTSrs+OydsRptUxeNCabQpCFUvcwIczgOigI8vhY/OJCnPnyE9rGiwgvr9cS1g== - dependencies: - es-abstract "^1.18.0-next.0" - object-inspect "^1.8.0" - side-channel@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" @@ -11387,11 +11285,6 @@ source-map-support@^0.5.16, source-map-support@~0.5.20: buffer-from "^1.0.0" source-map "^0.6.0" -source-map@^0.5.0: - version "0.5.7" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" - integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= - source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" @@ -11508,11 +11401,6 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -string-natural-compare@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/string-natural-compare/-/string-natural-compare-3.0.1.tgz#7a42d58474454963759e8e8b7ae63d71c1e7fdf4" - integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== - "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.0.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -11531,33 +11419,32 @@ string-width@^4.1.0, string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" -string.prototype.matchall@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.2.tgz#48bb510326fb9fdeb6a33ceaa81a6ea04ef7648e" - integrity sha512-N/jp6O5fMf9os0JU3E72Qhf590RSRZU/ungsL/qJUYVTNv7hTG0P/dbPjxINVN9jpscu3nzYwKESU3P3RY5tOg== +string.prototype.trim@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz#a68352740859f6893f14ce3ef1bb3037f7a90533" + integrity sha512-p6TmeT1T3411M8Cgg9wBTMRtY2q9+PNy9EV1i2lIXUN/btt763oIfxwN3RR8VU6wHX8j/1CFy0L+YuThm6bgOg== dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.0" - has-symbols "^1.0.1" - internal-slot "^1.0.2" - regexp.prototype.flags "^1.3.0" - side-channel "^1.0.2" + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" -string.prototype.trimend@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913" - integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g== +string.prototype.trimend@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz#c4a27fa026d979d79c04f17397f250a462944533" + integrity sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ== dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.5" + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" -string.prototype.trimstart@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54" - integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw== +string.prototype.trimstart@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz#e90ab66aa8e4007d92ef591bbf3cd422c56bdcf4" + integrity sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA== dependencies: - define-properties "^1.1.3" - es-abstract "^1.17.5" + call-bind "^1.0.2" + define-properties "^1.1.4" + es-abstract "^1.20.4" string_decoder@^1.1.1: version "1.3.0" @@ -11618,7 +11505,7 @@ strip-indent@^3.0.0: dependencies: min-indent "^1.0.0" -strip-json-comments@^3.0.1, strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: +strip-json-comments@^3.0.1, strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== @@ -11673,17 +11560,6 @@ synchronous-promise@^2.0.15: resolved "https://registry.yarnpkg.com/synchronous-promise/-/synchronous-promise-2.0.16.tgz#669b75e86b4295fdcc1bb0498de9ac1af6fd51a9" integrity sha512-qImOD23aDfnIDNqlG1NOehdB9IYsn1V9oByPjKY1nakv2MQYCEMyX033/q+aEtYCpmYK1cv2+NTmlH+ra6GA5A== -table@^6.0.9: - version "6.8.1" - resolved "https://registry.yarnpkg.com/table/-/table-6.8.1.tgz#ea2b71359fe03b017a5fbc296204471158080bdf" - integrity sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA== - dependencies: - ajv "^8.0.1" - lodash.truncate "^4.4.2" - slice-ansi "^4.0.0" - string-width "^4.2.3" - strip-ansi "^6.0.1" - tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" @@ -11937,14 +11813,14 @@ ts-loader@^9.4.2: micromatch "^4.0.0" semver "^7.3.4" -tsconfig-paths@^3.9.0: - version "3.9.0" - resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b" - integrity sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw== +tsconfig-paths@^3.14.2: + version "3.14.2" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz#6e32f1f79412decd261f92d633a9dc1cfa99f088" + integrity sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g== dependencies: "@types/json5" "^0.0.29" - json5 "^1.0.1" - minimist "^1.2.0" + json5 "^1.0.2" + minimist "^1.2.6" strip-bom "^3.0.0" tsconfig-paths@^4.1.2: @@ -11966,10 +11842,10 @@ tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4 resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== -tsutils@^3.17.1: - version "3.17.1" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" - integrity sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g== +tsutils@^3.21.0: + version "3.21.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" + integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== dependencies: tslib "^1.8.1" @@ -12048,6 +11924,45 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" +typed-array-buffer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz#18de3e7ed7974b0a729d3feecb94338d1472cd60" + integrity sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.1" + is-typed-array "^1.1.10" + +typed-array-byte-length@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz#d787a24a995711611fb2b87a4052799517b230d0" + integrity sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA== + dependencies: + call-bind "^1.0.2" + for-each "^0.3.3" + has-proto "^1.0.1" + is-typed-array "^1.1.10" + +typed-array-byte-offset@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz#cbbe89b51fdef9cd6aaf07ad4707340abbc4ea0b" + integrity sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + has-proto "^1.0.1" + is-typed-array "^1.1.10" + +typed-array-length@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb" + integrity sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng== + dependencies: + call-bind "^1.0.2" + for-each "^0.3.3" + is-typed-array "^1.1.9" + typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" @@ -12068,6 +11983,16 @@ uglify-js@^3.1.4: resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.11.1.tgz#32d274fea8aac333293044afd7f81409d5040d38" integrity sha512-OApPSuJcxcnewwjSGGfWOjx3oix5XpmrK9Z2j0fTRlHGoZ49IU6kExfZTM0++fCArOOCet+vIfWwFHbvWqwp6g== +unbox-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" + integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== + dependencies: + call-bind "^1.0.2" + has-bigints "^1.0.2" + has-symbols "^1.0.3" + which-boxed-primitive "^1.0.2" + unfetch@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be" @@ -12235,11 +12160,6 @@ v8-compile-cache@2.3.0: resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== -v8-compile-cache@^2.0.3: - version "2.1.1" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745" - integrity sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ== - v8-to-istanbul@^9.0.0, v8-to-istanbul@^9.0.1: version "9.1.0" resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz#1b83ed4e397f58c85c266a570fc2558b5feb9265" @@ -12302,7 +12222,7 @@ walker@^1.0.8: dependencies: makeerror "1.0.12" -watchpack@^2.2.0, watchpack@^2.4.0: +watchpack@^2.2.0, watchpack@^2.3.1, watchpack@^2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== @@ -12395,6 +12315,36 @@ webpack@5: watchpack "^2.4.0" webpack-sources "^3.2.3" +webpack@~5.72.0: + version "5.72.1" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.72.1.tgz#3500fc834b4e9ba573b9f430b2c0a61e1bb57d13" + integrity sha512-dXG5zXCLspQR4krZVR6QgajnZOjW2K/djHvdcRaDQvsjV9z9vaW6+ja5dZOYbqBBjF6kGXka/2ZyxNdc+8Jung== + dependencies: + "@types/eslint-scope" "^3.7.3" + "@types/estree" "^0.0.51" + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/wasm-edit" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + acorn "^8.4.1" + acorn-import-assertions "^1.7.6" + browserslist "^4.14.5" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.9.3" + es-module-lexer "^0.9.0" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.9" + json-parse-even-better-errors "^2.3.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^3.1.0" + tapable "^2.1.1" + terser-webpack-plugin "^5.1.3" + watchpack "^2.3.1" + webpack-sources "^3.2.3" + whatwg-encoding@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz#e7635f597fd87020858626805a2729fa7698ac53" @@ -12444,6 +12394,17 @@ which-collection@^1.0.1: is-weakmap "^2.0.1" is-weakset "^2.0.1" +which-typed-array@^1.1.10, which-typed-array@^1.1.11: + version "1.1.11" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.11.tgz#99d691f23c72aab6768680805a271b69761ed61a" + integrity sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.0" + which-typed-array@^1.1.2, which-typed-array@^1.1.9: version "1.1.9" resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.9.tgz#307cf898025848cf995e795e8423c7f337efbde6" @@ -12489,7 +12450,7 @@ wildcard@^2.0.0: resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw== -word-wrap@^1.2.3, word-wrap@~1.2.3: +word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==