@veams/status-quo 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.cjs +132 -0
- package/.nvmrc +1 -0
- package/.prettierrc +7 -0
- package/README.md +130 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.js +3 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/state-factory.d.ts +2 -0
- package/dist/hooks/state-factory.js +10 -0
- package/dist/hooks/state-factory.js.map +1 -0
- package/dist/hooks/state-singleton.d.ts +2 -0
- package/dist/hooks/state-singleton.js +9 -0
- package/dist/hooks/state-singleton.js.map +1 -0
- package/dist/hooks/state-subscription.d.ts +3 -0
- package/dist/hooks/state-subscription.js +15 -0
- package/dist/hooks/state-subscription.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/store/__tests__/state-handler.spec.d.ts +1 -0
- package/dist/store/__tests__/state-handler.spec.js +85 -0
- package/dist/store/__tests__/state-handler.spec.js.map +1 -0
- package/dist/store/dev-tools.d.ts +23 -0
- package/dist/store/dev-tools.js +16 -0
- package/dist/store/dev-tools.js.map +1 -0
- package/dist/store/index.d.ts +3 -0
- package/dist/store/index.js +3 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/state-handler.d.ts +36 -0
- package/dist/store/state-handler.js +122 -0
- package/dist/store/state-handler.js.map +1 -0
- package/dist/store/state-singleton.d.ts +5 -0
- package/dist/store/state-singleton.js +12 -0
- package/dist/store/state-singleton.js.map +1 -0
- package/dist/types/types.d.ts +7 -0
- package/dist/types/types.js +2 -0
- package/dist/types/types.js.map +1 -0
- package/jest.config.ci.cjs +7 -0
- package/jest.config.cjs +22 -0
- package/package.json +79 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/state-factory.tsx +17 -0
- package/src/hooks/state-singleton.tsx +14 -0
- package/src/hooks/state-subscription.tsx +25 -0
- package/src/index.ts +9 -0
- package/src/store/__tests__/state-handler.spec.ts +108 -0
- package/src/store/dev-tools.ts +44 -0
- package/src/store/index.ts +3 -0
- package/src/store/state-handler.ts +181 -0
- package/src/store/state-singleton.ts +21 -0
- package/src/types/types.ts +8 -0
- package/tsconfig.json +42 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { BehaviorSubject, distinctUntilKeyChanged, map, scan, Subject, pipe, distinctUntilChanged } from 'rxjs';
|
|
2
|
+
|
|
3
|
+
import { withDevTools } from './dev-tools.js';
|
|
4
|
+
|
|
5
|
+
import type { StateSubscriptionHandler } from '../types/types.js';
|
|
6
|
+
import type { DevTools, MessagePayload } from './dev-tools.js';
|
|
7
|
+
import type { Observable, Subscription } from 'rxjs';
|
|
8
|
+
|
|
9
|
+
type Subscriptions = Subscription[];
|
|
10
|
+
type StateHandlerProps<S> = {
|
|
11
|
+
initialState: S;
|
|
12
|
+
options?: {
|
|
13
|
+
devTools: {
|
|
14
|
+
enabled?: boolean;
|
|
15
|
+
namespace: string;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
type StateObservableOptions = { useDistinctUntilChanged?: boolean };
|
|
20
|
+
|
|
21
|
+
function distinctUntilChangedAsJson<T>() {
|
|
22
|
+
return pipe<Observable<T>, Observable<T>>(
|
|
23
|
+
distinctUntilChanged((a, b) => {
|
|
24
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
25
|
+
})
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
const pipeMap = {
|
|
31
|
+
useDistinctUntilChanged: distinctUntilChangedAsJson(),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const defaultOptions = { devTools: { enabled: false, namespace: 'Store' } };
|
|
35
|
+
|
|
36
|
+
export abstract class StateHandler<S, A> implements StateSubscriptionHandler<S, A> {
|
|
37
|
+
private readonly updates$: Subject<{
|
|
38
|
+
actionName: string;
|
|
39
|
+
state: Partial<S>;
|
|
40
|
+
}> = new Subject();
|
|
41
|
+
|
|
42
|
+
private readonly state$: BehaviorSubject<S>;
|
|
43
|
+
private readonly initialState: S;
|
|
44
|
+
|
|
45
|
+
private devTools: DevTools | null = null;
|
|
46
|
+
|
|
47
|
+
subscriptions: Subscriptions = [];
|
|
48
|
+
|
|
49
|
+
protected constructor({ initialState, options = defaultOptions }: StateHandlerProps<S>) {
|
|
50
|
+
const mergedOptions = {
|
|
51
|
+
...defaultOptions,
|
|
52
|
+
...options,
|
|
53
|
+
devTools: {
|
|
54
|
+
...defaultOptions.devTools,
|
|
55
|
+
...options?.devTools,
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
this.initialState = initialState;
|
|
59
|
+
this.state$ = new BehaviorSubject<S>(initialState);
|
|
60
|
+
this.devTools = mergedOptions.devTools.enabled
|
|
61
|
+
? withDevTools(this.initialState, {
|
|
62
|
+
name: mergedOptions.devTools.namespace,
|
|
63
|
+
instanceId: mergedOptions.devTools.namespace.toLowerCase().replaceAll(' ', '-'),
|
|
64
|
+
actionCreators: this.getActions(),
|
|
65
|
+
features: {
|
|
66
|
+
pause: true, // start/pause recording of dispatched actions
|
|
67
|
+
lock: true, // lock/unlock dispatching actions and side effects
|
|
68
|
+
persist: false, // persist states on page reloading (Using action creators under the hood which are not bound to our state)
|
|
69
|
+
export: true, // export history of actions in a file
|
|
70
|
+
import: 'custom', // import history of actions from a file
|
|
71
|
+
jump: true, // jump back and forth (time travelling)
|
|
72
|
+
skip: true, // skip (cancel) actions
|
|
73
|
+
reorder: true, // drag and drop actions in the history list
|
|
74
|
+
dispatch: false, // dispatch custom actions or action creators (This is only working in redux reducer context)
|
|
75
|
+
test: false, // generate tests for the selected actions (Reducer like tests make no sense)
|
|
76
|
+
},
|
|
77
|
+
})
|
|
78
|
+
: null;
|
|
79
|
+
|
|
80
|
+
this.bindUpdatesAndEvents();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
getInitialState() {
|
|
84
|
+
return this.initialState;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
getState() {
|
|
88
|
+
return this.state$.getValue();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
setState(newState: Partial<S>, actionName = 'change') {
|
|
92
|
+
this.updates$.next({
|
|
93
|
+
state: newState,
|
|
94
|
+
actionName,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
destroy(): void {
|
|
99
|
+
this.subscriptions.forEach((subscription) => subscription.unsubscribe());
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
getStateItemAsObservable(key: keyof S) {
|
|
103
|
+
return this.state$.pipe(
|
|
104
|
+
distinctUntilKeyChanged(key),
|
|
105
|
+
map((state) => state[key])
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
getStateAsObservable(
|
|
110
|
+
options: StateObservableOptions = {
|
|
111
|
+
useDistinctUntilChanged: true,
|
|
112
|
+
}
|
|
113
|
+
) {
|
|
114
|
+
// Unfortunately we cannot add pipe operators conditionally in an easy manner.
|
|
115
|
+
// That's why we use a simple object to attach operators to a new state observable via reduce().
|
|
116
|
+
// This way we can easily extend our default operators map.
|
|
117
|
+
return Object.keys(options)
|
|
118
|
+
.filter((optionKey) => options[optionKey as keyof StateObservableOptions] === true)
|
|
119
|
+
.map((enabledOptions) => pipeMap[enabledOptions as keyof StateObservableOptions])
|
|
120
|
+
.reduce((stateObservable$, operator) => {
|
|
121
|
+
return stateObservable$.pipe(operator) as BehaviorSubject<S>;
|
|
122
|
+
}, this.state$);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
getObservableItem(key: keyof S) {
|
|
126
|
+
return this.getStateItemAsObservable(key);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private bindUpdatesAndEvents() {
|
|
130
|
+
this.updates$
|
|
131
|
+
.pipe(
|
|
132
|
+
scan(
|
|
133
|
+
(current, updated) => {
|
|
134
|
+
const { state: oldState } = current;
|
|
135
|
+
const { state: newState, actionName } = updated;
|
|
136
|
+
|
|
137
|
+
return { actionName, state: { ...oldState, ...newState } };
|
|
138
|
+
},
|
|
139
|
+
{ actionName: 'init', state: this.initialState } // Initial event object which is changed by this.setState().
|
|
140
|
+
),
|
|
141
|
+
map(({ actionName, state }) => {
|
|
142
|
+
this.devTools?.send(actionName, state);
|
|
143
|
+
|
|
144
|
+
return state;
|
|
145
|
+
})
|
|
146
|
+
)
|
|
147
|
+
.subscribe(this.state$);
|
|
148
|
+
|
|
149
|
+
this.devTools?.subscribe(this.handleDevToolsEvents);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private handleDevToolsEvents = (message: MessagePayload) => {
|
|
153
|
+
if (message.type === 'DISPATCH') {
|
|
154
|
+
switch (message.payload.type) {
|
|
155
|
+
case 'RESET':
|
|
156
|
+
this.state$.next(this.getInitialState());
|
|
157
|
+
this.devTools?.init(this.getInitialState());
|
|
158
|
+
break;
|
|
159
|
+
|
|
160
|
+
case 'COMMIT':
|
|
161
|
+
this.state$.next(this.getState());
|
|
162
|
+
this.devTools?.init(this.getState());
|
|
163
|
+
break;
|
|
164
|
+
|
|
165
|
+
case 'JUMP_TO_STATE':
|
|
166
|
+
case 'JUMP_TO_ACTION':
|
|
167
|
+
this.state$.next(JSON.parse(message.state));
|
|
168
|
+
break;
|
|
169
|
+
|
|
170
|
+
default:
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
getObservable(): Observable<S> {
|
|
177
|
+
return this.getStateAsObservable();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
abstract getActions(): A;
|
|
181
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { StateSubscriptionHandler } from '../types/types.js';
|
|
2
|
+
|
|
3
|
+
export interface StateSingleton<V, A> {
|
|
4
|
+
getInstance: () => StateSubscriptionHandler<V, A>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function makeStateSingleton<S, A>(
|
|
8
|
+
stateHandlerFactory: () => StateSubscriptionHandler<S, A>
|
|
9
|
+
): StateSingleton<S, A> {
|
|
10
|
+
let instance: StateSubscriptionHandler<S, A> | null = null;
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
getInstance() {
|
|
14
|
+
if (!instance) {
|
|
15
|
+
instance = stateHandlerFactory();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return instance;
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"allowJs": true,
|
|
4
|
+
"allowSyntheticDefaultImports": true,
|
|
5
|
+
"skipLibCheck": true,
|
|
6
|
+
"strict": true,
|
|
7
|
+
"sourceMap": true,
|
|
8
|
+
"declaration": true,
|
|
9
|
+
"emitDecoratorMetadata": true,
|
|
10
|
+
"experimentalDecorators": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"lib": [
|
|
13
|
+
"es2015",
|
|
14
|
+
"es2016",
|
|
15
|
+
"es2017",
|
|
16
|
+
"es2021",
|
|
17
|
+
"es2022",
|
|
18
|
+
"dom",
|
|
19
|
+
"dom.iterable"
|
|
20
|
+
],
|
|
21
|
+
"module": "es2022",
|
|
22
|
+
"moduleResolution": "bundler",
|
|
23
|
+
"target": "es2022",
|
|
24
|
+
"declarationDir": "dist",
|
|
25
|
+
"outDir": "dist",
|
|
26
|
+
"typeRoots": [
|
|
27
|
+
"node",
|
|
28
|
+
"node_modules/@types",
|
|
29
|
+
"node_modules/@redux-devtools/extension"
|
|
30
|
+
],
|
|
31
|
+
"jsx": "react"
|
|
32
|
+
},
|
|
33
|
+
"include": [
|
|
34
|
+
"src/**/*.ts",
|
|
35
|
+
"src/**/*.tsx"
|
|
36
|
+
],
|
|
37
|
+
"exclude": [
|
|
38
|
+
"node_modules",
|
|
39
|
+
"tasks",
|
|
40
|
+
"src/**/*.mock.ts"
|
|
41
|
+
]
|
|
42
|
+
}
|