@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.
Files changed (52) hide show
  1. package/.eslintrc.cjs +132 -0
  2. package/.nvmrc +1 -0
  3. package/.prettierrc +7 -0
  4. package/README.md +130 -0
  5. package/dist/hooks/index.d.ts +2 -0
  6. package/dist/hooks/index.js +3 -0
  7. package/dist/hooks/index.js.map +1 -0
  8. package/dist/hooks/state-factory.d.ts +2 -0
  9. package/dist/hooks/state-factory.js +10 -0
  10. package/dist/hooks/state-factory.js.map +1 -0
  11. package/dist/hooks/state-singleton.d.ts +2 -0
  12. package/dist/hooks/state-singleton.js +9 -0
  13. package/dist/hooks/state-singleton.js.map +1 -0
  14. package/dist/hooks/state-subscription.d.ts +3 -0
  15. package/dist/hooks/state-subscription.js +15 -0
  16. package/dist/hooks/state-subscription.js.map +1 -0
  17. package/dist/index.d.ts +6 -0
  18. package/dist/index.js +4 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/store/__tests__/state-handler.spec.d.ts +1 -0
  21. package/dist/store/__tests__/state-handler.spec.js +85 -0
  22. package/dist/store/__tests__/state-handler.spec.js.map +1 -0
  23. package/dist/store/dev-tools.d.ts +23 -0
  24. package/dist/store/dev-tools.js +16 -0
  25. package/dist/store/dev-tools.js.map +1 -0
  26. package/dist/store/index.d.ts +3 -0
  27. package/dist/store/index.js +3 -0
  28. package/dist/store/index.js.map +1 -0
  29. package/dist/store/state-handler.d.ts +36 -0
  30. package/dist/store/state-handler.js +122 -0
  31. package/dist/store/state-handler.js.map +1 -0
  32. package/dist/store/state-singleton.d.ts +5 -0
  33. package/dist/store/state-singleton.js +12 -0
  34. package/dist/store/state-singleton.js.map +1 -0
  35. package/dist/types/types.d.ts +7 -0
  36. package/dist/types/types.js +2 -0
  37. package/dist/types/types.js.map +1 -0
  38. package/jest.config.ci.cjs +7 -0
  39. package/jest.config.cjs +22 -0
  40. package/package.json +79 -0
  41. package/src/hooks/index.ts +2 -0
  42. package/src/hooks/state-factory.tsx +17 -0
  43. package/src/hooks/state-singleton.tsx +14 -0
  44. package/src/hooks/state-subscription.tsx +25 -0
  45. package/src/index.ts +9 -0
  46. package/src/store/__tests__/state-handler.spec.ts +108 -0
  47. package/src/store/dev-tools.ts +44 -0
  48. package/src/store/index.ts +3 -0
  49. package/src/store/state-handler.ts +181 -0
  50. package/src/store/state-singleton.ts +21 -0
  51. package/src/types/types.ts +8 -0
  52. 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
+ }
@@ -0,0 +1,8 @@
1
+ import type { Observable } from 'rxjs';
2
+
3
+ export interface StateSubscriptionHandler<V, A> {
4
+ getObservable: () => Observable<V>;
5
+ destroy: () => void;
6
+ getInitialState: () => V;
7
+ getActions: () => A;
8
+ }
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
+ }