Some maintenance and minor fixes to state containers based on experience while working with them in #53582 Patch unit tests to use current "terminology" (e.g. "transition" vs "mutation") Fix docs where "store" was used instead of "state container" Allow to create state container without transition. Fix freeze function to deeply freeze objects. Restrict State to BaseState with extends object. in set() function, make sure the flow goes through dispatch to make sure middleware see this update Improve type inference for useTransition() Improve type inference for createStateContainer(). Other issues noticed, but didn't fix in reasonable time: Can't use addMiddleware without explicit type casting #54438 Transitions and Selectors allow any state, not bind to container's state #54439
This commit is contained in:
parent
19ddf27275
commit
26b5cd89c2
|
@ -41,6 +41,7 @@ import {
|
||||||
PureTransition,
|
PureTransition,
|
||||||
syncStates,
|
syncStates,
|
||||||
getStateFromKbnUrl,
|
getStateFromKbnUrl,
|
||||||
|
BaseState,
|
||||||
} from '../../../src/plugins/kibana_utils/public';
|
} from '../../../src/plugins/kibana_utils/public';
|
||||||
import { useUrlTracker } from '../../../src/plugins/kibana_react/public';
|
import { useUrlTracker } from '../../../src/plugins/kibana_react/public';
|
||||||
import {
|
import {
|
||||||
|
@ -79,7 +80,7 @@ const TodoApp: React.FC<TodoAppProps> = ({ filter }) => {
|
||||||
const { setText } = GlobalStateHelpers.useTransitions();
|
const { setText } = GlobalStateHelpers.useTransitions();
|
||||||
const { text } = GlobalStateHelpers.useState();
|
const { text } = GlobalStateHelpers.useState();
|
||||||
const { edit: editTodo, delete: deleteTodo, add: addTodo } = useTransitions();
|
const { edit: editTodo, delete: deleteTodo, add: addTodo } = useTransitions();
|
||||||
const todos = useState();
|
const todos = useState().todos;
|
||||||
const filteredTodos = todos.filter(todo => {
|
const filteredTodos = todos.filter(todo => {
|
||||||
if (!filter) return true;
|
if (!filter) return true;
|
||||||
if (filter === 'completed') return todo.completed;
|
if (filter === 'completed') return todo.completed;
|
||||||
|
@ -306,7 +307,7 @@ export const TodoAppPage: React.FC<{
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function withDefaultState<State>(
|
function withDefaultState<State extends BaseState>(
|
||||||
stateContainer: BaseStateContainer<State>,
|
stateContainer: BaseStateContainer<State>,
|
||||||
// eslint-disable-next-line no-shadow
|
// eslint-disable-next-line no-shadow
|
||||||
defaultState: State
|
defaultState: State
|
||||||
|
@ -314,14 +315,10 @@ function withDefaultState<State>(
|
||||||
return {
|
return {
|
||||||
...stateContainer,
|
...stateContainer,
|
||||||
set: (state: State | null) => {
|
set: (state: State | null) => {
|
||||||
if (Array.isArray(defaultState)) {
|
stateContainer.set({
|
||||||
stateContainer.set(state || defaultState);
|
...defaultState,
|
||||||
} else {
|
...state,
|
||||||
stateContainer.set({
|
});
|
||||||
...defaultState,
|
|
||||||
...state,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -162,6 +162,7 @@
|
||||||
"custom-event-polyfill": "^0.3.0",
|
"custom-event-polyfill": "^0.3.0",
|
||||||
"d3": "3.5.17",
|
"d3": "3.5.17",
|
||||||
"d3-cloud": "1.2.5",
|
"d3-cloud": "1.2.5",
|
||||||
|
"deep-freeze-strict": "^1.1.1",
|
||||||
"deepmerge": "^4.2.2",
|
"deepmerge": "^4.2.2",
|
||||||
"del": "^5.1.0",
|
"del": "^5.1.0",
|
||||||
"elastic-apm-node": "^3.2.0",
|
"elastic-apm-node": "^3.2.0",
|
||||||
|
@ -308,6 +309,7 @@
|
||||||
"@types/classnames": "^2.2.9",
|
"@types/classnames": "^2.2.9",
|
||||||
"@types/d3": "^3.5.43",
|
"@types/d3": "^3.5.43",
|
||||||
"@types/dedent": "^0.7.0",
|
"@types/dedent": "^0.7.0",
|
||||||
|
"@types/deep-freeze-strict": "^1.1.0",
|
||||||
"@types/delete-empty": "^2.0.0",
|
"@types/delete-empty": "^2.0.0",
|
||||||
"@types/elasticsearch": "^5.0.33",
|
"@types/elasticsearch": "^5.0.33",
|
||||||
"@types/enzyme": "^3.9.0",
|
"@types/enzyme": "^3.9.0",
|
||||||
|
|
|
@ -340,6 +340,14 @@
|
||||||
'@types/dedent',
|
'@types/dedent',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
groupSlug: 'deep-freeze-strict',
|
||||||
|
groupName: 'deep-freeze-strict related packages',
|
||||||
|
packageNames: [
|
||||||
|
'deep-freeze-strict',
|
||||||
|
'@types/deep-freeze-strict',
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
groupSlug: 'delete-empty',
|
groupSlug: 'delete-empty',
|
||||||
groupName: 'delete-empty related packages',
|
groupName: 'delete-empty related packages',
|
||||||
|
|
|
@ -38,7 +38,7 @@ describe('demos', () => {
|
||||||
describe('state sync', () => {
|
describe('state sync', () => {
|
||||||
test('url sync demo works', async () => {
|
test('url sync demo works', async () => {
|
||||||
expect(await urlSyncResult).toMatchInlineSnapshot(
|
expect(await urlSyncResult).toMatchInlineSnapshot(
|
||||||
`"http://localhost/#?_s=!((completed:!f,id:0,text:'Learning%20state%20containers'),(completed:!f,id:2,text:test))"`
|
`"http://localhost/#?_s=(todos:!((completed:!f,id:0,text:'Learning%20state%20containers'),(completed:!f,id:2,text:test)))"`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -19,14 +19,24 @@
|
||||||
|
|
||||||
import { createStateContainer } from '../../public/state_containers';
|
import { createStateContainer } from '../../public/state_containers';
|
||||||
|
|
||||||
const container = createStateContainer(0, {
|
interface State {
|
||||||
increment: (cnt: number) => (by: number) => cnt + by,
|
count: number;
|
||||||
double: (cnt: number) => () => cnt * 2,
|
}
|
||||||
});
|
|
||||||
|
const container = createStateContainer(
|
||||||
|
{ count: 0 },
|
||||||
|
{
|
||||||
|
increment: (state: State) => (by: number) => ({ count: state.count + by }),
|
||||||
|
double: (state: State) => () => ({ count: state.count * 2 }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
count: (state: State) => () => state.count,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
container.transitions.increment(5);
|
container.transitions.increment(5);
|
||||||
container.transitions.double();
|
container.transitions.double();
|
||||||
|
|
||||||
console.log(container.get()); // eslint-disable-line
|
console.log(container.selectors.count()); // eslint-disable-line
|
||||||
|
|
||||||
export const result = container.get();
|
export const result = container.selectors.count();
|
||||||
|
|
|
@ -25,15 +25,19 @@ export interface TodoItem {
|
||||||
id: number;
|
id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TodoState = TodoItem[];
|
export interface TodoState {
|
||||||
|
todos: TodoItem[];
|
||||||
|
}
|
||||||
|
|
||||||
export const defaultState: TodoState = [
|
export const defaultState: TodoState = {
|
||||||
{
|
todos: [
|
||||||
id: 0,
|
{
|
||||||
text: 'Learning state containers',
|
id: 0,
|
||||||
completed: false,
|
text: 'Learning state containers',
|
||||||
},
|
completed: false,
|
||||||
];
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
export interface TodoActions {
|
export interface TodoActions {
|
||||||
add: PureTransition<TodoState, [TodoItem]>;
|
add: PureTransition<TodoState, [TodoItem]>;
|
||||||
|
@ -44,17 +48,34 @@ export interface TodoActions {
|
||||||
clearCompleted: PureTransition<TodoState, []>;
|
clearCompleted: PureTransition<TodoState, []>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TodosSelectors {
|
||||||
|
todos: (state: TodoState) => () => TodoItem[];
|
||||||
|
todo: (state: TodoState) => (id: number) => TodoItem | null;
|
||||||
|
}
|
||||||
|
|
||||||
export const pureTransitions: TodoActions = {
|
export const pureTransitions: TodoActions = {
|
||||||
add: state => todo => [...state, todo],
|
add: state => todo => ({ todos: [...state.todos, todo] }),
|
||||||
edit: state => todo => state.map(item => (item.id === todo.id ? { ...item, ...todo } : item)),
|
edit: state => todo => ({
|
||||||
delete: state => id => state.filter(item => item.id !== id),
|
todos: state.todos.map(item => (item.id === todo.id ? { ...item, ...todo } : item)),
|
||||||
complete: state => id =>
|
}),
|
||||||
state.map(item => (item.id === id ? { ...item, completed: true } : item)),
|
delete: state => id => ({ todos: state.todos.filter(item => item.id !== id) }),
|
||||||
completeAll: state => () => state.map(item => ({ ...item, completed: true })),
|
complete: state => id => ({
|
||||||
clearCompleted: state => () => state.filter(({ completed }) => !completed),
|
todos: state.todos.map(item => (item.id === id ? { ...item, completed: true } : item)),
|
||||||
|
}),
|
||||||
|
completeAll: state => () => ({ todos: state.todos.map(item => ({ ...item, completed: true })) }),
|
||||||
|
clearCompleted: state => () => ({ todos: state.todos.filter(({ completed }) => !completed) }),
|
||||||
};
|
};
|
||||||
|
|
||||||
const container = createStateContainer<TodoState, TodoActions>(defaultState, pureTransitions);
|
export const pureSelectors: TodosSelectors = {
|
||||||
|
todos: state => () => state.todos,
|
||||||
|
todo: state => id => state.todos.find(todo => todo.id === id) ?? null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const container = createStateContainer<TodoState, TodoActions, TodosSelectors>(
|
||||||
|
defaultState,
|
||||||
|
pureTransitions,
|
||||||
|
pureSelectors
|
||||||
|
);
|
||||||
|
|
||||||
container.transitions.add({
|
container.transitions.add({
|
||||||
id: 1,
|
id: 1,
|
||||||
|
@ -64,6 +85,6 @@ container.transitions.add({
|
||||||
container.transitions.complete(0);
|
container.transitions.complete(0);
|
||||||
container.transitions.complete(1);
|
container.transitions.complete(1);
|
||||||
|
|
||||||
console.log(container.get()); // eslint-disable-line
|
console.log(container.selectors.todos()); // eslint-disable-line
|
||||||
|
|
||||||
export const result = container.get();
|
export const result = container.selectors.todos();
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defaultState, pureTransitions, TodoActions, TodoState } from '../state_containers/todomvc';
|
import { defaultState, pureTransitions, TodoActions, TodoState } from '../state_containers/todomvc';
|
||||||
import { BaseStateContainer, createStateContainer } from '../../public/state_containers';
|
import { BaseState, BaseStateContainer, createStateContainer } from '../../public/state_containers';
|
||||||
import {
|
import {
|
||||||
createKbnUrlStateStorage,
|
createKbnUrlStateStorage,
|
||||||
syncState,
|
syncState,
|
||||||
|
@ -55,7 +55,7 @@ export const result = Promise.resolve()
|
||||||
return window.location.href;
|
return window.location.href;
|
||||||
});
|
});
|
||||||
|
|
||||||
function withDefaultState<State>(
|
function withDefaultState<State extends BaseState>(
|
||||||
// eslint-disable-next-line no-shadow
|
// eslint-disable-next-line no-shadow
|
||||||
stateContainer: BaseStateContainer<State>,
|
stateContainer: BaseStateContainer<State>,
|
||||||
// eslint-disable-next-line no-shadow
|
// eslint-disable-next-line no-shadow
|
||||||
|
|
|
@ -18,14 +18,21 @@ your services or apps.
|
||||||
```ts
|
```ts
|
||||||
import { createStateContainer } from 'src/plugins/kibana_utils';
|
import { createStateContainer } from 'src/plugins/kibana_utils';
|
||||||
|
|
||||||
const container = createStateContainer(0, {
|
const container = createStateContainer(
|
||||||
increment: (cnt: number) => (by: number) => cnt + by,
|
{ count: 0 },
|
||||||
double: (cnt: number) => () => cnt * 2,
|
{
|
||||||
});
|
increment: (state: {count: number}) => (by: number) => ({ count: state.count + by }),
|
||||||
|
double: (state: {count: number}) => () => ({ count: state.count * 2 }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
count: (state: {count: number}) => () => state.count,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
container.transitions.increment(5);
|
container.transitions.increment(5);
|
||||||
container.transitions.double();
|
container.transitions.double();
|
||||||
console.log(container.get()); // 10
|
|
||||||
|
console.log(container.selectors.count()); // 10
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,7 @@ Create your a state container.
|
||||||
```ts
|
```ts
|
||||||
import { createStateContainer } from 'src/plugins/kibana_utils';
|
import { createStateContainer } from 'src/plugins/kibana_utils';
|
||||||
|
|
||||||
const container = createStateContainer<MyState>(defaultState, {});
|
const container = createStateContainer<MyState>(defaultState);
|
||||||
|
|
||||||
console.log(container.get());
|
console.log(container.get());
|
||||||
```
|
```
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
# Consuming state in non-React setting
|
# Consuming state in non-React setting
|
||||||
|
|
||||||
To read the current `state` of the store use `.get()` method.
|
To read the current `state` of the store use `.get()` method or `getState()` alias method.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
store.get();
|
stateContainer.get();
|
||||||
```
|
```
|
||||||
|
|
||||||
To listen for latest state changes use `.state$` observable.
|
To listen for latest state changes use `.state$` observable.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
store.state$.subscribe(state => { ... });
|
stateContainer.state$.subscribe(state => { ... });
|
||||||
```
|
```
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
```ts
|
```ts
|
||||||
import { createStateContainer, createStateContainerReactHelpers } from 'src/plugins/kibana_utils';
|
import { createStateContainer, createStateContainerReactHelpers } from 'src/plugins/kibana_utils';
|
||||||
|
|
||||||
const container = createStateContainer({}, {});
|
const container = createStateContainer({});
|
||||||
export const {
|
export const {
|
||||||
Provider,
|
Provider,
|
||||||
Consumer,
|
Consumer,
|
||||||
|
|
|
@ -19,18 +19,9 @@
|
||||||
|
|
||||||
import { createStateContainer } from './create_state_container';
|
import { createStateContainer } from './create_state_container';
|
||||||
|
|
||||||
const create = <S, T extends object>(state: S, transitions: T = {} as T) => {
|
test('can create state container', () => {
|
||||||
const pureTransitions = {
|
const stateContainer = createStateContainer({});
|
||||||
set: () => (newState: S) => newState,
|
expect(stateContainer).toMatchObject({
|
||||||
...transitions,
|
|
||||||
};
|
|
||||||
const store = createStateContainer<typeof state, typeof pureTransitions>(state, pureTransitions);
|
|
||||||
return { store, mutators: store.transitions };
|
|
||||||
};
|
|
||||||
|
|
||||||
test('can create store', () => {
|
|
||||||
const { store } = create({});
|
|
||||||
expect(store).toMatchObject({
|
|
||||||
getState: expect.any(Function),
|
getState: expect.any(Function),
|
||||||
state$: expect.any(Object),
|
state$: expect.any(Object),
|
||||||
transitions: expect.any(Object),
|
transitions: expect.any(Object),
|
||||||
|
@ -45,9 +36,9 @@ test('can set default state', () => {
|
||||||
const defaultState = {
|
const defaultState = {
|
||||||
foo: 'bar',
|
foo: 'bar',
|
||||||
};
|
};
|
||||||
const { store } = create(defaultState);
|
const stateContainer = createStateContainer(defaultState);
|
||||||
expect(store.get()).toEqual(defaultState);
|
expect(stateContainer.get()).toEqual(defaultState);
|
||||||
expect(store.getState()).toEqual(defaultState);
|
expect(stateContainer.getState()).toEqual(defaultState);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('can set state', () => {
|
test('can set state', () => {
|
||||||
|
@ -57,12 +48,12 @@ test('can set state', () => {
|
||||||
const newState = {
|
const newState = {
|
||||||
foo: 'baz',
|
foo: 'baz',
|
||||||
};
|
};
|
||||||
const { store, mutators } = create(defaultState);
|
const stateContainer = createStateContainer(defaultState);
|
||||||
|
|
||||||
mutators.set(newState);
|
stateContainer.set(newState);
|
||||||
|
|
||||||
expect(store.get()).toEqual(newState);
|
expect(stateContainer.get()).toEqual(newState);
|
||||||
expect(store.getState()).toEqual(newState);
|
expect(stateContainer.getState()).toEqual(newState);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('does not shallow merge states', () => {
|
test('does not shallow merge states', () => {
|
||||||
|
@ -72,22 +63,22 @@ test('does not shallow merge states', () => {
|
||||||
const newState = {
|
const newState = {
|
||||||
foo2: 'baz',
|
foo2: 'baz',
|
||||||
};
|
};
|
||||||
const { store, mutators } = create(defaultState);
|
const stateContainer = createStateContainer(defaultState);
|
||||||
|
|
||||||
mutators.set(newState as any);
|
stateContainer.set(newState as any);
|
||||||
|
|
||||||
expect(store.get()).toEqual(newState);
|
expect(stateContainer.get()).toEqual(newState);
|
||||||
expect(store.getState()).toEqual(newState);
|
expect(stateContainer.getState()).toEqual(newState);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('can subscribe and unsubscribe to state changes', () => {
|
test('can subscribe and unsubscribe to state changes', () => {
|
||||||
const { store, mutators } = create({});
|
const stateContainer = createStateContainer({});
|
||||||
const spy = jest.fn();
|
const spy = jest.fn();
|
||||||
const subscription = store.state$.subscribe(spy);
|
const subscription = stateContainer.state$.subscribe(spy);
|
||||||
mutators.set({ a: 1 });
|
stateContainer.set({ a: 1 });
|
||||||
mutators.set({ a: 2 });
|
stateContainer.set({ a: 2 });
|
||||||
subscription.unsubscribe();
|
subscription.unsubscribe();
|
||||||
mutators.set({ a: 3 });
|
stateContainer.set({ a: 3 });
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalledTimes(2);
|
expect(spy).toHaveBeenCalledTimes(2);
|
||||||
expect(spy.mock.calls[0][0]).toEqual({ a: 1 });
|
expect(spy.mock.calls[0][0]).toEqual({ a: 1 });
|
||||||
|
@ -95,16 +86,16 @@ test('can subscribe and unsubscribe to state changes', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('multiple subscribers can subscribe', () => {
|
test('multiple subscribers can subscribe', () => {
|
||||||
const { store, mutators } = create({});
|
const stateContainer = createStateContainer({});
|
||||||
const spy1 = jest.fn();
|
const spy1 = jest.fn();
|
||||||
const spy2 = jest.fn();
|
const spy2 = jest.fn();
|
||||||
const subscription1 = store.state$.subscribe(spy1);
|
const subscription1 = stateContainer.state$.subscribe(spy1);
|
||||||
const subscription2 = store.state$.subscribe(spy2);
|
const subscription2 = stateContainer.state$.subscribe(spy2);
|
||||||
mutators.set({ a: 1 });
|
stateContainer.set({ a: 1 });
|
||||||
subscription1.unsubscribe();
|
subscription1.unsubscribe();
|
||||||
mutators.set({ a: 2 });
|
stateContainer.set({ a: 2 });
|
||||||
subscription2.unsubscribe();
|
subscription2.unsubscribe();
|
||||||
mutators.set({ a: 3 });
|
stateContainer.set({ a: 3 });
|
||||||
|
|
||||||
expect(spy1).toHaveBeenCalledTimes(1);
|
expect(spy1).toHaveBeenCalledTimes(1);
|
||||||
expect(spy2).toHaveBeenCalledTimes(2);
|
expect(spy2).toHaveBeenCalledTimes(2);
|
||||||
|
@ -120,19 +111,19 @@ test('can create state container without transitions', () => {
|
||||||
expect(stateContainer.get()).toEqual(state);
|
expect(stateContainer.get()).toEqual(state);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('creates impure mutators from pure mutators', () => {
|
test('creates transitions', () => {
|
||||||
const { mutators } = create(
|
const stateContainer = createStateContainer(
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
setFoo: () => (bar: any) => ({ foo: bar }),
|
setFoo: () => (bar: any) => ({ foo: bar }),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(typeof mutators.setFoo).toBe('function');
|
expect(typeof stateContainer.transitions.setFoo).toBe('function');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('mutators can update state', () => {
|
test('transitions can update state', () => {
|
||||||
const { store, mutators } = create(
|
const stateContainer = createStateContainer(
|
||||||
{
|
{
|
||||||
value: 0,
|
value: 0,
|
||||||
foo: 'bar',
|
foo: 'bar',
|
||||||
|
@ -143,30 +134,30 @@ test('mutators can update state', () => {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(store.get()).toEqual({
|
expect(stateContainer.get()).toEqual({
|
||||||
value: 0,
|
value: 0,
|
||||||
foo: 'bar',
|
foo: 'bar',
|
||||||
});
|
});
|
||||||
|
|
||||||
mutators.add(11);
|
stateContainer.transitions.add(11);
|
||||||
mutators.setFoo('baz');
|
stateContainer.transitions.setFoo('baz');
|
||||||
|
|
||||||
expect(store.get()).toEqual({
|
expect(stateContainer.get()).toEqual({
|
||||||
value: 11,
|
value: 11,
|
||||||
foo: 'baz',
|
foo: 'baz',
|
||||||
});
|
});
|
||||||
|
|
||||||
mutators.add(-20);
|
stateContainer.transitions.add(-20);
|
||||||
mutators.setFoo('bazooka');
|
stateContainer.transitions.setFoo('bazooka');
|
||||||
|
|
||||||
expect(store.get()).toEqual({
|
expect(stateContainer.get()).toEqual({
|
||||||
value: -9,
|
value: -9,
|
||||||
foo: 'bazooka',
|
foo: 'bazooka',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('mutators methods are not bound', () => {
|
test('transitions methods are not bound', () => {
|
||||||
const { store, mutators } = create(
|
const stateContainer = createStateContainer(
|
||||||
{ value: -3 },
|
{ value: -3 },
|
||||||
{
|
{
|
||||||
add: (state: { value: number }) => (increment: number) => ({
|
add: (state: { value: number }) => (increment: number) => ({
|
||||||
|
@ -176,13 +167,13 @@ test('mutators methods are not bound', () => {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(store.get()).toEqual({ value: -3 });
|
expect(stateContainer.get()).toEqual({ value: -3 });
|
||||||
mutators.add(4);
|
stateContainer.transitions.add(4);
|
||||||
expect(store.get()).toEqual({ value: 1 });
|
expect(stateContainer.get()).toEqual({ value: 1 });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('created mutators are saved in store object', () => {
|
test('created transitions are saved in stateContainer object', () => {
|
||||||
const { store, mutators } = create(
|
const stateContainer = createStateContainer(
|
||||||
{ value: -3 },
|
{ value: -3 },
|
||||||
{
|
{
|
||||||
add: (state: { value: number }) => (increment: number) => ({
|
add: (state: { value: number }) => (increment: number) => ({
|
||||||
|
@ -192,55 +183,57 @@ test('created mutators are saved in store object', () => {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(typeof store.transitions.add).toBe('function');
|
expect(typeof stateContainer.transitions.add).toBe('function');
|
||||||
mutators.add(5);
|
stateContainer.transitions.add(5);
|
||||||
expect(store.get()).toEqual({ value: 2 });
|
expect(stateContainer.get()).toEqual({ value: 2 });
|
||||||
});
|
});
|
||||||
|
|
||||||
test('throws when state is modified inline - 1', () => {
|
test('throws when state is modified inline', () => {
|
||||||
const container = createStateContainer({ a: 'b' }, {});
|
const container = createStateContainer({ a: 'b', array: [{ a: 'b' }] });
|
||||||
|
|
||||||
let error: TypeError | null = null;
|
expect(() => {
|
||||||
try {
|
|
||||||
(container.get().a as any) = 'c';
|
(container.get().a as any) = 'c';
|
||||||
} catch (err) {
|
}).toThrowErrorMatchingInlineSnapshot(
|
||||||
error = err;
|
`"Cannot assign to read only property 'a' of object '#<Object>'"`
|
||||||
}
|
);
|
||||||
|
|
||||||
expect(error).toBeInstanceOf(TypeError);
|
expect(() => {
|
||||||
});
|
|
||||||
|
|
||||||
test('throws when state is modified inline - 2', () => {
|
|
||||||
const container = createStateContainer({ a: 'b' }, {});
|
|
||||||
|
|
||||||
let error: TypeError | null = null;
|
|
||||||
try {
|
|
||||||
(container.getState().a as any) = 'c';
|
(container.getState().a as any) = 'c';
|
||||||
} catch (err) {
|
}).toThrowErrorMatchingInlineSnapshot(
|
||||||
error = err;
|
`"Cannot assign to read only property 'a' of object '#<Object>'"`
|
||||||
}
|
);
|
||||||
|
|
||||||
expect(error).toBeInstanceOf(TypeError);
|
expect(() => {
|
||||||
|
(container.getState().array as any).push('c');
|
||||||
|
}).toThrowErrorMatchingInlineSnapshot(`"Cannot add property 1, object is not extensible"`);
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
(container.getState().array[0] as any).c = 'b';
|
||||||
|
}).toThrowErrorMatchingInlineSnapshot(`"Cannot add property c, object is not extensible"`);
|
||||||
|
|
||||||
|
expect(() => {
|
||||||
|
container.set(null as any);
|
||||||
|
expect(container.getState()).toBeNull();
|
||||||
|
}).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('throws when state is modified inline in subscription', done => {
|
test('throws when state is modified inline in subscription', () => {
|
||||||
const container = createStateContainer({ a: 'b' }, { set: () => (newState: any) => newState });
|
const container = createStateContainer({ a: 'b' }, { set: () => (newState: any) => newState });
|
||||||
|
|
||||||
container.subscribe(value => {
|
container.subscribe(value => {
|
||||||
let error: TypeError | null = null;
|
expect(() => {
|
||||||
try {
|
|
||||||
(value.a as any) = 'd';
|
(value.a as any) = 'd';
|
||||||
} catch (err) {
|
}).toThrowErrorMatchingInlineSnapshot(
|
||||||
error = err;
|
`"Cannot assign to read only property 'a' of object '#<Object>'"`
|
||||||
}
|
);
|
||||||
expect(error).toBeInstanceOf(TypeError);
|
|
||||||
done();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
container.transitions.set({ a: 'c' });
|
container.transitions.set({ a: 'c' });
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('selectors', () => {
|
describe('selectors', () => {
|
||||||
test('can specify no selectors, or can skip them', () => {
|
test('can specify no selectors, or can skip them', () => {
|
||||||
|
createStateContainer({});
|
||||||
createStateContainer({}, {});
|
createStateContainer({}, {});
|
||||||
createStateContainer({}, {}, {});
|
createStateContainer({}, {}, {});
|
||||||
});
|
});
|
||||||
|
|
|
@ -20,34 +20,52 @@
|
||||||
import { BehaviorSubject } from 'rxjs';
|
import { BehaviorSubject } from 'rxjs';
|
||||||
import { skip } from 'rxjs/operators';
|
import { skip } from 'rxjs/operators';
|
||||||
import { RecursiveReadonly } from '@kbn/utility-types';
|
import { RecursiveReadonly } from '@kbn/utility-types';
|
||||||
|
import deepFreeze from 'deep-freeze-strict';
|
||||||
import {
|
import {
|
||||||
PureTransitionsToTransitions,
|
PureTransitionsToTransitions,
|
||||||
PureTransition,
|
PureTransition,
|
||||||
ReduxLikeStateContainer,
|
ReduxLikeStateContainer,
|
||||||
PureSelectorsToSelectors,
|
PureSelectorsToSelectors,
|
||||||
|
BaseState,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
const $$observable = (typeof Symbol === 'function' && (Symbol as any).observable) || '@@observable';
|
const $$observable = (typeof Symbol === 'function' && (Symbol as any).observable) || '@@observable';
|
||||||
|
const $$setActionType = '@@SET';
|
||||||
|
|
||||||
const freeze: <T>(value: T) => RecursiveReadonly<T> =
|
const freeze: <T>(value: T) => RecursiveReadonly<T> =
|
||||||
process.env.NODE_ENV !== 'production'
|
process.env.NODE_ENV !== 'production'
|
||||||
? <T>(value: T): RecursiveReadonly<T> => {
|
? <T>(value: T): RecursiveReadonly<T> => {
|
||||||
if (!value) return value as RecursiveReadonly<T>;
|
const isFreezable = value !== null && typeof value === 'object';
|
||||||
if (value instanceof Array) return value as RecursiveReadonly<T>;
|
if (isFreezable) return deepFreeze(value) as RecursiveReadonly<T>;
|
||||||
if (typeof value === 'object') return Object.freeze({ ...value }) as RecursiveReadonly<T>;
|
return value as RecursiveReadonly<T>;
|
||||||
else return value as RecursiveReadonly<T>;
|
|
||||||
}
|
}
|
||||||
: <T>(value: T) => value as RecursiveReadonly<T>;
|
: <T>(value: T) => value as RecursiveReadonly<T>;
|
||||||
|
|
||||||
export const createStateContainer = <
|
export function createStateContainer<State extends BaseState>(
|
||||||
State,
|
defaultState: State
|
||||||
PureTransitions extends object = {},
|
): ReduxLikeStateContainer<State>;
|
||||||
PureSelectors extends object = {}
|
export function createStateContainer<State extends BaseState, PureTransitions extends object>(
|
||||||
|
defaultState: State,
|
||||||
|
pureTransitions: PureTransitions
|
||||||
|
): ReduxLikeStateContainer<State, PureTransitions>;
|
||||||
|
export function createStateContainer<
|
||||||
|
State extends BaseState,
|
||||||
|
PureTransitions extends object,
|
||||||
|
PureSelectors extends object
|
||||||
|
>(
|
||||||
|
defaultState: State,
|
||||||
|
pureTransitions: PureTransitions,
|
||||||
|
pureSelectors: PureSelectors
|
||||||
|
): ReduxLikeStateContainer<State, PureTransitions, PureSelectors>;
|
||||||
|
export function createStateContainer<
|
||||||
|
State extends BaseState,
|
||||||
|
PureTransitions extends object,
|
||||||
|
PureSelectors extends object
|
||||||
>(
|
>(
|
||||||
defaultState: State,
|
defaultState: State,
|
||||||
pureTransitions: PureTransitions = {} as PureTransitions,
|
pureTransitions: PureTransitions = {} as PureTransitions,
|
||||||
pureSelectors: PureSelectors = {} as PureSelectors
|
pureSelectors: PureSelectors = {} as PureSelectors
|
||||||
): ReduxLikeStateContainer<State, PureTransitions, PureSelectors> => {
|
): ReduxLikeStateContainer<State, PureTransitions, PureSelectors> {
|
||||||
const data$ = new BehaviorSubject<RecursiveReadonly<State>>(freeze(defaultState));
|
const data$ = new BehaviorSubject<RecursiveReadonly<State>>(freeze(defaultState));
|
||||||
const state$ = data$.pipe(skip(1));
|
const state$ = data$.pipe(skip(1));
|
||||||
const get = () => data$.getValue();
|
const get = () => data$.getValue();
|
||||||
|
@ -56,9 +74,13 @@ export const createStateContainer = <
|
||||||
state$,
|
state$,
|
||||||
getState: () => data$.getValue(),
|
getState: () => data$.getValue(),
|
||||||
set: (state: State) => {
|
set: (state: State) => {
|
||||||
data$.next(freeze(state));
|
container.dispatch({ type: $$setActionType, args: [state] });
|
||||||
},
|
},
|
||||||
reducer: (state, action) => {
|
reducer: (state, action) => {
|
||||||
|
if (action.type === $$setActionType) {
|
||||||
|
return freeze(action.args[0] as State);
|
||||||
|
}
|
||||||
|
|
||||||
const pureTransition = (pureTransitions as Record<string, PureTransition<State, any[]>>)[
|
const pureTransition = (pureTransitions as Record<string, PureTransition<State, any[]>>)[
|
||||||
action.type
|
action.type
|
||||||
];
|
];
|
||||||
|
@ -86,4 +108,4 @@ export const createStateContainer = <
|
||||||
[$$observable]: state$,
|
[$$observable]: state$,
|
||||||
};
|
};
|
||||||
return container;
|
return container;
|
||||||
};
|
}
|
||||||
|
|
|
@ -23,15 +23,6 @@ import { act, Simulate } from 'react-dom/test-utils';
|
||||||
import { createStateContainer } from './create_state_container';
|
import { createStateContainer } from './create_state_container';
|
||||||
import { createStateContainerReactHelpers } from './create_state_container_react_helpers';
|
import { createStateContainerReactHelpers } from './create_state_container_react_helpers';
|
||||||
|
|
||||||
const create = <S, T extends object>(state: S, transitions: T = {} as T) => {
|
|
||||||
const pureTransitions = {
|
|
||||||
set: () => (newState: S) => newState,
|
|
||||||
...transitions,
|
|
||||||
};
|
|
||||||
const store = createStateContainer<typeof state, typeof pureTransitions>(state, pureTransitions);
|
|
||||||
return { store, mutators: store.transitions };
|
|
||||||
};
|
|
||||||
|
|
||||||
let container: HTMLDivElement | null;
|
let container: HTMLDivElement | null;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -56,12 +47,12 @@ test('can create React context', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('<Provider> passes state to <Consumer>', () => {
|
test('<Provider> passes state to <Consumer>', () => {
|
||||||
const { store } = create({ hello: 'world' });
|
const stateContainer = createStateContainer({ hello: 'world' });
|
||||||
const { Provider, Consumer } = createStateContainerReactHelpers<typeof store>();
|
const { Provider, Consumer } = createStateContainerReactHelpers<typeof stateContainer>();
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<Provider value={store}>
|
<Provider value={stateContainer}>
|
||||||
<Consumer>{(s: typeof store) => s.get().hello}</Consumer>
|
<Consumer>{(s: typeof stateContainer) => s.get().hello}</Consumer>
|
||||||
</Provider>,
|
</Provider>,
|
||||||
container
|
container
|
||||||
);
|
);
|
||||||
|
@ -79,8 +70,8 @@ interface Props1 {
|
||||||
}
|
}
|
||||||
|
|
||||||
test('<Provider> passes state to connect()()', () => {
|
test('<Provider> passes state to connect()()', () => {
|
||||||
const { store } = create({ hello: 'Bob' });
|
const stateContainer = createStateContainer({ hello: 'Bob' });
|
||||||
const { Provider, connect } = createStateContainerReactHelpers();
|
const { Provider, connect } = createStateContainerReactHelpers<typeof stateContainer>();
|
||||||
|
|
||||||
const Demo: React.FC<Props1> = ({ message, stop }) => (
|
const Demo: React.FC<Props1> = ({ message, stop }) => (
|
||||||
<>
|
<>
|
||||||
|
@ -92,7 +83,7 @@ test('<Provider> passes state to connect()()', () => {
|
||||||
const DemoConnected = connect<Props1, 'message'>(mergeProps)(Demo);
|
const DemoConnected = connect<Props1, 'message'>(mergeProps)(Demo);
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<Provider value={store}>
|
<Provider value={stateContainer}>
|
||||||
<DemoConnected stop="?" />
|
<DemoConnected stop="?" />
|
||||||
</Provider>,
|
</Provider>,
|
||||||
container
|
container
|
||||||
|
@ -101,14 +92,14 @@ test('<Provider> passes state to connect()()', () => {
|
||||||
expect(container!.innerHTML).toBe('Bob?');
|
expect(container!.innerHTML).toBe('Bob?');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('context receives Redux store', () => {
|
test('context receives stateContainer', () => {
|
||||||
const { store } = create({ foo: 'bar' });
|
const stateContainer = createStateContainer({ foo: 'bar' });
|
||||||
const { Provider, context } = createStateContainerReactHelpers<typeof store>();
|
const { Provider, context } = createStateContainerReactHelpers<typeof stateContainer>();
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
/* eslint-disable no-shadow */
|
/* eslint-disable no-shadow */
|
||||||
<Provider value={store}>
|
<Provider value={stateContainer}>
|
||||||
<context.Consumer>{store => store.get().foo}</context.Consumer>
|
<context.Consumer>{stateContainer => stateContainer.get().foo}</context.Consumer>
|
||||||
</Provider>,
|
</Provider>,
|
||||||
/* eslint-enable no-shadow */
|
/* eslint-enable no-shadow */
|
||||||
container
|
container
|
||||||
|
@ -117,21 +108,21 @@ test('context receives Redux store', () => {
|
||||||
expect(container!.innerHTML).toBe('bar');
|
expect(container!.innerHTML).toBe('bar');
|
||||||
});
|
});
|
||||||
|
|
||||||
xtest('can use multiple stores in one React app', () => {});
|
test.todo('can use multiple stores in one React app');
|
||||||
|
|
||||||
describe('hooks', () => {
|
describe('hooks', () => {
|
||||||
describe('useStore', () => {
|
describe('useStore', () => {
|
||||||
test('can select store using useStore hook', () => {
|
test('can select store using useContainer hook', () => {
|
||||||
const { store } = create({ foo: 'bar' });
|
const stateContainer = createStateContainer({ foo: 'bar' });
|
||||||
const { Provider, useContainer } = createStateContainerReactHelpers<typeof store>();
|
const { Provider, useContainer } = createStateContainerReactHelpers<typeof stateContainer>();
|
||||||
const Demo: React.FC<{}> = () => {
|
const Demo: React.FC<{}> = () => {
|
||||||
// eslint-disable-next-line no-shadow
|
// eslint-disable-next-line no-shadow
|
||||||
const store = useContainer();
|
const stateContainer = useContainer();
|
||||||
return <>{store.get().foo}</>;
|
return <>{stateContainer.get().foo}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<Provider value={store}>
|
<Provider value={stateContainer}>
|
||||||
<Demo />
|
<Demo />
|
||||||
</Provider>,
|
</Provider>,
|
||||||
container
|
container
|
||||||
|
@ -143,15 +134,15 @@ describe('hooks', () => {
|
||||||
|
|
||||||
describe('useState', () => {
|
describe('useState', () => {
|
||||||
test('can select state using useState hook', () => {
|
test('can select state using useState hook', () => {
|
||||||
const { store } = create({ foo: 'qux' });
|
const stateContainer = createStateContainer({ foo: 'qux' });
|
||||||
const { Provider, useState } = createStateContainerReactHelpers<typeof store>();
|
const { Provider, useState } = createStateContainerReactHelpers<typeof stateContainer>();
|
||||||
const Demo: React.FC<{}> = () => {
|
const Demo: React.FC<{}> = () => {
|
||||||
const { foo } = useState();
|
const { foo } = useState();
|
||||||
return <>{foo}</>;
|
return <>{foo}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<Provider value={store}>
|
<Provider value={stateContainer}>
|
||||||
<Demo />
|
<Demo />
|
||||||
</Provider>,
|
</Provider>,
|
||||||
container
|
container
|
||||||
|
@ -161,23 +152,20 @@ describe('hooks', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('re-renders when state changes', () => {
|
test('re-renders when state changes', () => {
|
||||||
const {
|
const stateContainer = createStateContainer(
|
||||||
store,
|
|
||||||
mutators: { setFoo },
|
|
||||||
} = create(
|
|
||||||
{ foo: 'bar' },
|
{ foo: 'bar' },
|
||||||
{
|
{
|
||||||
setFoo: (state: { foo: string }) => (foo: string) => ({ ...state, foo }),
|
setFoo: (state: { foo: string }) => (foo: string) => ({ ...state, foo }),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const { Provider, useState } = createStateContainerReactHelpers<typeof store>();
|
const { Provider, useState } = createStateContainerReactHelpers<typeof stateContainer>();
|
||||||
const Demo: React.FC<{}> = () => {
|
const Demo: React.FC<{}> = () => {
|
||||||
const { foo } = useState();
|
const { foo } = useState();
|
||||||
return <>{foo}</>;
|
return <>{foo}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<Provider value={store}>
|
<Provider value={stateContainer}>
|
||||||
<Demo />
|
<Demo />
|
||||||
</Provider>,
|
</Provider>,
|
||||||
container
|
container
|
||||||
|
@ -185,7 +173,7 @@ describe('hooks', () => {
|
||||||
|
|
||||||
expect(container!.innerHTML).toBe('bar');
|
expect(container!.innerHTML).toBe('bar');
|
||||||
act(() => {
|
act(() => {
|
||||||
setFoo('baz');
|
stateContainer.transitions.setFoo('baz');
|
||||||
});
|
});
|
||||||
expect(container!.innerHTML).toBe('baz');
|
expect(container!.innerHTML).toBe('baz');
|
||||||
});
|
});
|
||||||
|
@ -193,7 +181,7 @@ describe('hooks', () => {
|
||||||
|
|
||||||
describe('useTransitions', () => {
|
describe('useTransitions', () => {
|
||||||
test('useTransitions hook returns mutations that can update state', () => {
|
test('useTransitions hook returns mutations that can update state', () => {
|
||||||
const { store } = create(
|
const stateContainer = createStateContainer(
|
||||||
{
|
{
|
||||||
cnt: 0,
|
cnt: 0,
|
||||||
},
|
},
|
||||||
|
@ -206,7 +194,7 @@ describe('hooks', () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const { Provider, useState, useTransitions } = createStateContainerReactHelpers<
|
const { Provider, useState, useTransitions } = createStateContainerReactHelpers<
|
||||||
typeof store
|
typeof stateContainer
|
||||||
>();
|
>();
|
||||||
const Demo: React.FC<{}> = () => {
|
const Demo: React.FC<{}> = () => {
|
||||||
const { cnt } = useState();
|
const { cnt } = useState();
|
||||||
|
@ -220,7 +208,7 @@ describe('hooks', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<Provider value={store}>
|
<Provider value={stateContainer}>
|
||||||
<Demo />
|
<Demo />
|
||||||
</Provider>,
|
</Provider>,
|
||||||
container
|
container
|
||||||
|
@ -240,7 +228,7 @@ describe('hooks', () => {
|
||||||
|
|
||||||
describe('useSelector', () => {
|
describe('useSelector', () => {
|
||||||
test('can select deeply nested value', () => {
|
test('can select deeply nested value', () => {
|
||||||
const { store } = create({
|
const stateContainer = createStateContainer({
|
||||||
foo: {
|
foo: {
|
||||||
bar: {
|
bar: {
|
||||||
baz: 'qux',
|
baz: 'qux',
|
||||||
|
@ -248,14 +236,14 @@ describe('hooks', () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const selector = (state: { foo: { bar: { baz: string } } }) => state.foo.bar.baz;
|
const selector = (state: { foo: { bar: { baz: string } } }) => state.foo.bar.baz;
|
||||||
const { Provider, useSelector } = createStateContainerReactHelpers<typeof store>();
|
const { Provider, useSelector } = createStateContainerReactHelpers<typeof stateContainer>();
|
||||||
const Demo: React.FC<{}> = () => {
|
const Demo: React.FC<{}> = () => {
|
||||||
const value = useSelector(selector);
|
const value = useSelector(selector);
|
||||||
return <>{value}</>;
|
return <>{value}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<Provider value={store}>
|
<Provider value={stateContainer}>
|
||||||
<Demo />
|
<Demo />
|
||||||
</Provider>,
|
</Provider>,
|
||||||
container
|
container
|
||||||
|
@ -265,7 +253,7 @@ describe('hooks', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('re-renders when state changes', () => {
|
test('re-renders when state changes', () => {
|
||||||
const { store, mutators } = create({
|
const stateContainer = createStateContainer({
|
||||||
foo: {
|
foo: {
|
||||||
bar: {
|
bar: {
|
||||||
baz: 'qux',
|
baz: 'qux',
|
||||||
|
@ -280,7 +268,7 @@ describe('hooks', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<Provider value={store}>
|
<Provider value={stateContainer}>
|
||||||
<Demo />
|
<Demo />
|
||||||
</Provider>,
|
</Provider>,
|
||||||
container
|
container
|
||||||
|
@ -288,7 +276,7 @@ describe('hooks', () => {
|
||||||
|
|
||||||
expect(container!.innerHTML).toBe('qux');
|
expect(container!.innerHTML).toBe('qux');
|
||||||
act(() => {
|
act(() => {
|
||||||
mutators.set({
|
stateContainer.set({
|
||||||
foo: {
|
foo: {
|
||||||
bar: {
|
bar: {
|
||||||
baz: 'quux',
|
baz: 'quux',
|
||||||
|
@ -300,9 +288,9 @@ describe('hooks', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("re-renders only when selector's result changes", async () => {
|
test("re-renders only when selector's result changes", async () => {
|
||||||
const { store, mutators } = create({ a: 'b', foo: 'bar' });
|
const stateContainer = createStateContainer({ a: 'b', foo: 'bar' });
|
||||||
const selector = (state: { foo: string }) => state.foo;
|
const selector = (state: { foo: string }) => state.foo;
|
||||||
const { Provider, useSelector } = createStateContainerReactHelpers<typeof store>();
|
const { Provider, useSelector } = createStateContainerReactHelpers<typeof stateContainer>();
|
||||||
|
|
||||||
let cnt = 0;
|
let cnt = 0;
|
||||||
const Demo: React.FC<{}> = () => {
|
const Demo: React.FC<{}> = () => {
|
||||||
|
@ -311,7 +299,7 @@ describe('hooks', () => {
|
||||||
return <>{value}</>;
|
return <>{value}</>;
|
||||||
};
|
};
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<Provider value={store}>
|
<Provider value={stateContainer}>
|
||||||
<Demo />
|
<Demo />
|
||||||
</Provider>,
|
</Provider>,
|
||||||
container
|
container
|
||||||
|
@ -321,14 +309,14 @@ describe('hooks', () => {
|
||||||
expect(cnt).toBe(1);
|
expect(cnt).toBe(1);
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
mutators.set({ a: 'c', foo: 'bar' });
|
stateContainer.set({ a: 'c', foo: 'bar' });
|
||||||
});
|
});
|
||||||
|
|
||||||
await new Promise(r => setTimeout(r, 1));
|
await new Promise(r => setTimeout(r, 1));
|
||||||
expect(cnt).toBe(1);
|
expect(cnt).toBe(1);
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
mutators.set({ a: 'd', foo: 'bar 2' });
|
stateContainer.set({ a: 'd', foo: 'bar 2' });
|
||||||
});
|
});
|
||||||
|
|
||||||
await new Promise(r => setTimeout(r, 1));
|
await new Promise(r => setTimeout(r, 1));
|
||||||
|
@ -336,9 +324,9 @@ describe('hooks', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('does not re-render on same shape object', async () => {
|
test('does not re-render on same shape object', async () => {
|
||||||
const { store, mutators } = create({ foo: { bar: 'baz' } });
|
const stateContainer = createStateContainer({ foo: { bar: 'baz' } });
|
||||||
const selector = (state: { foo: any }) => state.foo;
|
const selector = (state: { foo: any }) => state.foo;
|
||||||
const { Provider, useSelector } = createStateContainerReactHelpers<typeof store>();
|
const { Provider, useSelector } = createStateContainerReactHelpers<typeof stateContainer>();
|
||||||
|
|
||||||
let cnt = 0;
|
let cnt = 0;
|
||||||
const Demo: React.FC<{}> = () => {
|
const Demo: React.FC<{}> = () => {
|
||||||
|
@ -347,7 +335,7 @@ describe('hooks', () => {
|
||||||
return <>{JSON.stringify(value)}</>;
|
return <>{JSON.stringify(value)}</>;
|
||||||
};
|
};
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<Provider value={store}>
|
<Provider value={stateContainer}>
|
||||||
<Demo />
|
<Demo />
|
||||||
</Provider>,
|
</Provider>,
|
||||||
container
|
container
|
||||||
|
@ -357,14 +345,14 @@ describe('hooks', () => {
|
||||||
expect(cnt).toBe(1);
|
expect(cnt).toBe(1);
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
mutators.set({ foo: { bar: 'baz' } });
|
stateContainer.set({ foo: { bar: 'baz' } });
|
||||||
});
|
});
|
||||||
|
|
||||||
await new Promise(r => setTimeout(r, 1));
|
await new Promise(r => setTimeout(r, 1));
|
||||||
expect(cnt).toBe(1);
|
expect(cnt).toBe(1);
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
mutators.set({ foo: { bar: 'qux' } });
|
stateContainer.set({ foo: { bar: 'qux' } });
|
||||||
});
|
});
|
||||||
|
|
||||||
await new Promise(r => setTimeout(r, 1));
|
await new Promise(r => setTimeout(r, 1));
|
||||||
|
@ -372,7 +360,7 @@ describe('hooks', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('can set custom comparator function to prevent re-renders on deep equality', async () => {
|
test('can set custom comparator function to prevent re-renders on deep equality', async () => {
|
||||||
const { store, mutators } = create(
|
const stateContainer = createStateContainer(
|
||||||
{ foo: { bar: 'baz' } },
|
{ foo: { bar: 'baz' } },
|
||||||
{
|
{
|
||||||
set: () => (newState: { foo: { bar: string } }) => newState,
|
set: () => (newState: { foo: { bar: string } }) => newState,
|
||||||
|
@ -380,7 +368,7 @@ describe('hooks', () => {
|
||||||
);
|
);
|
||||||
const selector = (state: { foo: any }) => state.foo;
|
const selector = (state: { foo: any }) => state.foo;
|
||||||
const comparator = (prev: any, curr: any) => JSON.stringify(prev) === JSON.stringify(curr);
|
const comparator = (prev: any, curr: any) => JSON.stringify(prev) === JSON.stringify(curr);
|
||||||
const { Provider, useSelector } = createStateContainerReactHelpers<typeof store>();
|
const { Provider, useSelector } = createStateContainerReactHelpers<typeof stateContainer>();
|
||||||
|
|
||||||
let cnt = 0;
|
let cnt = 0;
|
||||||
const Demo: React.FC<{}> = () => {
|
const Demo: React.FC<{}> = () => {
|
||||||
|
@ -389,7 +377,7 @@ describe('hooks', () => {
|
||||||
return <>{JSON.stringify(value)}</>;
|
return <>{JSON.stringify(value)}</>;
|
||||||
};
|
};
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<Provider value={store}>
|
<Provider value={stateContainer}>
|
||||||
<Demo />
|
<Demo />
|
||||||
</Provider>,
|
</Provider>,
|
||||||
container
|
container
|
||||||
|
@ -399,13 +387,13 @@ describe('hooks', () => {
|
||||||
expect(cnt).toBe(1);
|
expect(cnt).toBe(1);
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
mutators.set({ foo: { bar: 'baz' } });
|
stateContainer.set({ foo: { bar: 'baz' } });
|
||||||
});
|
});
|
||||||
|
|
||||||
await new Promise(r => setTimeout(r, 1));
|
await new Promise(r => setTimeout(r, 1));
|
||||||
expect(cnt).toBe(1);
|
expect(cnt).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
xtest('unsubscribes when React un-mounts', () => {});
|
test.todo('unsubscribes when React un-mounts');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -35,7 +35,7 @@ export const createStateContainerReactHelpers = <Container extends StateContaine
|
||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
|
|
||||||
const useTransitions = (): Container['transitions'] => useContainer().transitions;
|
const useTransitions: () => Container['transitions'] = () => useContainer().transitions;
|
||||||
|
|
||||||
const useSelector = <Result>(
|
const useSelector = <Result>(
|
||||||
selector: (state: UnboxState<Container>) => Result,
|
selector: (state: UnboxState<Container>) => Result,
|
||||||
|
|
|
@ -20,12 +20,13 @@
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { Ensure, RecursiveReadonly } from '@kbn/utility-types';
|
import { Ensure, RecursiveReadonly } from '@kbn/utility-types';
|
||||||
|
|
||||||
|
export type BaseState = object;
|
||||||
export interface TransitionDescription<Type extends string = string, Args extends any[] = any[]> {
|
export interface TransitionDescription<Type extends string = string, Args extends any[] = any[]> {
|
||||||
type: Type;
|
type: Type;
|
||||||
args: Args;
|
args: Args;
|
||||||
}
|
}
|
||||||
export type Transition<State, Args extends any[]> = (...args: Args) => State;
|
export type Transition<State extends BaseState, Args extends any[]> = (...args: Args) => State;
|
||||||
export type PureTransition<State, Args extends any[]> = (
|
export type PureTransition<State extends BaseState, Args extends any[]> = (
|
||||||
state: RecursiveReadonly<State>
|
state: RecursiveReadonly<State>
|
||||||
) => Transition<State, Args>;
|
) => Transition<State, Args>;
|
||||||
export type EnsurePureTransition<T> = Ensure<T, PureTransition<any, any>>;
|
export type EnsurePureTransition<T> = Ensure<T, PureTransition<any, any>>;
|
||||||
|
@ -34,15 +35,15 @@ export type PureTransitionsToTransitions<T extends object> = {
|
||||||
[K in keyof T]: PureTransitionToTransition<EnsurePureTransition<T[K]>>;
|
[K in keyof T]: PureTransitionToTransition<EnsurePureTransition<T[K]>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface BaseStateContainer<State> {
|
export interface BaseStateContainer<State extends BaseState> {
|
||||||
get: () => RecursiveReadonly<State>;
|
get: () => RecursiveReadonly<State>;
|
||||||
set: (state: State) => void;
|
set: (state: State) => void;
|
||||||
state$: Observable<RecursiveReadonly<State>>;
|
state$: Observable<RecursiveReadonly<State>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StateContainer<
|
export interface StateContainer<
|
||||||
State,
|
State extends BaseState,
|
||||||
PureTransitions extends object = {},
|
PureTransitions extends object,
|
||||||
PureSelectors extends object = {}
|
PureSelectors extends object = {}
|
||||||
> extends BaseStateContainer<State> {
|
> extends BaseStateContainer<State> {
|
||||||
transitions: Readonly<PureTransitionsToTransitions<PureTransitions>>;
|
transitions: Readonly<PureTransitionsToTransitions<PureTransitions>>;
|
||||||
|
@ -50,7 +51,7 @@ export interface StateContainer<
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReduxLikeStateContainer<
|
export interface ReduxLikeStateContainer<
|
||||||
State,
|
State extends BaseState,
|
||||||
PureTransitions extends object = {},
|
PureTransitions extends object = {},
|
||||||
PureSelectors extends object = {}
|
PureSelectors extends object = {}
|
||||||
> extends StateContainer<State, PureTransitions, PureSelectors> {
|
> extends StateContainer<State, PureTransitions, PureSelectors> {
|
||||||
|
@ -63,14 +64,16 @@ export interface ReduxLikeStateContainer<
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Dispatch<T> = (action: T) => void;
|
export type Dispatch<T> = (action: T) => void;
|
||||||
|
export type Middleware<State extends BaseState = BaseState> = (
|
||||||
export type Middleware<State = any> = (
|
|
||||||
store: Pick<ReduxLikeStateContainer<State, any>, 'getState' | 'dispatch'>
|
store: Pick<ReduxLikeStateContainer<State, any>, 'getState' | 'dispatch'>
|
||||||
) => (
|
) => (
|
||||||
next: (action: TransitionDescription) => TransitionDescription | any
|
next: (action: TransitionDescription) => TransitionDescription | any
|
||||||
) => Dispatch<TransitionDescription>;
|
) => Dispatch<TransitionDescription>;
|
||||||
|
|
||||||
export type Reducer<State> = (state: State, action: TransitionDescription) => State;
|
export type Reducer<State extends BaseState> = (
|
||||||
|
state: State,
|
||||||
|
action: TransitionDescription
|
||||||
|
) => State;
|
||||||
|
|
||||||
export type UnboxState<
|
export type UnboxState<
|
||||||
Container extends StateContainer<any, any>
|
Container extends StateContainer<any, any>
|
||||||
|
@ -80,7 +83,7 @@ export type UnboxTransitions<
|
||||||
> = Container extends StateContainer<any, infer T> ? T : never;
|
> = Container extends StateContainer<any, infer T> ? T : never;
|
||||||
|
|
||||||
export type Selector<Result, Args extends any[] = []> = (...args: Args) => Result;
|
export type Selector<Result, Args extends any[] = []> = (...args: Args) => Result;
|
||||||
export type PureSelector<State, Result, Args extends any[] = []> = (
|
export type PureSelector<State extends BaseState, Result, Args extends any[] = []> = (
|
||||||
state: State
|
state: State
|
||||||
) => Selector<Result, Args>;
|
) => Selector<Result, Args>;
|
||||||
export type EnsurePureSelector<T> = Ensure<T, PureSelector<any, any, any>>;
|
export type EnsurePureSelector<T> = Ensure<T, PureSelector<any, any, any>>;
|
||||||
|
@ -93,7 +96,12 @@ export type PureSelectorsToSelectors<T extends object> = {
|
||||||
|
|
||||||
export type Comparator<Result> = (previous: Result, current: Result) => boolean;
|
export type Comparator<Result> = (previous: Result, current: Result) => boolean;
|
||||||
|
|
||||||
export type MapStateToProps<State, StateProps extends object> = (state: State) => StateProps;
|
export type MapStateToProps<State extends BaseState, StateProps extends object> = (
|
||||||
export type Connect<State> = <Props extends object, StatePropKeys extends keyof Props>(
|
state: State
|
||||||
|
) => StateProps;
|
||||||
|
export type Connect<State extends BaseState> = <
|
||||||
|
Props extends object,
|
||||||
|
StatePropKeys extends keyof Props
|
||||||
|
>(
|
||||||
mapStateToProp: MapStateToProps<State, Pick<Props, StatePropKeys>>
|
mapStateToProp: MapStateToProps<State, Pick<Props, StatePropKeys>>
|
||||||
) => (component: React.ComponentType<Props>) => React.FC<Omit<Props, StatePropKeys>>;
|
) => (component: React.ComponentType<Props>) => React.FC<Omit<Props, StatePropKeys>>;
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { BaseStateContainer, createStateContainer } from '../state_containers';
|
import { BaseState, BaseStateContainer, createStateContainer } from '../state_containers';
|
||||||
import {
|
import {
|
||||||
defaultState,
|
defaultState,
|
||||||
pureTransitions,
|
pureTransitions,
|
||||||
|
@ -89,7 +89,7 @@ describe('state_sync', () => {
|
||||||
// initial sync of storage to state is not happening
|
// initial sync of storage to state is not happening
|
||||||
expect(container.getState()).toEqual(defaultState);
|
expect(container.getState()).toEqual(defaultState);
|
||||||
|
|
||||||
const storageState2 = [{ id: 1, text: 'todo', completed: true }];
|
const storageState2 = { todos: [{ id: 1, text: 'todo', completed: true }] };
|
||||||
(testStateStorage.get as jest.Mock).mockImplementation(() => storageState2);
|
(testStateStorage.get as jest.Mock).mockImplementation(() => storageState2);
|
||||||
storageChange$.next(storageState2);
|
storageChange$.next(storageState2);
|
||||||
|
|
||||||
|
@ -124,7 +124,7 @@ describe('state_sync', () => {
|
||||||
start();
|
start();
|
||||||
|
|
||||||
const originalState = container.getState();
|
const originalState = container.getState();
|
||||||
const storageState = [...originalState];
|
const storageState = { ...originalState };
|
||||||
(testStateStorage.get as jest.Mock).mockImplementation(() => storageState);
|
(testStateStorage.get as jest.Mock).mockImplementation(() => storageState);
|
||||||
storageChange$.next(storageState);
|
storageChange$.next(storageState);
|
||||||
|
|
||||||
|
@ -134,7 +134,7 @@ describe('state_sync', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('storage change to null should notify state', () => {
|
it('storage change to null should notify state', () => {
|
||||||
container.set([{ completed: false, id: 1, text: 'changed' }]);
|
container.set({ todos: [{ completed: false, id: 1, text: 'changed' }] });
|
||||||
const { stop, start } = syncStates([
|
const { stop, start } = syncStates([
|
||||||
{
|
{
|
||||||
stateContainer: withDefaultState(container, defaultState),
|
stateContainer: withDefaultState(container, defaultState),
|
||||||
|
@ -189,8 +189,8 @@ describe('state_sync', () => {
|
||||||
]);
|
]);
|
||||||
start();
|
start();
|
||||||
|
|
||||||
const newStateFromUrl = [{ completed: false, id: 1, text: 'changed' }];
|
const newStateFromUrl = { todos: [{ completed: false, id: 1, text: 'changed' }] };
|
||||||
history.replace('/#?_s=!((completed:!f,id:1,text:changed))');
|
history.replace('/#?_s=(todos:!((completed:!f,id:1,text:changed)))');
|
||||||
|
|
||||||
expect(container.getState()).toEqual(newStateFromUrl);
|
expect(container.getState()).toEqual(newStateFromUrl);
|
||||||
expect(JSON.parse(sessionStorage.getItem(key)!)).toEqual(newStateFromUrl);
|
expect(JSON.parse(sessionStorage.getItem(key)!)).toEqual(newStateFromUrl);
|
||||||
|
@ -220,7 +220,7 @@ describe('state_sync', () => {
|
||||||
expect(history.length).toBe(startHistoryLength + 1);
|
expect(history.length).toBe(startHistoryLength + 1);
|
||||||
|
|
||||||
expect(getCurrentUrl()).toMatchInlineSnapshot(
|
expect(getCurrentUrl()).toMatchInlineSnapshot(
|
||||||
`"/#?_s=!((completed:!t,id:0,text:'Learning%20state%20containers'),(completed:!t,id:2,text:'2'),(completed:!t,id:3,text:'3'))"`
|
`"/#?_s=(todos:!((completed:!t,id:0,text:'Learning%20state%20containers'),(completed:!t,id:2,text:'2'),(completed:!t,id:3,text:'3')))"`
|
||||||
);
|
);
|
||||||
|
|
||||||
stop();
|
stop();
|
||||||
|
@ -248,14 +248,14 @@ describe('state_sync', () => {
|
||||||
|
|
||||||
expect(history.length).toBe(startHistoryLength + 1);
|
expect(history.length).toBe(startHistoryLength + 1);
|
||||||
expect(getCurrentUrl()).toMatchInlineSnapshot(
|
expect(getCurrentUrl()).toMatchInlineSnapshot(
|
||||||
`"/#?_s=!((completed:!t,id:0,text:'Learning%20state%20containers'),(completed:!t,id:2,text:'2'),(completed:!t,id:3,text:'3'))"`
|
`"/#?_s=(todos:!((completed:!t,id:0,text:'Learning%20state%20containers'),(completed:!t,id:2,text:'2'),(completed:!t,id:3,text:'3')))"`
|
||||||
);
|
);
|
||||||
|
|
||||||
await tick();
|
await tick();
|
||||||
|
|
||||||
expect(history.length).toBe(startHistoryLength + 1);
|
expect(history.length).toBe(startHistoryLength + 1);
|
||||||
expect(getCurrentUrl()).toMatchInlineSnapshot(
|
expect(getCurrentUrl()).toMatchInlineSnapshot(
|
||||||
`"/#?_s=!((completed:!t,id:0,text:'Learning%20state%20containers'),(completed:!t,id:2,text:'2'),(completed:!t,id:3,text:'3'))"`
|
`"/#?_s=(todos:!((completed:!t,id:0,text:'Learning%20state%20containers'),(completed:!t,id:2,text:'2'),(completed:!t,id:3,text:'3')))"`
|
||||||
);
|
);
|
||||||
|
|
||||||
stop();
|
stop();
|
||||||
|
@ -294,7 +294,7 @@ describe('state_sync', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function withDefaultState<State>(
|
function withDefaultState<State extends BaseState>(
|
||||||
stateContainer: BaseStateContainer<State>,
|
stateContainer: BaseStateContainer<State>,
|
||||||
// eslint-disable-next-line no-shadow
|
// eslint-disable-next-line no-shadow
|
||||||
defaultState: State
|
defaultState: State
|
||||||
|
@ -302,7 +302,10 @@ function withDefaultState<State>(
|
||||||
return {
|
return {
|
||||||
...stateContainer,
|
...stateContainer,
|
||||||
set: (state: State | null) => {
|
set: (state: State | null) => {
|
||||||
stateContainer.set(state || defaultState);
|
stateContainer.set({
|
||||||
|
...defaultState,
|
||||||
|
...state,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ import defaultComparator from 'fast-deep-equal';
|
||||||
import { IStateSyncConfig } from './types';
|
import { IStateSyncConfig } from './types';
|
||||||
import { IStateStorage } from './state_sync_state_storage';
|
import { IStateStorage } from './state_sync_state_storage';
|
||||||
import { distinctUntilChangedWithInitialValue } from '../../common';
|
import { distinctUntilChangedWithInitialValue } from '../../common';
|
||||||
|
import { BaseState } from '../state_containers';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility for syncing application state wrapped in state container
|
* Utility for syncing application state wrapped in state container
|
||||||
|
@ -86,7 +87,10 @@ export interface ISyncStateRef<stateStorage extends IStateStorage = IStateStorag
|
||||||
// start syncing state with storage
|
// start syncing state with storage
|
||||||
start: StartSyncStateFnType;
|
start: StartSyncStateFnType;
|
||||||
}
|
}
|
||||||
export function syncState<State = unknown, StateStorage extends IStateStorage = IStateStorage>({
|
export function syncState<
|
||||||
|
State extends BaseState,
|
||||||
|
StateStorage extends IStateStorage = IStateStorage
|
||||||
|
>({
|
||||||
storageKey,
|
storageKey,
|
||||||
stateStorage,
|
stateStorage,
|
||||||
stateContainer,
|
stateContainer,
|
||||||
|
|
|
@ -17,10 +17,11 @@
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { BaseStateContainer } from '../state_containers/types';
|
import { BaseState, BaseStateContainer } from '../state_containers/types';
|
||||||
import { IStateStorage } from './state_sync_state_storage';
|
import { IStateStorage } from './state_sync_state_storage';
|
||||||
|
|
||||||
export interface INullableBaseStateContainer<State> extends BaseStateContainer<State> {
|
export interface INullableBaseStateContainer<State extends BaseState>
|
||||||
|
extends BaseStateContainer<State> {
|
||||||
// State container for stateSync() have to accept "null"
|
// State container for stateSync() have to accept "null"
|
||||||
// for example, set() implementation could handle null and fallback to some default state
|
// for example, set() implementation could handle null and fallback to some default state
|
||||||
// this is required to handle edge case, when state in storage becomes empty and syncing is in progress.
|
// this is required to handle edge case, when state in storage becomes empty and syncing is in progress.
|
||||||
|
@ -29,7 +30,7 @@ export interface INullableBaseStateContainer<State> extends BaseStateContainer<S
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IStateSyncConfig<
|
export interface IStateSyncConfig<
|
||||||
State = unknown,
|
State extends BaseState,
|
||||||
StateStorage extends IStateStorage = IStateStorage
|
StateStorage extends IStateStorage = IStateStorage
|
||||||
> {
|
> {
|
||||||
/**
|
/**
|
||||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -3929,6 +3929,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/dedent/-/dedent-0.7.0.tgz#155f339ca404e6dd90b9ce46a3f78fd69ca9b050"
|
resolved "https://registry.yarnpkg.com/@types/dedent/-/dedent-0.7.0.tgz#155f339ca404e6dd90b9ce46a3f78fd69ca9b050"
|
||||||
integrity sha512-EGlKlgMhnLt/cM4DbUSafFdrkeJoC9Mvnj0PUCU7tFmTjMjNRT957kXCx0wYm3JuEq4o4ZsS5vG+NlkM2DMd2A==
|
integrity sha512-EGlKlgMhnLt/cM4DbUSafFdrkeJoC9Mvnj0PUCU7tFmTjMjNRT957kXCx0wYm3JuEq4o4ZsS5vG+NlkM2DMd2A==
|
||||||
|
|
||||||
|
"@types/deep-freeze-strict@^1.1.0":
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/deep-freeze-strict/-/deep-freeze-strict-1.1.0.tgz#447a6a2576191344aa42310131dd3df5c41492c4"
|
||||||
|
integrity sha1-RHpqJXYZE0SqQjEBMd099cQUksQ=
|
||||||
|
|
||||||
"@types/delete-empty@^2.0.0":
|
"@types/delete-empty@^2.0.0":
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/delete-empty/-/delete-empty-2.0.0.tgz#1647ae9e68f708a6ba778531af667ec55bc61964"
|
resolved "https://registry.yarnpkg.com/@types/delete-empty/-/delete-empty-2.0.0.tgz#1647ae9e68f708a6ba778531af667ec55bc61964"
|
||||||
|
@ -10523,6 +10528,11 @@ deep-extend@^0.6.0:
|
||||||
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
|
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac"
|
||||||
integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
|
integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==
|
||||||
|
|
||||||
|
deep-freeze-strict@^1.1.1:
|
||||||
|
version "1.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/deep-freeze-strict/-/deep-freeze-strict-1.1.1.tgz#77d0583ca24a69be4bbd9ac2fae415d55523e5b0"
|
||||||
|
integrity sha1-d9BYPKJKab5LvZrC+uQV1VUj5bA=
|
||||||
|
|
||||||
deep-is@^0.1.3, deep-is@~0.1.3:
|
deep-is@^0.1.3, deep-is@~0.1.3:
|
||||||
version "0.1.3"
|
version "0.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
|
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
|
||||||
|
|
Loading…
Reference in a new issue