From fba1ee363b858127c4fa5b0a834640b784de551d Mon Sep 17 00:00:00 2001 From: Tommaso Berlose Date: Wed, 29 May 2024 16:19:53 +0200 Subject: [PATCH] Add tests and major refactoring --- .eslintrc.js | 28 ++ .prettierrc | 7 + eslint.config.mjs | 10 + jest.config.ts | 16 ++ package.json | 10 +- src/extraReducersBuilder.ts | 159 ++++++++--- src/index.ts | 159 +---------- src/middleware.ts | 6 + src/settings.ts | 54 +++- src/slice.ts | 213 +++++++++++++++ src/store.ts | 40 ++- src/types.d.ts | 13 +- src/updatedAtHelper.ts | 74 ++++++ test/blah.test.ts | 7 - test/mocks.ts | 37 +++ test/settings.test.ts | 24 ++ test/store.test.ts | 22 ++ test/updatedAtHelper.test.ts | 20 ++ yarn.lock | 492 +++++++++++++++++++++++++++++++++-- 19 files changed, 1148 insertions(+), 243 deletions(-) create mode 100644 .eslintrc.js create mode 100644 .prettierrc create mode 100644 eslint.config.mjs create mode 100644 jest.config.ts create mode 100644 src/slice.ts create mode 100644 src/updatedAtHelper.ts delete mode 100644 test/blah.test.ts create mode 100644 test/mocks.ts create mode 100644 test/settings.test.ts create mode 100644 test/store.test.ts create mode 100644 test/updatedAtHelper.test.ts diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..700c4a0 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,28 @@ +const globals = require("globals"); + +module.exports = { + root: true, + plugins: [ + "@typescript-eslint/eslint-plugin", + "eslint-plugin-tsdoc" + ], + extends: [ + 'plugin:@typescript-eslint/recommended' + ], + parser: '@typescript-eslint/parser', + parserOptions: { + project: "./tsconfig.json", + tsconfigRootDir: __dirname, + ecmaVersion: 2018, + sourceType: "module" + }, + rules: { + "tsdoc/syntax": "warn", + 'prettier/prettier': [ + 'error', + { + 'no-inline-styles': false, + }, + ], + }, +}; diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..2ae7b38 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +module.exports = { + arrowParens: 'avoid', + bracketSameLine: true, + bracketSpacing: true, + singleQuote: true, + trailingComma: 'all', +}; diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..67c6a34 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,10 @@ +import globals from "globals"; +import pluginJs from "@eslint/js"; +import tseslint from "typescript-eslint"; + + +export default [ + {languageOptions: { globals: globals.browser }}, + pluginJs.configs.recommended, + ...tseslint.configs.recommended, +]; \ No newline at end of file diff --git a/jest.config.ts b/jest.config.ts new file mode 100644 index 0000000..161715a --- /dev/null +++ b/jest.config.ts @@ -0,0 +1,16 @@ +module.exports = { + testMatch: [ + '(/test/.*|(\\.|/)(test|spec))\\.[jt]sx?$', + '**/?(*.)+(spec|test).[tj]s?(x)', + ], + transform: { + '^.+\\.[t|j]sx?$': 'babel-jest', + }, + preset: 'ts-jest', + testEnvironment: 'node', + globals: { + 'ts-jest': { + tsconfig: 'tsconfig.json', + }, + }, +}; \ No newline at end of file diff --git a/package.json b/package.json index d332bce..07d045a 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "version": "0.1.0-alpha.13", + "version": "0.1.0-alpha.35", "license": "MIT", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -45,12 +45,18 @@ } ], "devDependencies": { + "@eslint/js": "^9.1.1", "@size-limit/preset-small-lib": "^11.1.2", + "eslint": "^9.1.1", + "eslint-plugin-tsdoc": "^0.2.17", + "globals": "^15.1.0", "husky": "^9.0.11", + "prettier": "3.2.5", "size-limit": "^11.1.2", "tsdx": "^0.14.1", "tslib": "^2.6.2", - "typescript": "^5.4.5" + "typescript": "^5.4.5", + "typescript-eslint": "^7.8.0" }, "dependencies": { "@reduxjs/toolkit": "^2.2.3" diff --git a/src/extraReducersBuilder.ts b/src/extraReducersBuilder.ts index 9950577..32ddb9c 100644 --- a/src/extraReducersBuilder.ts +++ b/src/extraReducersBuilder.ts @@ -1,59 +1,132 @@ -import { ActionReducerMapBuilder, CaseReducer, Action } from "@reduxjs/toolkit"; - -export class Builder< - SliceState, - PersistedSliceState extends SliceState & { - sliceStorageLastUpdateAt?: number; - }, -> implements ActionReducerMapBuilder +import { ActionReducerMapBuilder, CaseReducer, Action, Draft } from "@reduxjs/toolkit"; + +/** + * Extension of a reducer that invokes a callback every time + * this specific reducer is called. + * + * @internal + */ +const persistReducer = (r: CaseReducer, onStateUpdate: () => void) => (s: Draft, a: Action): void | SliceState | Draft => { + const ns = r(s, a); + onStateUpdate(); + if (ns) return ns; +}; + +/** + * A builder for an action <-> reducer map updating the attribute + * sliceStorageLastUpdateAt whenever the state is changed. + * + * @param builder The builder of the extraReducers + * @param onStateUpdate The callback to invoke when the state is changed + * + * @public + */ +export class Builder implements ActionReducerMapBuilder { - name: string; - builder: ActionReducerMapBuilder; + builder: ActionReducerMapBuilder; + onStateUpdate: () => void; constructor( - name: string, - builder: ActionReducerMapBuilder, + builder: ActionReducerMapBuilder, + onStateUpdate: () => void, ) { - this.name = name; this.builder = builder; + this.onStateUpdate = onStateUpdate; } - addDefaultCase(r: CaseReducer) { - this.builder.addDefaultCase((s, a) => { - const ns = r(s, a); - if (ns) { - ns.sliceStorageLastUpdateAt = new Date().getMilliseconds(); - } else { - s.sliceStorageLastUpdateAt = new Date().getMilliseconds(); - } - return ns; - }); + /** + * Adds a "default case" reducer that is executed if no case reducer and no matcher + * reducer was executed for this action. + * @param reducer - The fallback "default case" reducer function. + * + * @example + ```ts + import { createReducer } from '@reduxjs/toolkit' + const initialState = { otherActions: 0 } + const reducer = createReducer(initialState, builder => { + builder + // .addCase(...) + // .addMatcher(...) + .addDefaultCase((state, action) => { + state.otherActions++ + }) + }) + ``` + */ + addDefaultCase(r: CaseReducer) { + this.builder.addDefaultCase(persistReducer(r, this.onStateUpdate)); return this; } - addMatcher(p: any, r: CaseReducer) { - this.builder.addMatcher(p, (s, a) => { - const ns = r(s, a); - if (ns) { - ns.sliceStorageLastUpdateAt = new Date().getMilliseconds(); - } else { - s.sliceStorageLastUpdateAt = new Date().getMilliseconds(); - } - return ns; - }); + /** + * Allows you to match your incoming actions against your own filter function instead of only the `action.type` property. + * @remarks + * If multiple matcher reducers match, all of them will be executed in the order + * they were defined in - even if a case reducer already matched. + * All calls to `builder.addMatcher` must come after any calls to `builder.addCase` and before any calls to `builder.addDefaultCase`. + * @param matcher - A matcher function. In TypeScript, this should be a [type predicate](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates) + * function + * @param reducer - The actual case reducer function. + * + * @example + ```ts + import { + createAction, + createReducer, + AsyncThunk, + UnknownAction, + } from "@reduxjs/toolkit"; + + type GenericAsyncThunk = AsyncThunk; + + type PendingAction = ReturnType; + type RejectedAction = ReturnType; + type FulfilledAction = ReturnType; + + const initialState: Record = {}; + const resetAction = createAction("reset-tracked-loading-state"); + + function isPendingAction(action: UnknownAction): action is PendingAction { + return typeof action.type === "string" && action.type.endsWith("/pending"); + } + const reducer = createReducer(initialState, (builder) => { + builder + .addCase(resetAction, () => initialState) + // matcher can be defined outside as a type predicate function + .addMatcher(isPendingAction, (state, action) => { + state[action.meta.requestId] = "pending"; + }) + .addMatcher( + // matcher can be defined inline as a type predicate function + (action): action is RejectedAction => action.type.endsWith("/rejected"), + (state, action) => { + state[action.meta.requestId] = "rejected"; + } + ) + // matcher can just return boolean and the matcher can receive a generic argument + .addMatcher( + (action) => action.type.endsWith("/fulfilled"), + (state, action) => { + state[action.meta.requestId] = "fulfilled"; + } + ); + }); + ``` + */ + addMatcher(p: any, r: CaseReducer) { + this.builder.addMatcher(p, persistReducer(r, this.onStateUpdate)); return this; } - addCase(ac: any, r: CaseReducer) { - this.builder.addCase(ac, (s, a) => { - const ns = r(s, a); - if (ns) { - ns.sliceStorageLastUpdateAt = new Date().getMilliseconds(); - } else { - s.sliceStorageLastUpdateAt = new Date().getMilliseconds(); - } - return ns; - }); + /** + * Adds a case reducer to handle a single exact action type. + * @remarks + * All calls to `builder.addCase` must come before any calls to `builder.addMatcher` or `builder.addDefaultCase`. + * @param actionCreator - Either a plain action type string, or an action creator generated by [`createAction`](./createAction) that can be used to determine the action type. + * @param reducer - The actual case reducer function. + */ + addCase(ac: string, r: CaseReducer) { + this.builder.addCase(ac, persistReducer(r, this.onStateUpdate)); return this; } } \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index ddb2abf..34e22be 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,160 +1,7 @@ -import { - createSlice, - CreateSliceOptions, - ListenerMiddlewareInstance, - Slice, - SliceCaseReducers, - SliceSelectors, -} from '@reduxjs/toolkit'; -import Settings from './settings'; -import { Builder } from './extraReducersBuilder'; -import { configurePersistedStore } from './store'; -import { listenerMiddleware } from './middleware'; - -const createPersistedSlice: < - PersistedSliceState extends Object & { - sliceStorageLastUpdateAt?: number; - }, - Name extends string = string, - PCR extends - SliceCaseReducers = SliceCaseReducers, - ReducerPath extends string = Name, - PeristedSelectors extends - SliceSelectors = SliceSelectors, ->( - sliceOptions: CreateSliceOptions< - PersistedSliceState, - PCR, - Name, - ReducerPath, - PeristedSelectors - >, - filtersSlice?: (state: PersistedSliceState) => Partial, -) => Omit< - Slice, - 'getInitialState' -> & { - getInitialState: () => Promise; - listenerMiddleware: ListenerMiddlewareInstance; - clearPersistedStorage: () => void; -} = < - PersistedSliceState extends { - sliceStorageLastUpdateAt?: number; - }, - Name extends string = string, - PCR extends - SliceCaseReducers = SliceCaseReducers, - ReducerPath extends string = Name, - PeristedSelectors extends - SliceSelectors = SliceSelectors, ->( - sliceOptions: CreateSliceOptions< - PersistedSliceState, - PCR, - Name, - ReducerPath, - PeristedSelectors - >, - filtersSlice: (state: PersistedSliceState) => Partial = state => state, -) => { - const storageName = `storage-${sliceOptions.name}`; - - const startAppListening = - listenerMiddleware.startListening.withTypes< - Record - >(); - - const slice = createSlice< - PersistedSliceState, - PCR, - Name, - PeristedSelectors, - ReducerPath - >({ - ...sliceOptions, - initialState: () => { - const init = - typeof sliceOptions.initialState === 'function' - ? sliceOptions.initialState() - : sliceOptions.initialState; - return { - ...init, - sliceStorageLastUpdateAt: 0, - }; - }, - extraReducers: builder => { - const b = new Builder(slice.name, builder); - sliceOptions.extraReducers?.(b); - }, - }); - - async function getInitialState(): Promise { - let storage = slice.getInitialState(); - try { - const storageJson = (await Settings.storageHandler.getItem(storageName)) as - | string - | null; - storage = JSON.parse(storageJson ?? '{}'); - } catch (e) { - console.error(e); - } - return { ...slice.getInitialState(), ...storage }; - } - - async function writePersistedStorage(storedData: PersistedSliceState) { - await Settings.storageHandler.setItem( - storageName, - JSON.stringify(filtersSlice(storedData)), - ); - } - - async function clearPersistedStorage() { - await Settings.storageHandler.removeItem(storageName); - } - - // Slice reducer handler - Object.keys(slice.actions).forEach(type => { - startAppListening({ - type: `${slice.name}/${type}`, - effect: (_action, { getState }) => { - const state = getState(); - writePersistedStorage(state[sliceOptions.name]); - }, - }); - }); - - startAppListening({ - predicate: (_action, currentState, previousState) => { - const current = currentState[sliceOptions.name]; - const previous = previousState[sliceOptions.name]; - return ( - current.sliceStorageLastUpdateAt !== previous.sliceStorageLastUpdateAt - ); - }, - effect: (_action, { getState }) => { - const state = getState(); - writePersistedStorage(state[sliceOptions.name]); - }, - }); - - startAppListening({ - type: '@@INIT-PERSIST', - effect: async (_action, { getState }) => { - const state = getState(); - state[sliceOptions.name] = await getInitialState(); - }, - }); - - return { - ...slice, - getInitialState, - listenerMiddleware, - clearPersistedStorage, - }; -}; +import { createPersistedSlice } from "./slice"; +import { configurePersistedStore } from "./store"; export { - Settings, + createPersistedSlice, configurePersistedStore, }; -export default createPersistedSlice; diff --git a/src/middleware.ts b/src/middleware.ts index e7c4091..0a1339b 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,3 +1,9 @@ import { createListenerMiddleware } from "@reduxjs/toolkit"; +/** + * Global RTK listener middleware + * + * {@link https://redux-toolkit.js.org/api/createListenerMiddleware} + * + */ export const listenerMiddleware = createListenerMiddleware(); diff --git a/src/settings.ts b/src/settings.ts index 35efab8..24374aa 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,15 +1,57 @@ import { StorageHandler } from "./types"; -let defaultStorageHandler: StorageHandler; +/** + * Global storage handler variable + * @private + */ +let _storageHandler: StorageHandler | undefined; + +/** + * Global Setting class that encapsulates all the library settings. + * @public + */ export default class Settings { + constructor() { + _storageHandler = undefined; + } + + /** + * Returns the selected storage handler if set. + * + * @returns The selected storage handler + * + * @throws {@link TypeError} + * This exception is thrown if the storage handler has not been set. + * + * @public + */ static get storageHandler(): StorageHandler { - if (!defaultStorageHandler) { - throw new Error('The default storage handler must be set.'); + if (!_storageHandler) { + throw new TypeError('The default storage handler must be set.'); } - return defaultStorageHandler; + return _storageHandler; } + /** + * Sets the storage handler to be used when persist the data. + * + * @param storageHandler - The selected storage handler. + * + * @public + */ static set storageHandler(storageHandler: StorageHandler) { - defaultStorageHandler = storageHandler; + _storageHandler = storageHandler; + } +} + +export class TestSettings extends Settings { + + + /** + * Restores the default settings. + * @internal + */ + static _clearSettings() { + _storageHandler = undefined; } -} \ No newline at end of file +} diff --git a/src/slice.ts b/src/slice.ts new file mode 100644 index 0000000..a925598 --- /dev/null +++ b/src/slice.ts @@ -0,0 +1,213 @@ +import { + createSlice, + CreateSliceOptions, + ListenerMiddlewareInstance, + PayloadAction, + Slice, + SliceCaseReducers, + SliceSelectors, +} from '@reduxjs/toolkit'; +import Settings from './settings'; +import { Builder } from './extraReducersBuilder'; +import { DEFAULT_INIT_ACTION_TYPE } from './store'; +import { listenerMiddleware } from './middleware'; +import UpdatedAtHelper from './updatedAtHelper'; + +const getStorageName = (sliceName: string) => `persisted-storage-${sliceName}`; + +/** + * Return the stored data of a slice if saved + * + * @returns The stored state of the slice if saved + * + * @public + */ +export async function getStoredState(sliceName: string): Promise | null> { + try { + const storageJson = (await Settings.storageHandler.getItem(getStorageName(sliceName))); + if (!storageJson) return null; + return JSON.parse(storageJson); + } catch (e) { + // console.error(e); + } + return null; +} + +/** + * A function that accepts an initial state, an object full of reducer + * functions, and a "slice name", and automatically generates + * action creators and action types that correspond to the + * reducers and state. + * + * The state will be persisted throughout multiple reloads. + * It requires to use {@link configurePersistedStore | configurePersistedStore} + * + * @param options The slice configuration. + * @returns A persisted Redux slice. + * + * @public + */ +export const createPersistedSlice: < + SliceState, + Name extends string = string, + PCR extends + SliceCaseReducers = SliceCaseReducers, + ReducerPath extends string = Name, + PeristedSelectors extends + SliceSelectors = SliceSelectors, +>( + sliceOptions: CreateSliceOptions< + SliceState, + PCR, + Name, + ReducerPath, + PeristedSelectors + >, + filtersSlice?: (state: SliceState) => Partial, +) => Omit< + Slice, + 'getInitialState' +> & { + getInitialState: () => Promise; + listenerMiddleware: ListenerMiddlewareInstance; + clearPersistedStorage: () => void; +} = < + SliceState, + Name extends string = string, + PCR extends + SliceCaseReducers = SliceCaseReducers, + ReducerPath extends string = Name, + PeristedSelectors extends + SliceSelectors = SliceSelectors, +>( + sliceOptions: CreateSliceOptions< + SliceState, + PCR, + Name, + ReducerPath, + PeristedSelectors + >, + filtersSlice: (state: SliceState) => Partial = state => state, +) => { + const storageName = getStorageName(sliceOptions.name); + + /** + * Creates a typed version of the startListening function + * of the listener middlerware + * + * {@link @reduxjs/toolkit#createListenerMiddleware} + */ + const startAppListening = + listenerMiddleware.startListening.withTypes< + Record + >(); + + /** + * Overrides the getInitialState function to return the stored data + * + * @returns The initial state of the slice merged with the stored data + * + * @public + */ + async function getInitialState(): Promise { + let storage: Partial = slice.getInitialState(); + try { + storage = await getStoredState(sliceOptions.name) ?? storage; + } catch (e) { + // console.error(e); + } + return { ...slice.getInitialState(), ...storage }; + } + + /** + * Writes the updated state to the selected storage + * + * @param storedData The state to be persisted + * + * @internal + */ + async function writePersistedStorage(storedData: SliceState) { + await Settings.storageHandler.setItem( + storageName, + JSON.stringify(filtersSlice(storedData)), + ); + UpdatedAtHelper.onSave(sliceOptions.name); + } + + /** + * Clears the stored data from the selected storage + * + * @public + */ + async function clearPersistedStorage() { + await Settings.storageHandler.removeItem(storageName); + } + + /** + * Creates the slice using the default options passed by the user. + * + * We add the default state the attribute sliceStorageLastUpdateAt that tracks + * when a change happens only on the specific state slice. + * + * We then extend each extra readucer to update sliceStorageLastUpdateAt. + */ + const slice = createSlice({ + ...sliceOptions, + extraReducers: builder => { + builder.addMatcher(({ type }) => type === `${sliceOptions.name}\\${DEFAULT_INIT_ACTION_TYPE}`, (_state, action: PayloadAction): void | SliceState => { + if (action.payload) return action.payload; + }); + const b = new Builder(builder, UpdatedAtHelper.onStateChange.bind(null, sliceOptions.name)); + sliceOptions.extraReducers?.(b); + }, + }); + + /** + * Adds a listener for every action of the slice + * to react when they are dispatched and save + * the new state to the storage + */ + Object.keys(slice.actions).forEach(type => { + startAppListening({ + type: `${slice.name}/${type}`, + effect: (_action, { getState }) => { + const state = getState(); + writePersistedStorage(state[sliceOptions.name]); + }, + }); + }); + + /** + * Adds the listener to any actions handled by the slice + * and updates the stored data if a change happened. + * + * We track all the changes of our state updating a custom + * attribute saving the time when the change happened. + */ + startAppListening({ + predicate: (action) => { + if (action.type === DEFAULT_INIT_ACTION_TYPE || action.type.startsWith(`${slice.name}/`)) return false; + return true; + }, + effect: async (_action, { getState }) => { + if (!await UpdatedAtHelper.shouldSave(sliceOptions.name)) return; + const state = getState(); + writePersistedStorage(state[sliceOptions.name]); + }, + }); + + // TODO + startAppListening({ + type: `${slice.name}/${DEFAULT_INIT_ACTION_TYPE}`, + effect: () => { + UpdatedAtHelper.onStateChange(sliceOptions.name); + }, + }); + + return { + ...slice, + getInitialState, + listenerMiddleware, + clearPersistedStorage, + }; +}; diff --git a/src/store.ts b/src/store.ts index 2b97998..fda988d 100644 --- a/src/store.ts +++ b/src/store.ts @@ -1,25 +1,47 @@ -import { UnknownAction, EnhancedStore, configureStore, ConfigureStoreOptions } from "@reduxjs/toolkit"; +import { configureStore, ConfigureStoreOptions } from "@reduxjs/toolkit"; import { listenerMiddleware } from "./middleware"; import { StorageHandler } from "./types"; import Settings from "./settings"; +import { getStoredState } from "./slice"; -const INIT_ACTION: UnknownAction = { type: '@@INIT-PERSIST' }; -const setupPersistedStore = (store: EnhancedStore) => { - store.dispatch(INIT_ACTION); -}; +export const DEFAULT_INIT_ACTION_TYPE = '@@INIT-PERSIST'; +/** + * A friendly incapsulation of the standard RTK `configureStore()` function + * to add the option to persist slices. + * + * @param options The store configuration. + * @param storageHandler The storage handler to use to persist the data. + * @returns A configured Redux store. + * + * If persisted slices are present they will be persisted + * throughout multiple reloads of the store. + * + * {@link @reduxjs/toolkit#configureStore} + * + * @public + */ export const configurePersistedStore = (options: ConfigureStoreOptions, storageHandler: StorageHandler) => { + // Set the default storage handler Settings.storageHandler = storageHandler; + // Create the store adding our listener middleware to react to the state changes const persistedStore = configureStore({ ...options, middleware: (getDefaultMiddleware) => { - if (options.middleware) return options.middleware(getDefaultMiddleware).prepend(listenerMiddleware.middleware); - else return getDefaultMiddleware().prepend(listenerMiddleware.middleware); + if (options.middleware) return options.middleware(getDefaultMiddleware).concat(listenerMiddleware.middleware); + else return getDefaultMiddleware().concat(listenerMiddleware.middleware); } }); - setupPersistedStore(persistedStore); + Object.keys(persistedStore.getState()).forEach((k) => { + getStoredState(k).then((initialState) => { + persistedStore.dispatch({ + type: `${k}\\${DEFAULT_INIT_ACTION_TYPE}`, + payload: initialState, + }); + }); + }); return persistedStore; -} \ No newline at end of file +} diff --git a/src/types.d.ts b/src/types.d.ts index 55be97a..f299517 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -1,5 +1,16 @@ +/** + * This is the description of the storage handler used to persist the data + * + * @interface StorageHandler + * @member {function} setItem is used to save the stringified version of the data inot the database + * @member {function} getItem is used to retrive the stringified version of the data from the database + * @member {function} removeItem is used to remove the data from the database + * + * Available storage could be {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage | localStorage}, {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage | sessionStorage} or {@link https://github.com/react-native-async-storage/async-storage | AsyncStorage} + * + */ export interface StorageHandler { setItem: (key: string, value: string) => Promise | void; getItem: (key: string) => Promise | (string | null); removeItem: (key: string) => Promise | void; -} \ No newline at end of file +} diff --git a/src/updatedAtHelper.ts b/src/updatedAtHelper.ts new file mode 100644 index 0000000..0dac2dc --- /dev/null +++ b/src/updatedAtHelper.ts @@ -0,0 +1,74 @@ +/** + * Global value that stores for each slice the last time + * it was updated and dumped locally + * @private + */ +let cache: Record = {}; + +/** + * Global class that handles the last time each slice was + * updated and saved locally. + * @internal + */ +export default class UpdatedAtHelper { + constructor() { + cache = {}; + } + + protected static getCacheName(name: string) { + return `persisted-storage-${name}-update-at`; + } + + /** + * Returns the last time the slice was saved locally + * It returns the valued cached if set + * + * @params The name of the slice + * @returns The last time the slice was saved locally + * + * @public + */ + static async getStoredUpdateAtOf(name: string): Promise { + // If there's already a cached version of the updateAt return it + if (cache[name]?.storedUpdatedAt !== undefined) return cache[name].storedUpdatedAt; + + // Othwerwise return a defualt value + return 0; + } + + /** + * Returns true if the slice should be saved locally. + * + * @params The name of the slice + * @returns If the slice should be saved locally + * + * @public + */ + static async shouldSave(name: string): Promise { + const stored = await this.getStoredUpdateAtOf(name); + return (cache[name]?.localUpdatedAt ?? 0) > stored; + } + + /** + * Sets the last time the slice was saved locally. + * + * @params The name of the slice + * + * @public + */ + static onSave(name: string) { + const updatedAt = new Date().getTime(); + cache[name] = { localUpdatedAt: cache[name]?.localUpdatedAt ?? updatedAt, storedUpdatedAt: updatedAt }; + } + + /** + * Sets the last time the slice was updated. + * + * @param storageHandler - The selected storage handler. + * + * @public + */ + static onStateChange(name: string) { + cache[name] = { ...cache[name], localUpdatedAt: new Date().getTime() }; + } +} diff --git a/test/blah.test.ts b/test/blah.test.ts deleted file mode 100644 index 33c7ef5..0000000 --- a/test/blah.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { sum } from '../src'; - -describe('blah', () => { - it('works', () => { - expect(sum(1, 1)).toEqual(2); - }); -}); diff --git a/test/mocks.ts b/test/mocks.ts new file mode 100644 index 0000000..08c3c5a --- /dev/null +++ b/test/mocks.ts @@ -0,0 +1,37 @@ +import { PayloadAction } from "@reduxjs/toolkit"; +import { StorageHandler } from "../src/types"; +import { createPersistedSlice } from "../src/slice"; + +let data: Record = {}; +export const mockStorageHandler: StorageHandler = { + getItem: (key: string) => { + if (key in data) return data[key]; + throw new Error(); + }, + setItem: (key: string, value: string) => { + data[key] = value; + }, + removeItem: (key: string) => { + delete data[key]; + }, +}; + +export const sliceInitialState = { counter: 0 }; +export const mockPersistedSlice = createPersistedSlice({ + name: 'test-counter', + initialState: sliceInitialState, + reducers: { + increment: (state) => { + state.counter++; + }, + decrement: (state) => { + state.counter--; + }, + setCounter: (state, action: PayloadAction) => { + state.counter = action.payload; + } + }, + selectors: { + getCounter: (state) => state.counter, + } +}); diff --git a/test/settings.test.ts b/test/settings.test.ts new file mode 100644 index 0000000..e766558 --- /dev/null +++ b/test/settings.test.ts @@ -0,0 +1,24 @@ +import { configurePersistedStore } from "../src"; +import { TestSettings as Settings } from '../src/settings'; +import { mockPersistedSlice, mockStorageHandler } from "./mocks"; + +describe('Global settings', () => { + beforeEach(() => { + Settings._clearSettings(); + }); + it('should allow to get and set static variables', () => { + Settings.storageHandler = mockStorageHandler; + expect(Settings.storageHandler).toEqual(mockStorageHandler); + }); + + it('should be initiated when configuring a store', () => { + configurePersistedStore({ + reducer: ({ [mockPersistedSlice.name]: mockPersistedSlice.reducer }), + }, mockStorageHandler); + expect(Settings.storageHandler).toEqual(mockStorageHandler); + }); + + it('should throw an exception when requesting variables not set', () => { + expect(() => Settings.storageHandler).toThrow(TypeError); + }); +}); \ No newline at end of file diff --git a/test/store.test.ts b/test/store.test.ts new file mode 100644 index 0000000..dd5e614 --- /dev/null +++ b/test/store.test.ts @@ -0,0 +1,22 @@ +import { Store } from "@reduxjs/toolkit"; +import { configurePersistedStore } from "../src"; +import { mockPersistedSlice, mockStorageHandler, sliceInitialState } from "./mocks"; +import Settings from "../src/settings"; + +describe('Persisted Store', () => { + let store: Store; + + beforeEach(() => { + store = configurePersistedStore({ + reducer: ({ [mockPersistedSlice.name]: mockPersistedSlice.reducer }), + }, mockStorageHandler); + }) + + it('should set the storage handler', () => { + expect(Settings.storageHandler).toBe(mockStorageHandler); + }) + + it('should set the initial state of the persisted slices', () => { + expect(store.getState()).toEqual({ [mockPersistedSlice.name]: sliceInitialState }) + }) +}); diff --git a/test/updatedAtHelper.test.ts b/test/updatedAtHelper.test.ts new file mode 100644 index 0000000..0ee47ba --- /dev/null +++ b/test/updatedAtHelper.test.ts @@ -0,0 +1,20 @@ +import Settings from '../src/settings'; +import UpdatedAtHelper from '../src/updatedAtHelper'; +import { mockStorageHandler } from "./mocks"; + +describe('Updated At helper', () => { + beforeAll(() => { + Settings.storageHandler = mockStorageHandler; + }); + + it('should return 0 when a slice has never been updated', async () => { + expect(await UpdatedAtHelper.getStoredUpdateAtOf('mocked')).toBe(0); + }); + + it('should allow to save when the slice is updated but not saved', async () => { + UpdatedAtHelper.onStateChange('mocked'); + expect(await UpdatedAtHelper.shouldSave('mocked')).toBeTruthy(); + UpdatedAtHelper.onSave('mocked'); + expect(await UpdatedAtHelper.shouldSave('mocked')).toBeFalsy(); + }); +}); \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 5323b18..3b9589b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1108,6 +1108,62 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz#9c907b21e30a52db959ba4f80bb01a0cc403d5cc" integrity sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ== +"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.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.10.0", "@eslint-community/regexpp@^4.6.1": + version "4.10.0" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" + integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== + +"@eslint/eslintrc@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.0.2.tgz#36180f8e85bf34d2fe3ccc2261e8e204a411ab4e" + integrity sha512-wV19ZEGEMAC1eHgrS7UQPqsdEiCIbTKTasEfcXAigzoXICcqZSjBZEHlZwNVvKg6UBCjSlos84XiLqsRJnIcIg== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^10.0.1" + globals "^14.0.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + +"@eslint/js@9.1.1", "@eslint/js@^9.1.1": + version "9.1.1" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.1.1.tgz#eb0f82461d12779bbafc1b5045cde3143d350a8a" + integrity sha512-5WoDz3Y19Bg2BnErkZTp0en+c/i9PvgFS7MBe1+m60HjFr0hrphlAGp4yzI7pxpt4xShln4ZyYp4neJm8hmOkQ== + +"@humanwhocodes/config-array@^0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748" + integrity sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw== + dependencies: + "@humanwhocodes/object-schema" "^2.0.3" + debug "^4.3.1" + minimatch "^3.0.5" + +"@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@^2.0.3": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" + integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== + +"@humanwhocodes/retry@^0.2.3": + version "0.2.3" + resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.2.3.tgz#c9aa036d1afa643f1250e83150f39efb3a15a631" + integrity sha512-X38nUbachlb01YMlvPFojKoiXq+LzZvuSce70KPMPdeM1Rj03k4dR7lDslhbqXn3Ang4EU3+EAmwEAsbrjHW3g== + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -1324,6 +1380,21 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@microsoft/tsdoc-config@0.16.2": + version "0.16.2" + resolved "https://registry.yarnpkg.com/@microsoft/tsdoc-config/-/tsdoc-config-0.16.2.tgz#b786bb4ead00d54f53839a458ce626c8548d3adf" + integrity sha512-OGiIzzoBLgWWR0UdRJX98oYO+XKGf7tiK4Zk6tQ/E4IJqGCe7dvkTvgDZV5cFJUzLGDOjeAXrnZoA6QkVySuxw== + dependencies: + "@microsoft/tsdoc" "0.14.2" + ajv "~6.12.6" + jju "~1.4.0" + resolve "~1.19.0" + +"@microsoft/tsdoc@0.14.2": + version "0.14.2" + resolved "https://registry.yarnpkg.com/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz#c3ec604a0b54b9a9b87e9735dfc59e1a5da6a5fb" + integrity sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug== + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -1337,7 +1408,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== @@ -1529,7 +1600,7 @@ jest-diff "^25.2.1" pretty-format "^25.2.1" -"@types/json-schema@^7.0.3": +"@types/json-schema@^7.0.15", "@types/json-schema@^7.0.3": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== @@ -1568,6 +1639,11 @@ dependencies: "@types/node" "*" +"@types/semver@^7.5.8": + version "7.5.8" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" + integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ== + "@types/stack-utils@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" @@ -1585,6 +1661,23 @@ dependencies: "@types/yargs-parser" "*" +"@typescript-eslint/eslint-plugin@7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.8.0.tgz#c78e309fe967cb4de05b85cdc876fb95f8e01b6f" + integrity sha512-gFTT+ezJmkwutUPmB0skOj3GZJtlEGnlssems4AjkVweUPGj7jRwwqg0Hhg7++kPGJqKtTYx+R05Ftww372aIg== + dependencies: + "@eslint-community/regexpp" "^4.10.0" + "@typescript-eslint/scope-manager" "7.8.0" + "@typescript-eslint/type-utils" "7.8.0" + "@typescript-eslint/utils" "7.8.0" + "@typescript-eslint/visitor-keys" "7.8.0" + debug "^4.3.4" + graphemer "^1.4.0" + ignore "^5.3.1" + natural-compare "^1.4.0" + semver "^7.6.0" + ts-api-utils "^1.3.0" + "@typescript-eslint/eslint-plugin@^2.12.0": version "2.34.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.34.0.tgz#6f8ce8a46c7dea4a6f1d171d2bb8fbae6dac2be9" @@ -1605,6 +1698,17 @@ eslint-scope "^5.0.0" eslint-utils "^2.0.0" +"@typescript-eslint/parser@7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.8.0.tgz#1e1db30c8ab832caffee5f37e677dbcb9357ddc8" + integrity sha512-KgKQly1pv0l4ltcftP59uQZCi4HUYswCLbTqVZEJu7uLX8CTLyswqMLqLN+2QFz4jCptqWVV4SB7vdxcH2+0kQ== + dependencies: + "@typescript-eslint/scope-manager" "7.8.0" + "@typescript-eslint/types" "7.8.0" + "@typescript-eslint/typescript-estree" "7.8.0" + "@typescript-eslint/visitor-keys" "7.8.0" + debug "^4.3.4" + "@typescript-eslint/parser@^2.12.0": version "2.34.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.34.0.tgz#50252630ca319685420e9a39ca05fe185a256bc8" @@ -1615,6 +1719,29 @@ "@typescript-eslint/typescript-estree" "2.34.0" eslint-visitor-keys "^1.1.0" +"@typescript-eslint/scope-manager@7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.8.0.tgz#bb19096d11ec6b87fb6640d921df19b813e02047" + integrity sha512-viEmZ1LmwsGcnr85gIq+FCYI7nO90DVbE37/ll51hjv9aG+YZMb4WDE2fyWpUR4O/UrhGRpYXK/XajcGTk2B8g== + dependencies: + "@typescript-eslint/types" "7.8.0" + "@typescript-eslint/visitor-keys" "7.8.0" + +"@typescript-eslint/type-utils@7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.8.0.tgz#9de166f182a6e4d1c5da76e94880e91831e3e26f" + integrity sha512-H70R3AefQDQpz9mGv13Uhi121FNMh+WEaRqcXTX09YEDky21km4dV1ZXJIp8QjXc4ZaVkXVdohvWDzbnbHDS+A== + dependencies: + "@typescript-eslint/typescript-estree" "7.8.0" + "@typescript-eslint/utils" "7.8.0" + debug "^4.3.4" + ts-api-utils "^1.3.0" + +"@typescript-eslint/types@7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.8.0.tgz#1fd2577b3ad883b769546e2d1ef379f929a7091d" + integrity sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw== + "@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" @@ -1628,6 +1755,41 @@ semver "^7.3.2" tsutils "^3.17.1" +"@typescript-eslint/typescript-estree@7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.8.0.tgz#b028a9226860b66e623c1ee55cc2464b95d2987c" + integrity sha512-5pfUCOwK5yjPaJQNy44prjCwtr981dO8Qo9J9PwYXZ0MosgAbfEMB008dJ5sNo3+/BN6ytBPuSvXUg9SAqB0dg== + dependencies: + "@typescript-eslint/types" "7.8.0" + "@typescript-eslint/visitor-keys" "7.8.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + minimatch "^9.0.4" + semver "^7.6.0" + ts-api-utils "^1.3.0" + +"@typescript-eslint/utils@7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.8.0.tgz#57a79f9c0c0740ead2f622e444cfaeeb9fd047cd" + integrity sha512-L0yFqOCflVqXxiZyXrDr80lnahQfSOfc9ELAAZ75sqicqp2i36kEZZGuUymHNFoYOqxRT05up760b4iGsl02nQ== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@types/json-schema" "^7.0.15" + "@types/semver" "^7.5.8" + "@typescript-eslint/scope-manager" "7.8.0" + "@typescript-eslint/types" "7.8.0" + "@typescript-eslint/typescript-estree" "7.8.0" + semver "^7.6.0" + +"@typescript-eslint/visitor-keys@7.8.0": + version "7.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.8.0.tgz#7285aab991da8bee411a42edbd5db760d22fdd91" + integrity sha512-q4/gibTNBQNA0lGyYQCmWRS5D15n8rXh4QjK3KV+MBPlTYHpfBUT3D3PaPR/HeNiI9W6R7FvlkcGhNyAoP+caA== + dependencies: + "@typescript-eslint/types" "7.8.0" + eslint-visitor-keys "^3.4.3" + abab@^2.0.0: version "2.0.6" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" @@ -1641,7 +1803,7 @@ acorn-globals@^4.3.2: acorn "^6.0.1" acorn-walk "^6.0.1" -acorn-jsx@^5.2.0: +acorn-jsx@^5.2.0, 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== @@ -1661,7 +1823,12 @@ acorn@^7.1.0, acorn@^7.1.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3: +acorn@^8.11.3: + version "8.11.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" + integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== + +ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3, ajv@^6.12.4, ajv@~6.12.6: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -1740,6 +1907,11 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" +argparse@^2.0.1: + version "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@^5.3.0: version "5.3.0" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" @@ -1787,6 +1959,11 @@ array-includes@^3.1.6, array-includes@^3.1.7: get-intrinsic "^1.2.4" 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-unique@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" @@ -2111,6 +2288,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + braces@^2.3.1: version "2.3.2" resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" @@ -2469,7 +2653,7 @@ cross-spawn@^6.0.0, cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^7.0.0: +cross-spawn@^7.0.0, cross-spawn@^7.0.2: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -2557,7 +2741,7 @@ debug@^3.2.7: dependencies: ms "^2.1.1" -debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: +debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, 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== @@ -2574,7 +2758,7 @@ decode-uri-component@^0.2.0: resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== -deep-is@~0.1.3: +deep-is@^0.1.3, deep-is@~0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== @@ -2651,6 +2835,13 @@ diff-sequences@^25.2.6: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-25.2.6.tgz#5f467c00edd35352b7bca46d7927d60e687a76dd" integrity sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg== +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + doctrine@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" @@ -2882,6 +3073,11 @@ escape-string-regexp@^2.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + escodegen@^1.11.1: version "1.14.3" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" @@ -3012,6 +3208,14 @@ eslint-plugin-react@^7.14.3: semver "^6.3.1" string.prototype.matchall "^4.0.10" +eslint-plugin-tsdoc@^0.2.17: + version "0.2.17" + resolved "https://registry.yarnpkg.com/eslint-plugin-tsdoc/-/eslint-plugin-tsdoc-0.2.17.tgz#27789495bbd8778abbf92db1707fec2ed3dfe281" + integrity sha512-xRmVi7Zx44lOBuYqG8vzTXuL6IdGOeF9nHX17bjJ8+VE6fsxpdGem0/SBTmAwgYMKYB1WBkqRJVQ+n8GK041pA== + dependencies: + "@microsoft/tsdoc" "0.14.2" + "@microsoft/tsdoc-config" "0.16.2" + eslint-scope@^5.0.0: version "5.1.1" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" @@ -3020,6 +3224,14 @@ eslint-scope@^5.0.0: esrecurse "^4.3.0" estraverse "^4.1.1" +eslint-scope@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.0.1.tgz#a9601e4b81a0b9171657c343fb13111688963cfc" + integrity sha512-pL8XjgP4ZOmmwfFE8mEhSxA7ZY4C+LWyqjQ3o4yWkkmD0qcMT9kkW3zWHOczhWcjTSgqycYAgwSlXvZltv65og== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + eslint-utils@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f" @@ -3039,6 +3251,16 @@ eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.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== +eslint-visitor-keys@^3.3.0, 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-visitor-keys@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz#e3adc021aa038a2a8e0b2f8b0ce8f66b9483b1fb" + integrity sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw== + eslint@^6.1.0: version "6.8.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.8.0.tgz#62262d6729739f9275723824302fb227c8c93ffb" @@ -3082,6 +3304,55 @@ eslint@^6.1.0: text-table "^0.2.0" v8-compile-cache "^2.0.3" +eslint@^9.1.1: + version "9.1.1" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.1.1.tgz#39ec657ccd12813cb4a1dab2f9229dcc6e468271" + integrity sha512-b4cRQ0BeZcSEzPpY2PjFY70VbO32K7BStTGtBsnIGdTSEEQzBi8hPBcGQmTG2zUvFr9uLe0TK42bw8YszuHEqg== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.6.1" + "@eslint/eslintrc" "^3.0.2" + "@eslint/js" "9.1.1" + "@humanwhocodes/config-array" "^0.13.0" + "@humanwhocodes/module-importer" "^1.0.1" + "@humanwhocodes/retry" "^0.2.3" + "@nodelib/fs.walk" "^1.2.8" + ajv "^6.12.4" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + escape-string-regexp "^4.0.0" + eslint-scope "^8.0.1" + eslint-visitor-keys "^4.0.0" + espree "^10.0.1" + esquery "^1.4.2" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^8.0.0" + find-up "^5.0.0" + glob-parent "^6.0.2" + ignore "^5.2.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + is-path-inside "^3.0.3" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.3" + strip-ansi "^6.0.1" + text-table "^0.2.0" + +espree@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-10.0.1.tgz#600e60404157412751ba4a6f3a2ee1a42433139f" + integrity sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww== + dependencies: + acorn "^8.11.3" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^4.0.0" + espree@^6.1.2: version "6.2.1" resolved "https://registry.yarnpkg.com/espree/-/espree-6.2.1.tgz#77fc72e1fd744a2052c20f38a5b575832e82734a" @@ -3096,7 +3367,7 @@ esprima@^4.0.0, esprima@^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.0.1: +esquery@^1.0.1, 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== @@ -3267,7 +3538,7 @@ extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.1.tgz#8d172c064867f235c0c84a596806d279bf4bcc07" integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== -fast-deep-equal@^3.1.1: +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: version "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== @@ -3277,7 +3548,7 @@ fast-diff@^1.1.2: resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0" integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== -fast-glob@^3.3.2: +fast-glob@^3.2.9, fast-glob@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== @@ -3293,7 +3564,7 @@ fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== -fast-levenshtein@~2.0.6: +fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== @@ -3326,6 +3597,13 @@ file-entry-cache@^5.0.1: dependencies: flat-cache "^2.0.1" +file-entry-cache@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f" + integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ== + dependencies: + flat-cache "^4.0.0" + fill-range@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" @@ -3360,6 +3638,14 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + flat-cache@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" @@ -3369,11 +3655,24 @@ flat-cache@^2.0.1: rimraf "2.6.3" write "1.0.3" +flat-cache@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c" + integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw== + dependencies: + flatted "^3.2.9" + keyv "^4.5.4" + flatted@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138" integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA== +flatted@^3.2.9: + version "3.3.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" + integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== + for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -3534,6 +3833,13 @@ glob-parent@^5.0.0, 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@^7.0.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" @@ -3558,6 +3864,16 @@ globals@^12.1.0: dependencies: type-fest "^0.8.1" +globals@^14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e" + integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== + +globals@^15.1.0: + version "15.1.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-15.1.0.tgz#4e03d200c8362201636b8cdfaa316d6cef67ff1e" + integrity sha512-926gJqg+4mkxwYKiFvoomM4J0kWESfk3qfTvRL2/oc/tK/eTDBbrfcKnSa2KtfdxB5onoL7D3A3qIHQFpd4+UA== + globalthis@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" @@ -3571,6 +3887,18 @@ globalyzer@0.1.0: resolved "https://registry.yarnpkg.com/globalyzer/-/globalyzer-0.1.0.tgz#cb76da79555669a1519d5a8edf093afaa0bf1465" integrity sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q== +globby@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + globby@^14.0.1: version "14.0.1" resolved "https://registry.yarnpkg.com/globby/-/globby-14.0.1.tgz#a1b44841aa7f4c6d8af2bc39951109d77301959b" @@ -3600,6 +3928,11 @@ graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4: 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== + growly@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" @@ -3748,7 +4081,7 @@ ignore@^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.2.4: +ignore@^5.2.0, ignore@^5.2.4, ignore@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== @@ -3758,7 +4091,7 @@ immer@^10.0.3: resolved "https://registry.yarnpkg.com/immer/-/immer-10.1.1.tgz#206f344ea372d8ea176891545ee53ccc062db7bc" integrity sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw== -import-fresh@^3.0.0, import-fresh@^3.1.0: +import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== @@ -3896,7 +4229,7 @@ is-ci@^2.0.0: dependencies: ci-info "^2.0.0" -is-core-module@^2.13.0, is-core-module@^2.13.1: +is-core-module@^2.1.0, is-core-module@^2.13.0, is-core-module@^2.13.1: version "2.13.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== @@ -4037,6 +4370,11 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +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== + is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" @@ -4624,6 +4962,11 @@ jiti@^1.21.0: resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d" integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q== +jju@~1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/jju/-/jju-1.4.0.tgz#a3abe2718af241a2b2904f84a625970f389ae32a" + integrity sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA== + jpjs@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/jpjs/-/jpjs-1.2.1.tgz#f343833de8838a5beba1f42d5a219be0114c44b7" @@ -4642,6 +4985,13 @@ js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" @@ -4689,6 +5039,11 @@ jsesc@~0.5.0: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + json-parse-even-better-errors@^2.3.0: version "2.3.1" resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" @@ -4762,6 +5117,13 @@ jsprim@^1.2.2: object.assign "^4.1.4" object.values "^1.1.6" +keyv@^4.5.4: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" @@ -4811,6 +5173,14 @@ levn@^0.3.0, levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + lilconfig@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.1.tgz#9d8a246fa753106cfc205fd2d77042faca56e5e3" @@ -4828,6 +5198,13 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + lodash.debounce@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" @@ -4954,7 +5331,7 @@ merge-stream@^2.0.0: resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== -merge2@^1.3.0: +merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== @@ -5008,13 +5385,20 @@ mimic-fn@^2.1.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== -minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: +minimatch@^3.0.4, 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== dependencies: brace-expansion "^1.1.7" +minimatch@^9.0.4: + version "9.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51" + integrity sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw== + dependencies: + brace-expansion "^2.0.1" + minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.6: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" @@ -5301,6 +5685,18 @@ optionator@^0.8.1, optionator@^0.8.3: type-check "~0.3.2" word-wrap "~1.2.3" +optionator@^0.9.3: + version "0.9.4" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" + integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== + dependencies: + 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.5" + ora@^4.0.3: version "4.1.1" resolved "https://registry.yarnpkg.com/ora/-/ora-4.1.1.tgz#566cc0348a15c36f5f0e979612842e02ba9dddbc" @@ -5342,6 +5738,13 @@ p-limit@^2.2.0: dependencies: p-try "^2.0.0" +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + p-locate@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" @@ -5349,6 +5752,13 @@ p-locate@^4.1.0: dependencies: p-limit "^2.2.0" +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + p-try@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" @@ -5466,6 +5876,11 @@ possible-typed-array-names@^1.0.0: resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -5478,6 +5893,11 @@ prettier-linter-helpers@^1.0.0: dependencies: fast-diff "^1.1.2" +prettier@3.2.5: + version "3.2.5" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.5.tgz#e52bc3090586e824964a8813b09aba6233b28368" + integrity sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A== + prettier@^1.19.1: version "1.19.1" resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" @@ -5826,6 +6246,14 @@ resolve@^2.0.0-next.5: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" +resolve@~1.19.0: + version "1.19.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.19.0.tgz#1af5bf630409734a067cae29318aac7fa29a267c" + integrity sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg== + dependencies: + is-core-module "^2.1.0" + path-parse "^1.0.6" + restore-cursor@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" @@ -6011,7 +6439,7 @@ semver@6.x, semver@^6.0.0, semver@^6.1.2, semver@^6.3.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.1.1, semver@^7.3.2, semver@^7.5.3: +semver@^7.1.1, semver@^7.3.2, semver@^7.5.3, semver@^7.6.0: version "7.6.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== @@ -6428,7 +6856,7 @@ strip-final-newline@^2.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== -strip-json-comments@^3.0.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== @@ -6604,6 +7032,11 @@ tr46@^1.0.1: dependencies: punycode "^2.1.0" +ts-api-utils@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" + integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ== + ts-jest@^25.3.1: version "25.5.1" resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-25.5.1.tgz#2913afd08f28385d54f2f4e828be4d261f4337c7" @@ -6726,6 +7159,13 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + type-check@~0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" @@ -6804,6 +7244,15 @@ typedarray-to-buffer@^3.1.5: dependencies: is-typedarray "^1.0.0" +typescript-eslint@^7.8.0: + version "7.8.0" + resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-7.8.0.tgz#d2a73d4caac35d4d9825bfdfac06a9bf2ba175e4" + integrity sha512-sheFG+/D8N/L7gC3WT0Q8sB97Nm573Yfr+vZFzl/4nBdYcmviBPtwGSX9TJ7wpVg28ocerKVOt+k2eGmHzcgVA== + dependencies: + "@typescript-eslint/eslint-plugin" "7.8.0" + "@typescript-eslint/parser" "7.8.0" + "@typescript-eslint/utils" "7.8.0" + typescript@^3.7.3: version "3.9.10" resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.10.tgz#70f3910ac7a51ed6bef79da7800690b19bf778b8" @@ -7071,7 +7520,7 @@ which@^2.0.1, which@^2.0.2: dependencies: isexe "^2.0.0" -word-wrap@~1.2.3: +word-wrap@^1.2.5, word-wrap@~1.2.3: version "1.2.5" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== @@ -7174,3 +7623,8 @@ yargs@^15.3.1: which-module "^2.0.0" y18n "^4.0.0" yargs-parser "^18.1.2" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==