@dungarees/store 0.11.4

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/effect.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import type { DomainEvent } from '@dungarees/core/event.ts';
2
+ import type { OperatorFunction } from 'rxjs';
3
+ export declare const filterByType: <ALL_EVENTS extends DomainEvent, const TYPE extends string>(selectedType: TYPE) => OperatorFunction<ALL_EVENTS, Extract<ALL_EVENTS, {
4
+ type: TYPE;
5
+ }>>;
package/effect.js ADDED
@@ -0,0 +1,5 @@
1
+ import { filter } from 'rxjs/operators';
2
+ // TYPE is constrained to `string` rather than to ALL_EVENTS['type'] on purpose: constraining it to
3
+ // the union makes TypeScript infer ALL_EVENTS from this argument instead of from the pipe the
4
+ // operator is placed in, which collapses the input type to the single event being selected.
5
+ export const filterByType = (selectedType) => filter((event) => event.type === selectedType);
@@ -0,0 +1 @@
1
+ export {};
package/effect.test.js ADDED
@@ -0,0 +1,36 @@
1
+ import { filterByType } from './effect.js';
2
+ import { mtest } from '@dungarees/core/marbles-vitest.ts';
3
+ import { map } from 'rxjs/operators';
4
+ import { expectTypeOf, test } from 'vitest';
5
+ mtest('filterByType keeps only the events of the selected type', ({ expect, cold }) => {
6
+ const event$ = cold('-12', {
7
+ 1: { type: 'event-1', payload: 1 },
8
+ 2: { type: 'event-2', payload: undefined },
9
+ });
10
+ const filtered$ = event$.pipe(filterByType('event-1'), map(({ payload }) => payload));
11
+ expect(filtered$).toBeObservable('-1-', { 1: 1 });
12
+ });
13
+ mtest('filterByType passes nothing on when no event matches', ({ expect, cold }) => {
14
+ const event$ = cold('-2', { 2: { type: 'event-2', payload: undefined } });
15
+ expect(event$.pipe(filterByType('event-1'))).toBeObservable('--');
16
+ });
17
+ // Deliberately left unannotated: the inferred output is the contract under test, and annotating it
18
+ // would feed the expected type back into inference instead of checking it.
19
+ const filterStarted = (event$) => event$.pipe(filterByType('event-1'));
20
+ const filterUnknownType = (event$) => event$.pipe(filterByType('event-3'));
21
+ test('filterByType narrows the event union down to the selected member', () => {
22
+ expectTypeOf(filterStarted).returns.toEqualTypeOf();
23
+ });
24
+ test('filterByType narrows the payload, so a mistyped read does not compile', () => {
25
+ const check = (event$) => {
26
+ event$.pipe(filterByType('event-1'), map(({ payload }) => {
27
+ // @ts-expect-error the 'event-1' payload is a number, so it is never a string
28
+ const wrong = payload;
29
+ return wrong;
30
+ }));
31
+ };
32
+ expectTypeOf(check).returns.toBeVoid();
33
+ });
34
+ test('filterByType yields no event at all for a type outside the union', () => {
35
+ expectTypeOf(filterUnknownType).returns.toEqualTypeOf();
36
+ });
package/error.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ import type { StoreError } from './type.ts';
2
+ export declare const toStoreError: ({ message, stack }: Error) => StoreError;
3
+ export declare const createStoreError: (message: string) => StoreError;
package/error.js ADDED
@@ -0,0 +1,5 @@
1
+ export const toStoreError = ({ message, stack = '' }) => ({
2
+ message,
3
+ stack,
4
+ });
5
+ export const createStoreError = (message) => toStoreError(new Error(message));
@@ -0,0 +1 @@
1
+ export {};
package/error.test.js ADDED
@@ -0,0 +1,24 @@
1
+ import { createStoreError, toStoreError } from './error.js';
2
+ import { expect, test } from 'vitest';
3
+ test('toStoreError keeps the message of the error it is given', () => {
4
+ expect(toStoreError(new Error('message')).message).toBe('message');
5
+ });
6
+ test('toStoreError carries the stack over, so the origin survives serialisation', () => {
7
+ expect(toStoreError(new Error('message')).stack).toContain('error.test.ts');
8
+ });
9
+ test('toStoreError falls back to an empty stack rather than undefined', () => {
10
+ const stackless = new Error('message');
11
+ delete stackless.stack;
12
+ expect(toStoreError(stackless).stack).toBe('');
13
+ });
14
+ test('toStoreError produces a plain object, so it can go through the store', () => {
15
+ const storeError = toStoreError(new Error('message'));
16
+ expect(storeError).toEqual({ message: 'message', stack: storeError.stack });
17
+ expect(storeError instanceof Error).toBe(false);
18
+ });
19
+ test('createStoreError builds a store error straight from a message', () => {
20
+ expect(createStoreError('message').message).toBe('message');
21
+ });
22
+ test('createStoreError records where it was created', () => {
23
+ expect(createStoreError('message').stack).toContain('error.test.ts');
24
+ });
package/fake.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ import type { Reducer, ReducersObject, StateReadable, Store } from './type.ts';
2
+ import type { DomainEvent } from '@dungarees/core/event.ts';
3
+ import type { JsonObject, ObjectWithStringLiteralKey, StringLiteral } from '@dungarees/core/type-util.ts';
4
+ import type { Observable } from 'rxjs';
5
+ export type TestAppStoreState<STATE, STATE_KEY> = ObjectWithStringLiteralKey<STATE_KEY, STATE>;
6
+ export declare const STORE_INIT: DomainEvent<'init', undefined>;
7
+ export type StoreTools<STATE, EVENT extends DomainEvent, STATE_KEY, BASE_STATE extends JsonObject = Record<never, never>, APP_STORE_STATE = TestAppStoreState<STATE, STATE_KEY>> = {
8
+ getStateReadable: (in$: Observable<Partial<STATE>>) => StateReadable<APP_STORE_STATE>;
9
+ createAppStore: () => {
10
+ store: Store<APP_STORE_STATE & BASE_STATE, EVENT>;
11
+ sliceState$: Observable<STATE>;
12
+ };
13
+ };
14
+ type CreateStoreToolsArgs<NAMESPACE, STATE, EVENT extends DomainEvent, BASE_STATE extends JsonObject> = {
15
+ namespace: StringLiteral<NAMESPACE>;
16
+ reducer: Reducer<STATE, EVENT>;
17
+ baseStore?: ReducersObject<BASE_STATE, EVENT>;
18
+ };
19
+ export declare const createStoreTools: <NAMESPACE, STATE, EVENT extends DomainEvent = DomainEvent, BASE_STATE extends JsonObject = Record<never, never>>({ namespace, reducer, baseStore, }: CreateStoreToolsArgs<NAMESPACE, STATE, EVENT, BASE_STATE>) => StoreTools<STATE, EVENT, NAMESPACE, BASE_STATE>;
20
+ export {};
package/fake.js ADDED
@@ -0,0 +1,27 @@
1
+ import { createStore } from './service.js';
2
+ import { makeObjectFromStringLiteral } from '@dungarees/core/util.ts';
3
+ import { map } from 'rxjs/operators';
4
+ // No reducer handles this, so reducing it returns whatever initial state each one declares. That
5
+ // is the only way to read an initial state out of a reducer without standing up a store.
6
+ export const STORE_INIT = { type: 'init', payload: undefined };
7
+ export const createStoreTools = ({ namespace, reducer, baseStore, }) => {
8
+ return {
9
+ getStateReadable: (in$) => ({
10
+ // Standing in for the whole store state while only part of one slice is supplied is the
11
+ // point of this helper: a query test states the fields it reads and nothing else.
12
+ state$: in$.pipe(map((sliceState) => makeObjectFromStringLiteral(namespace, sliceState))),
13
+ }),
14
+ createAppStore: () => {
15
+ const store = createStore({
16
+ ...baseStore,
17
+ ...makeObjectFromStringLiteral(namespace, reducer),
18
+ // The reducer map is assembled from a dynamically keyed object, which cannot be built in a
19
+ // way TypeScript can check against the state it produces.
20
+ });
21
+ return {
22
+ store,
23
+ sliceState$: store.state$.pipe(map((state) => state[namespace])),
24
+ };
25
+ },
26
+ };
27
+ };
package/fake.test.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/fake.test.js ADDED
@@ -0,0 +1,46 @@
1
+ import { createStoreTools, STORE_INIT } from './fake.js';
2
+ import { createStoreSlice } from './service.js';
3
+ import { mtest } from '@dungarees/core/marbles-vitest.ts';
4
+ import { map } from 'rxjs/operators';
5
+ import { expect, test } from 'vitest';
6
+ const initialCountState = { count: 0, label: 'none' };
7
+ const countSlice = createStoreSlice({
8
+ name: 'count',
9
+ initialState: initialCountState,
10
+ reducers: {
11
+ increment: (state, _) => ({ ...state, count: state.count + 1 }),
12
+ },
13
+ });
14
+ const countReducer = countSlice.reducer;
15
+ test('STORE_INIT drives a reducer to the initial state it declares', () => {
16
+ expect(countReducer(undefined, STORE_INIT)).toEqual({ count: 0, label: 'none' });
17
+ });
18
+ test('STORE_INIT is an event no reducer is expected to handle', () => {
19
+ expect(STORE_INIT).toEqual({ type: 'init', payload: undefined });
20
+ });
21
+ mtest('getStateReadable presents slice state as the whole store state', ({ expect, cold }) => {
22
+ const { getStateReadable } = createStoreTools({ namespace: 'count', reducer: countReducer });
23
+ const { state$ } = getStateReadable(cold('s', { s: { count: 7, label: 'seven' } }));
24
+ expect(state$).toBeObservable('s', { s: { count: { count: 7, label: 'seven' } } });
25
+ });
26
+ mtest('getStateReadable lets a test supply only the state it cares about', ({ expect, cold }) => {
27
+ const { getStateReadable } = createStoreTools({ namespace: 'count', reducer: countReducer });
28
+ const count$ = getStateReadable(cold('s', { s: { count: 7 } })).state$.pipe(map(({ count }) => count.count));
29
+ expect(count$).toBeObservable('s', { s: 7 });
30
+ });
31
+ mtest('createAppStore builds a store namespaced under the slice name', ({ expect }) => {
32
+ const { createAppStore } = createStoreTools({ namespace: 'count', reducer: countReducer });
33
+ const { store } = createAppStore();
34
+ expect(store.state$).toBeObservable('s', { s: { count: { count: 0, label: 'none' } } });
35
+ });
36
+ mtest('createAppStore exposes the slice state on its own', ({ expect }) => {
37
+ const { createAppStore } = createStoreTools({ namespace: 'count', reducer: countReducer });
38
+ const { sliceState$ } = createAppStore();
39
+ expect(sliceState$).toBeObservable('s', { s: { count: 0, label: 'none' } });
40
+ });
41
+ mtest('a store from createAppStore reduces the events sent to it', ({ expect }) => {
42
+ const { createAppStore } = createStoreTools({ namespace: 'count', reducer: countReducer });
43
+ const { sliceState$, store } = createAppStore();
44
+ store.send(countSlice.eventCreators.increment());
45
+ expect(sliceState$).toBeObservable('s', { s: { count: 1, label: 'none' } });
46
+ });
package/package.json ADDED
@@ -0,0 +1,468 @@
1
+ {
2
+ "name": "@dungarees/store",
3
+ "engines": {
4
+ "node": ">=22.0.0"
5
+ },
6
+ "scripts": {
7
+ "type-check": "tsc --noEmit"
8
+ },
9
+ "type": "module",
10
+ "dependencies": {
11
+ "@dungarees/core": "*",
12
+ "@reduxjs/toolkit": "^2.8.2",
13
+ "rxjs": "^7.8.1"
14
+ },
15
+ "devDependencies": {
16
+ "typescript": "^5.9.3",
17
+ "vitest": "^3.0.2"
18
+ },
19
+ "author": "info@productkind.com",
20
+ "license": "MIT",
21
+ "version": "0.11.4",
22
+ "exports": {
23
+ "./effect.test.ts": {
24
+ "import": "./effect.test.js",
25
+ "types": "./effect.test.d.ts"
26
+ },
27
+ "./effect.ts": {
28
+ "import": "./effect.js",
29
+ "types": "./effect.d.ts"
30
+ },
31
+ "./error.test.ts": {
32
+ "import": "./error.test.js",
33
+ "types": "./error.test.d.ts"
34
+ },
35
+ "./error.ts": {
36
+ "import": "./error.js",
37
+ "types": "./error.d.ts"
38
+ },
39
+ "./fake.test.ts": {
40
+ "import": "./fake.test.js",
41
+ "types": "./fake.test.d.ts"
42
+ },
43
+ "./fake.ts": {
44
+ "import": "./fake.js",
45
+ "types": "./fake.d.ts"
46
+ },
47
+ "./node_modules/typescript/lib/lib.d.ts": {
48
+ "import": "./node_modules/typescript/lib/lib.d.js",
49
+ "types": "./node_modules/typescript/lib/lib.d.d.ts"
50
+ },
51
+ "./node_modules/typescript/lib/lib.decorators.d.ts": {
52
+ "import": "./node_modules/typescript/lib/lib.decorators.d.js",
53
+ "types": "./node_modules/typescript/lib/lib.decorators.d.d.ts"
54
+ },
55
+ "./node_modules/typescript/lib/lib.decorators.legacy.d.ts": {
56
+ "import": "./node_modules/typescript/lib/lib.decorators.legacy.d.js",
57
+ "types": "./node_modules/typescript/lib/lib.decorators.legacy.d.d.ts"
58
+ },
59
+ "./node_modules/typescript/lib/lib.dom.asynciterable.d.ts": {
60
+ "import": "./node_modules/typescript/lib/lib.dom.asynciterable.d.js",
61
+ "types": "./node_modules/typescript/lib/lib.dom.asynciterable.d.d.ts"
62
+ },
63
+ "./node_modules/typescript/lib/lib.dom.d.ts": {
64
+ "import": "./node_modules/typescript/lib/lib.dom.d.js",
65
+ "types": "./node_modules/typescript/lib/lib.dom.d.d.ts"
66
+ },
67
+ "./node_modules/typescript/lib/lib.dom.iterable.d.ts": {
68
+ "import": "./node_modules/typescript/lib/lib.dom.iterable.d.js",
69
+ "types": "./node_modules/typescript/lib/lib.dom.iterable.d.d.ts"
70
+ },
71
+ "./node_modules/typescript/lib/lib.es2015.collection.d.ts": {
72
+ "import": "./node_modules/typescript/lib/lib.es2015.collection.d.js",
73
+ "types": "./node_modules/typescript/lib/lib.es2015.collection.d.d.ts"
74
+ },
75
+ "./node_modules/typescript/lib/lib.es2015.core.d.ts": {
76
+ "import": "./node_modules/typescript/lib/lib.es2015.core.d.js",
77
+ "types": "./node_modules/typescript/lib/lib.es2015.core.d.d.ts"
78
+ },
79
+ "./node_modules/typescript/lib/lib.es2015.d.ts": {
80
+ "import": "./node_modules/typescript/lib/lib.es2015.d.js",
81
+ "types": "./node_modules/typescript/lib/lib.es2015.d.d.ts"
82
+ },
83
+ "./node_modules/typescript/lib/lib.es2015.generator.d.ts": {
84
+ "import": "./node_modules/typescript/lib/lib.es2015.generator.d.js",
85
+ "types": "./node_modules/typescript/lib/lib.es2015.generator.d.d.ts"
86
+ },
87
+ "./node_modules/typescript/lib/lib.es2015.iterable.d.ts": {
88
+ "import": "./node_modules/typescript/lib/lib.es2015.iterable.d.js",
89
+ "types": "./node_modules/typescript/lib/lib.es2015.iterable.d.d.ts"
90
+ },
91
+ "./node_modules/typescript/lib/lib.es2015.promise.d.ts": {
92
+ "import": "./node_modules/typescript/lib/lib.es2015.promise.d.js",
93
+ "types": "./node_modules/typescript/lib/lib.es2015.promise.d.d.ts"
94
+ },
95
+ "./node_modules/typescript/lib/lib.es2015.proxy.d.ts": {
96
+ "import": "./node_modules/typescript/lib/lib.es2015.proxy.d.js",
97
+ "types": "./node_modules/typescript/lib/lib.es2015.proxy.d.d.ts"
98
+ },
99
+ "./node_modules/typescript/lib/lib.es2015.reflect.d.ts": {
100
+ "import": "./node_modules/typescript/lib/lib.es2015.reflect.d.js",
101
+ "types": "./node_modules/typescript/lib/lib.es2015.reflect.d.d.ts"
102
+ },
103
+ "./node_modules/typescript/lib/lib.es2015.symbol.d.ts": {
104
+ "import": "./node_modules/typescript/lib/lib.es2015.symbol.d.js",
105
+ "types": "./node_modules/typescript/lib/lib.es2015.symbol.d.d.ts"
106
+ },
107
+ "./node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts": {
108
+ "import": "./node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.js",
109
+ "types": "./node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.d.ts"
110
+ },
111
+ "./node_modules/typescript/lib/lib.es2016.array.include.d.ts": {
112
+ "import": "./node_modules/typescript/lib/lib.es2016.array.include.d.js",
113
+ "types": "./node_modules/typescript/lib/lib.es2016.array.include.d.d.ts"
114
+ },
115
+ "./node_modules/typescript/lib/lib.es2016.d.ts": {
116
+ "import": "./node_modules/typescript/lib/lib.es2016.d.js",
117
+ "types": "./node_modules/typescript/lib/lib.es2016.d.d.ts"
118
+ },
119
+ "./node_modules/typescript/lib/lib.es2016.full.d.ts": {
120
+ "import": "./node_modules/typescript/lib/lib.es2016.full.d.js",
121
+ "types": "./node_modules/typescript/lib/lib.es2016.full.d.d.ts"
122
+ },
123
+ "./node_modules/typescript/lib/lib.es2016.intl.d.ts": {
124
+ "import": "./node_modules/typescript/lib/lib.es2016.intl.d.js",
125
+ "types": "./node_modules/typescript/lib/lib.es2016.intl.d.d.ts"
126
+ },
127
+ "./node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts": {
128
+ "import": "./node_modules/typescript/lib/lib.es2017.arraybuffer.d.js",
129
+ "types": "./node_modules/typescript/lib/lib.es2017.arraybuffer.d.d.ts"
130
+ },
131
+ "./node_modules/typescript/lib/lib.es2017.d.ts": {
132
+ "import": "./node_modules/typescript/lib/lib.es2017.d.js",
133
+ "types": "./node_modules/typescript/lib/lib.es2017.d.d.ts"
134
+ },
135
+ "./node_modules/typescript/lib/lib.es2017.date.d.ts": {
136
+ "import": "./node_modules/typescript/lib/lib.es2017.date.d.js",
137
+ "types": "./node_modules/typescript/lib/lib.es2017.date.d.d.ts"
138
+ },
139
+ "./node_modules/typescript/lib/lib.es2017.full.d.ts": {
140
+ "import": "./node_modules/typescript/lib/lib.es2017.full.d.js",
141
+ "types": "./node_modules/typescript/lib/lib.es2017.full.d.d.ts"
142
+ },
143
+ "./node_modules/typescript/lib/lib.es2017.intl.d.ts": {
144
+ "import": "./node_modules/typescript/lib/lib.es2017.intl.d.js",
145
+ "types": "./node_modules/typescript/lib/lib.es2017.intl.d.d.ts"
146
+ },
147
+ "./node_modules/typescript/lib/lib.es2017.object.d.ts": {
148
+ "import": "./node_modules/typescript/lib/lib.es2017.object.d.js",
149
+ "types": "./node_modules/typescript/lib/lib.es2017.object.d.d.ts"
150
+ },
151
+ "./node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts": {
152
+ "import": "./node_modules/typescript/lib/lib.es2017.sharedmemory.d.js",
153
+ "types": "./node_modules/typescript/lib/lib.es2017.sharedmemory.d.d.ts"
154
+ },
155
+ "./node_modules/typescript/lib/lib.es2017.string.d.ts": {
156
+ "import": "./node_modules/typescript/lib/lib.es2017.string.d.js",
157
+ "types": "./node_modules/typescript/lib/lib.es2017.string.d.d.ts"
158
+ },
159
+ "./node_modules/typescript/lib/lib.es2017.typedarrays.d.ts": {
160
+ "import": "./node_modules/typescript/lib/lib.es2017.typedarrays.d.js",
161
+ "types": "./node_modules/typescript/lib/lib.es2017.typedarrays.d.d.ts"
162
+ },
163
+ "./node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts": {
164
+ "import": "./node_modules/typescript/lib/lib.es2018.asyncgenerator.d.js",
165
+ "types": "./node_modules/typescript/lib/lib.es2018.asyncgenerator.d.d.ts"
166
+ },
167
+ "./node_modules/typescript/lib/lib.es2018.asynciterable.d.ts": {
168
+ "import": "./node_modules/typescript/lib/lib.es2018.asynciterable.d.js",
169
+ "types": "./node_modules/typescript/lib/lib.es2018.asynciterable.d.d.ts"
170
+ },
171
+ "./node_modules/typescript/lib/lib.es2018.d.ts": {
172
+ "import": "./node_modules/typescript/lib/lib.es2018.d.js",
173
+ "types": "./node_modules/typescript/lib/lib.es2018.d.d.ts"
174
+ },
175
+ "./node_modules/typescript/lib/lib.es2018.full.d.ts": {
176
+ "import": "./node_modules/typescript/lib/lib.es2018.full.d.js",
177
+ "types": "./node_modules/typescript/lib/lib.es2018.full.d.d.ts"
178
+ },
179
+ "./node_modules/typescript/lib/lib.es2018.intl.d.ts": {
180
+ "import": "./node_modules/typescript/lib/lib.es2018.intl.d.js",
181
+ "types": "./node_modules/typescript/lib/lib.es2018.intl.d.d.ts"
182
+ },
183
+ "./node_modules/typescript/lib/lib.es2018.promise.d.ts": {
184
+ "import": "./node_modules/typescript/lib/lib.es2018.promise.d.js",
185
+ "types": "./node_modules/typescript/lib/lib.es2018.promise.d.d.ts"
186
+ },
187
+ "./node_modules/typescript/lib/lib.es2018.regexp.d.ts": {
188
+ "import": "./node_modules/typescript/lib/lib.es2018.regexp.d.js",
189
+ "types": "./node_modules/typescript/lib/lib.es2018.regexp.d.d.ts"
190
+ },
191
+ "./node_modules/typescript/lib/lib.es2019.array.d.ts": {
192
+ "import": "./node_modules/typescript/lib/lib.es2019.array.d.js",
193
+ "types": "./node_modules/typescript/lib/lib.es2019.array.d.d.ts"
194
+ },
195
+ "./node_modules/typescript/lib/lib.es2019.d.ts": {
196
+ "import": "./node_modules/typescript/lib/lib.es2019.d.js",
197
+ "types": "./node_modules/typescript/lib/lib.es2019.d.d.ts"
198
+ },
199
+ "./node_modules/typescript/lib/lib.es2019.full.d.ts": {
200
+ "import": "./node_modules/typescript/lib/lib.es2019.full.d.js",
201
+ "types": "./node_modules/typescript/lib/lib.es2019.full.d.d.ts"
202
+ },
203
+ "./node_modules/typescript/lib/lib.es2019.intl.d.ts": {
204
+ "import": "./node_modules/typescript/lib/lib.es2019.intl.d.js",
205
+ "types": "./node_modules/typescript/lib/lib.es2019.intl.d.d.ts"
206
+ },
207
+ "./node_modules/typescript/lib/lib.es2019.object.d.ts": {
208
+ "import": "./node_modules/typescript/lib/lib.es2019.object.d.js",
209
+ "types": "./node_modules/typescript/lib/lib.es2019.object.d.d.ts"
210
+ },
211
+ "./node_modules/typescript/lib/lib.es2019.string.d.ts": {
212
+ "import": "./node_modules/typescript/lib/lib.es2019.string.d.js",
213
+ "types": "./node_modules/typescript/lib/lib.es2019.string.d.d.ts"
214
+ },
215
+ "./node_modules/typescript/lib/lib.es2019.symbol.d.ts": {
216
+ "import": "./node_modules/typescript/lib/lib.es2019.symbol.d.js",
217
+ "types": "./node_modules/typescript/lib/lib.es2019.symbol.d.d.ts"
218
+ },
219
+ "./node_modules/typescript/lib/lib.es2020.bigint.d.ts": {
220
+ "import": "./node_modules/typescript/lib/lib.es2020.bigint.d.js",
221
+ "types": "./node_modules/typescript/lib/lib.es2020.bigint.d.d.ts"
222
+ },
223
+ "./node_modules/typescript/lib/lib.es2020.d.ts": {
224
+ "import": "./node_modules/typescript/lib/lib.es2020.d.js",
225
+ "types": "./node_modules/typescript/lib/lib.es2020.d.d.ts"
226
+ },
227
+ "./node_modules/typescript/lib/lib.es2020.date.d.ts": {
228
+ "import": "./node_modules/typescript/lib/lib.es2020.date.d.js",
229
+ "types": "./node_modules/typescript/lib/lib.es2020.date.d.d.ts"
230
+ },
231
+ "./node_modules/typescript/lib/lib.es2020.full.d.ts": {
232
+ "import": "./node_modules/typescript/lib/lib.es2020.full.d.js",
233
+ "types": "./node_modules/typescript/lib/lib.es2020.full.d.d.ts"
234
+ },
235
+ "./node_modules/typescript/lib/lib.es2020.intl.d.ts": {
236
+ "import": "./node_modules/typescript/lib/lib.es2020.intl.d.js",
237
+ "types": "./node_modules/typescript/lib/lib.es2020.intl.d.d.ts"
238
+ },
239
+ "./node_modules/typescript/lib/lib.es2020.number.d.ts": {
240
+ "import": "./node_modules/typescript/lib/lib.es2020.number.d.js",
241
+ "types": "./node_modules/typescript/lib/lib.es2020.number.d.d.ts"
242
+ },
243
+ "./node_modules/typescript/lib/lib.es2020.promise.d.ts": {
244
+ "import": "./node_modules/typescript/lib/lib.es2020.promise.d.js",
245
+ "types": "./node_modules/typescript/lib/lib.es2020.promise.d.d.ts"
246
+ },
247
+ "./node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts": {
248
+ "import": "./node_modules/typescript/lib/lib.es2020.sharedmemory.d.js",
249
+ "types": "./node_modules/typescript/lib/lib.es2020.sharedmemory.d.d.ts"
250
+ },
251
+ "./node_modules/typescript/lib/lib.es2020.string.d.ts": {
252
+ "import": "./node_modules/typescript/lib/lib.es2020.string.d.js",
253
+ "types": "./node_modules/typescript/lib/lib.es2020.string.d.d.ts"
254
+ },
255
+ "./node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts": {
256
+ "import": "./node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.js",
257
+ "types": "./node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.d.ts"
258
+ },
259
+ "./node_modules/typescript/lib/lib.es2021.d.ts": {
260
+ "import": "./node_modules/typescript/lib/lib.es2021.d.js",
261
+ "types": "./node_modules/typescript/lib/lib.es2021.d.d.ts"
262
+ },
263
+ "./node_modules/typescript/lib/lib.es2021.full.d.ts": {
264
+ "import": "./node_modules/typescript/lib/lib.es2021.full.d.js",
265
+ "types": "./node_modules/typescript/lib/lib.es2021.full.d.d.ts"
266
+ },
267
+ "./node_modules/typescript/lib/lib.es2021.intl.d.ts": {
268
+ "import": "./node_modules/typescript/lib/lib.es2021.intl.d.js",
269
+ "types": "./node_modules/typescript/lib/lib.es2021.intl.d.d.ts"
270
+ },
271
+ "./node_modules/typescript/lib/lib.es2021.promise.d.ts": {
272
+ "import": "./node_modules/typescript/lib/lib.es2021.promise.d.js",
273
+ "types": "./node_modules/typescript/lib/lib.es2021.promise.d.d.ts"
274
+ },
275
+ "./node_modules/typescript/lib/lib.es2021.string.d.ts": {
276
+ "import": "./node_modules/typescript/lib/lib.es2021.string.d.js",
277
+ "types": "./node_modules/typescript/lib/lib.es2021.string.d.d.ts"
278
+ },
279
+ "./node_modules/typescript/lib/lib.es2021.weakref.d.ts": {
280
+ "import": "./node_modules/typescript/lib/lib.es2021.weakref.d.js",
281
+ "types": "./node_modules/typescript/lib/lib.es2021.weakref.d.d.ts"
282
+ },
283
+ "./node_modules/typescript/lib/lib.es2022.array.d.ts": {
284
+ "import": "./node_modules/typescript/lib/lib.es2022.array.d.js",
285
+ "types": "./node_modules/typescript/lib/lib.es2022.array.d.d.ts"
286
+ },
287
+ "./node_modules/typescript/lib/lib.es2022.d.ts": {
288
+ "import": "./node_modules/typescript/lib/lib.es2022.d.js",
289
+ "types": "./node_modules/typescript/lib/lib.es2022.d.d.ts"
290
+ },
291
+ "./node_modules/typescript/lib/lib.es2022.error.d.ts": {
292
+ "import": "./node_modules/typescript/lib/lib.es2022.error.d.js",
293
+ "types": "./node_modules/typescript/lib/lib.es2022.error.d.d.ts"
294
+ },
295
+ "./node_modules/typescript/lib/lib.es2022.full.d.ts": {
296
+ "import": "./node_modules/typescript/lib/lib.es2022.full.d.js",
297
+ "types": "./node_modules/typescript/lib/lib.es2022.full.d.d.ts"
298
+ },
299
+ "./node_modules/typescript/lib/lib.es2022.intl.d.ts": {
300
+ "import": "./node_modules/typescript/lib/lib.es2022.intl.d.js",
301
+ "types": "./node_modules/typescript/lib/lib.es2022.intl.d.d.ts"
302
+ },
303
+ "./node_modules/typescript/lib/lib.es2022.object.d.ts": {
304
+ "import": "./node_modules/typescript/lib/lib.es2022.object.d.js",
305
+ "types": "./node_modules/typescript/lib/lib.es2022.object.d.d.ts"
306
+ },
307
+ "./node_modules/typescript/lib/lib.es2022.regexp.d.ts": {
308
+ "import": "./node_modules/typescript/lib/lib.es2022.regexp.d.js",
309
+ "types": "./node_modules/typescript/lib/lib.es2022.regexp.d.d.ts"
310
+ },
311
+ "./node_modules/typescript/lib/lib.es2022.string.d.ts": {
312
+ "import": "./node_modules/typescript/lib/lib.es2022.string.d.js",
313
+ "types": "./node_modules/typescript/lib/lib.es2022.string.d.d.ts"
314
+ },
315
+ "./node_modules/typescript/lib/lib.es2023.array.d.ts": {
316
+ "import": "./node_modules/typescript/lib/lib.es2023.array.d.js",
317
+ "types": "./node_modules/typescript/lib/lib.es2023.array.d.d.ts"
318
+ },
319
+ "./node_modules/typescript/lib/lib.es2023.collection.d.ts": {
320
+ "import": "./node_modules/typescript/lib/lib.es2023.collection.d.js",
321
+ "types": "./node_modules/typescript/lib/lib.es2023.collection.d.d.ts"
322
+ },
323
+ "./node_modules/typescript/lib/lib.es2023.d.ts": {
324
+ "import": "./node_modules/typescript/lib/lib.es2023.d.js",
325
+ "types": "./node_modules/typescript/lib/lib.es2023.d.d.ts"
326
+ },
327
+ "./node_modules/typescript/lib/lib.es2023.full.d.ts": {
328
+ "import": "./node_modules/typescript/lib/lib.es2023.full.d.js",
329
+ "types": "./node_modules/typescript/lib/lib.es2023.full.d.d.ts"
330
+ },
331
+ "./node_modules/typescript/lib/lib.es2023.intl.d.ts": {
332
+ "import": "./node_modules/typescript/lib/lib.es2023.intl.d.js",
333
+ "types": "./node_modules/typescript/lib/lib.es2023.intl.d.d.ts"
334
+ },
335
+ "./node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts": {
336
+ "import": "./node_modules/typescript/lib/lib.es2024.arraybuffer.d.js",
337
+ "types": "./node_modules/typescript/lib/lib.es2024.arraybuffer.d.d.ts"
338
+ },
339
+ "./node_modules/typescript/lib/lib.es2024.collection.d.ts": {
340
+ "import": "./node_modules/typescript/lib/lib.es2024.collection.d.js",
341
+ "types": "./node_modules/typescript/lib/lib.es2024.collection.d.d.ts"
342
+ },
343
+ "./node_modules/typescript/lib/lib.es2024.d.ts": {
344
+ "import": "./node_modules/typescript/lib/lib.es2024.d.js",
345
+ "types": "./node_modules/typescript/lib/lib.es2024.d.d.ts"
346
+ },
347
+ "./node_modules/typescript/lib/lib.es2024.full.d.ts": {
348
+ "import": "./node_modules/typescript/lib/lib.es2024.full.d.js",
349
+ "types": "./node_modules/typescript/lib/lib.es2024.full.d.d.ts"
350
+ },
351
+ "./node_modules/typescript/lib/lib.es2024.object.d.ts": {
352
+ "import": "./node_modules/typescript/lib/lib.es2024.object.d.js",
353
+ "types": "./node_modules/typescript/lib/lib.es2024.object.d.d.ts"
354
+ },
355
+ "./node_modules/typescript/lib/lib.es2024.promise.d.ts": {
356
+ "import": "./node_modules/typescript/lib/lib.es2024.promise.d.js",
357
+ "types": "./node_modules/typescript/lib/lib.es2024.promise.d.d.ts"
358
+ },
359
+ "./node_modules/typescript/lib/lib.es2024.regexp.d.ts": {
360
+ "import": "./node_modules/typescript/lib/lib.es2024.regexp.d.js",
361
+ "types": "./node_modules/typescript/lib/lib.es2024.regexp.d.d.ts"
362
+ },
363
+ "./node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts": {
364
+ "import": "./node_modules/typescript/lib/lib.es2024.sharedmemory.d.js",
365
+ "types": "./node_modules/typescript/lib/lib.es2024.sharedmemory.d.d.ts"
366
+ },
367
+ "./node_modules/typescript/lib/lib.es2024.string.d.ts": {
368
+ "import": "./node_modules/typescript/lib/lib.es2024.string.d.js",
369
+ "types": "./node_modules/typescript/lib/lib.es2024.string.d.d.ts"
370
+ },
371
+ "./node_modules/typescript/lib/lib.es5.d.ts": {
372
+ "import": "./node_modules/typescript/lib/lib.es5.d.js",
373
+ "types": "./node_modules/typescript/lib/lib.es5.d.d.ts"
374
+ },
375
+ "./node_modules/typescript/lib/lib.es6.d.ts": {
376
+ "import": "./node_modules/typescript/lib/lib.es6.d.js",
377
+ "types": "./node_modules/typescript/lib/lib.es6.d.d.ts"
378
+ },
379
+ "./node_modules/typescript/lib/lib.esnext.array.d.ts": {
380
+ "import": "./node_modules/typescript/lib/lib.esnext.array.d.js",
381
+ "types": "./node_modules/typescript/lib/lib.esnext.array.d.d.ts"
382
+ },
383
+ "./node_modules/typescript/lib/lib.esnext.collection.d.ts": {
384
+ "import": "./node_modules/typescript/lib/lib.esnext.collection.d.js",
385
+ "types": "./node_modules/typescript/lib/lib.esnext.collection.d.d.ts"
386
+ },
387
+ "./node_modules/typescript/lib/lib.esnext.d.ts": {
388
+ "import": "./node_modules/typescript/lib/lib.esnext.d.js",
389
+ "types": "./node_modules/typescript/lib/lib.esnext.d.d.ts"
390
+ },
391
+ "./node_modules/typescript/lib/lib.esnext.decorators.d.ts": {
392
+ "import": "./node_modules/typescript/lib/lib.esnext.decorators.d.js",
393
+ "types": "./node_modules/typescript/lib/lib.esnext.decorators.d.d.ts"
394
+ },
395
+ "./node_modules/typescript/lib/lib.esnext.disposable.d.ts": {
396
+ "import": "./node_modules/typescript/lib/lib.esnext.disposable.d.js",
397
+ "types": "./node_modules/typescript/lib/lib.esnext.disposable.d.d.ts"
398
+ },
399
+ "./node_modules/typescript/lib/lib.esnext.error.d.ts": {
400
+ "import": "./node_modules/typescript/lib/lib.esnext.error.d.js",
401
+ "types": "./node_modules/typescript/lib/lib.esnext.error.d.d.ts"
402
+ },
403
+ "./node_modules/typescript/lib/lib.esnext.float16.d.ts": {
404
+ "import": "./node_modules/typescript/lib/lib.esnext.float16.d.js",
405
+ "types": "./node_modules/typescript/lib/lib.esnext.float16.d.d.ts"
406
+ },
407
+ "./node_modules/typescript/lib/lib.esnext.full.d.ts": {
408
+ "import": "./node_modules/typescript/lib/lib.esnext.full.d.js",
409
+ "types": "./node_modules/typescript/lib/lib.esnext.full.d.d.ts"
410
+ },
411
+ "./node_modules/typescript/lib/lib.esnext.intl.d.ts": {
412
+ "import": "./node_modules/typescript/lib/lib.esnext.intl.d.js",
413
+ "types": "./node_modules/typescript/lib/lib.esnext.intl.d.d.ts"
414
+ },
415
+ "./node_modules/typescript/lib/lib.esnext.iterator.d.ts": {
416
+ "import": "./node_modules/typescript/lib/lib.esnext.iterator.d.js",
417
+ "types": "./node_modules/typescript/lib/lib.esnext.iterator.d.d.ts"
418
+ },
419
+ "./node_modules/typescript/lib/lib.esnext.promise.d.ts": {
420
+ "import": "./node_modules/typescript/lib/lib.esnext.promise.d.js",
421
+ "types": "./node_modules/typescript/lib/lib.esnext.promise.d.d.ts"
422
+ },
423
+ "./node_modules/typescript/lib/lib.esnext.sharedmemory.d.ts": {
424
+ "import": "./node_modules/typescript/lib/lib.esnext.sharedmemory.d.js",
425
+ "types": "./node_modules/typescript/lib/lib.esnext.sharedmemory.d.d.ts"
426
+ },
427
+ "./node_modules/typescript/lib/lib.scripthost.d.ts": {
428
+ "import": "./node_modules/typescript/lib/lib.scripthost.d.js",
429
+ "types": "./node_modules/typescript/lib/lib.scripthost.d.d.ts"
430
+ },
431
+ "./node_modules/typescript/lib/lib.webworker.asynciterable.d.ts": {
432
+ "import": "./node_modules/typescript/lib/lib.webworker.asynciterable.d.js",
433
+ "types": "./node_modules/typescript/lib/lib.webworker.asynciterable.d.d.ts"
434
+ },
435
+ "./node_modules/typescript/lib/lib.webworker.d.ts": {
436
+ "import": "./node_modules/typescript/lib/lib.webworker.d.js",
437
+ "types": "./node_modules/typescript/lib/lib.webworker.d.d.ts"
438
+ },
439
+ "./node_modules/typescript/lib/lib.webworker.importscripts.d.ts": {
440
+ "import": "./node_modules/typescript/lib/lib.webworker.importscripts.d.js",
441
+ "types": "./node_modules/typescript/lib/lib.webworker.importscripts.d.d.ts"
442
+ },
443
+ "./node_modules/typescript/lib/lib.webworker.iterable.d.ts": {
444
+ "import": "./node_modules/typescript/lib/lib.webworker.iterable.d.js",
445
+ "types": "./node_modules/typescript/lib/lib.webworker.iterable.d.d.ts"
446
+ },
447
+ "./node_modules/typescript/lib/tsserverlibrary.d.ts": {
448
+ "import": "./node_modules/typescript/lib/tsserverlibrary.d.js",
449
+ "types": "./node_modules/typescript/lib/tsserverlibrary.d.d.ts"
450
+ },
451
+ "./node_modules/typescript/lib/typescript.d.ts": {
452
+ "import": "./node_modules/typescript/lib/typescript.d.js",
453
+ "types": "./node_modules/typescript/lib/typescript.d.d.ts"
454
+ },
455
+ "./service.test.ts": {
456
+ "import": "./service.test.js",
457
+ "types": "./service.test.d.ts"
458
+ },
459
+ "./service.ts": {
460
+ "import": "./service.js",
461
+ "types": "./service.d.ts"
462
+ },
463
+ "./type.ts": {
464
+ "import": "./type.js",
465
+ "types": "./type.d.ts"
466
+ }
467
+ }
468
+ }
package/service.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ import type { CaseReducersObject, ReducersObject, Store, StoreImportExport, StoreSlice, StoreSliceConfig } from './type.ts';
2
+ import type { DomainEvent } from '@dungarees/core/event.ts';
3
+ import type { JsonObject } from '@dungarees/core/type-util.ts';
4
+ export declare const createStore: <STATE extends JsonObject, EVENT extends DomainEvent>(reducers: ReducersObject<STATE, EVENT>, config?: {
5
+ rehydrateMode: boolean;
6
+ }) => Store<STATE, EVENT> & StoreImportExport<STATE>;
7
+ export declare const createStoreSlice: <STATE, CASE_REDUCERS extends CaseReducersObject<STATE>, NAMESPACE extends string = string>(config: StoreSliceConfig<STATE, CASE_REDUCERS, NAMESPACE>) => StoreSlice<STATE, CASE_REDUCERS, NAMESPACE>;
8
+ export declare const identityReducer: <STATE, EVENT extends DomainEvent>(state: STATE, _: EVENT) => STATE;
package/service.js ADDED
@@ -0,0 +1,99 @@
1
+ import { capitalize } from '@dungarees/core/util.ts';
2
+ import { combineReducers, configureStore, createSlice, isAction, } from '@reduxjs/toolkit';
3
+ import { Observable, of, Subject } from 'rxjs';
4
+ import { concatMap, mergeAll } from 'rxjs/operators';
5
+ const IMPORT_TYPE = 'IMPORT';
6
+ // redux hands middleware an `unknown` action. isAction checks the part that matters — an object
7
+ // with a string `type` — and the index signature UnknownAction adds on top is vacuously true of
8
+ // any object, since reading an absent key yields undefined.
9
+ const isUnknownAction = (action) => isAction(action);
10
+ export const createStore = (reducers, config = { rehydrateMode: false }) => {
11
+ // combineReducers is typed against the state it rebuilds key by key from the reducer map and the
12
+ // action union it collects from them, neither of which TypeScript can recognise as the STATE and
13
+ // EVENT that are still generic here. Cast once, at this boundary, rather than at each call.
14
+ const combinedReducer = combineReducers(reducers);
15
+ const reducerWithImport = (state, action) => {
16
+ if (action.type === IMPORT_TYPE) {
17
+ // The imported state is whatever exportState produced, and STATE is still generic here, so
18
+ // there is no shape to check it against. importState below is its only source.
19
+ return action['payload'];
20
+ }
21
+ return combinedReducer(state, action);
22
+ };
23
+ const eventsBeforeImport = [];
24
+ let rehydrationComplete = false;
25
+ // Buffers everything that arrives before the imported state and replays it on top, so a store
26
+ // rehydrated from storage does not discard what happened while that load was in flight.
27
+ const rehydrationMiddleware = (store) => (next) => (action) => {
28
+ if (!config.rehydrateMode || rehydrationComplete || !isUnknownAction(action)) {
29
+ return next(action);
30
+ }
31
+ if (action.type !== IMPORT_TYPE) {
32
+ eventsBeforeImport.push(action);
33
+ return next(action);
34
+ }
35
+ rehydrationComplete = true;
36
+ const imported = next(action);
37
+ eventsBeforeImport.forEach((event) => store.dispatch(event));
38
+ eventsBeforeImport.length = 0;
39
+ return imported;
40
+ };
41
+ const store = configureStore({
42
+ reducer: reducerWithImport,
43
+ middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(rehydrationMiddleware),
44
+ });
45
+ const event$ = new Subject();
46
+ const event$$ = new Subject();
47
+ const eventAfterEffects$ = new Subject();
48
+ event$$.pipe(mergeAll()).subscribe((event) => {
49
+ store.dispatch(event);
50
+ eventAfterEffects$.next(event);
51
+ });
52
+ event$$.next(event$);
53
+ const state$ = new Observable((subscriber) => {
54
+ subscriber.next(store.getState());
55
+ store.subscribe(() => {
56
+ subscriber.next(store.getState());
57
+ });
58
+ });
59
+ return {
60
+ state$,
61
+ send: (event) => {
62
+ event$.next(event);
63
+ },
64
+ registerEffect: (effect) => {
65
+ event$$.next(eventAfterEffects$.pipe(concatMap((event) => effect(of(event), state$))));
66
+ },
67
+ registerSourceEffect: (sourceEffect) => {
68
+ event$$.next(sourceEffect(state$));
69
+ },
70
+ exportState: () => store.getState(),
71
+ importState: (state) => {
72
+ store.dispatch({ type: IMPORT_TYPE, payload: state });
73
+ },
74
+ };
75
+ };
76
+ export const createStoreSlice = (config) => {
77
+ const { name, reducer, actions } = createSlice({
78
+ name: config.name,
79
+ initialState: config.initialState,
80
+ // Our case reducers return the next state, which createSlice accepts, but its parameter type is
81
+ // written in terms of immer's Draft and cannot be satisfied from outside.
82
+ reducers: config.reducers,
83
+ });
84
+ return {
85
+ name,
86
+ reducer,
87
+ // The action creators already have the right runtime shape; only immer's types stand between
88
+ // them and StoreEventCreators, and restating those would duplicate redux-toolkit.
89
+ eventCreators: actions,
90
+ ...createStateMapper(config.name),
91
+ };
92
+ };
93
+ // The mapper's key is built from the namespace at runtime, and no type-safe construction of a
94
+ // dynamically keyed object exists — the same limitation core's createEventCreators works around.
95
+ const createStateMapper = (namespace) => {
96
+ const readSlice = ({ [namespace]: state }) => state;
97
+ return { [`stateTo${capitalize(namespace)}`]: readSlice };
98
+ };
99
+ export const identityReducer = (state, _) => state;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,180 @@
1
+ import { filterByType } from './effect.js';
2
+ import { createStore, createStoreSlice, identityReducer } from './service.js';
3
+ import { mtest } from '@dungarees/core/marbles-vitest.ts';
4
+ import { of } from 'rxjs';
5
+ import { catchError, map, mergeMap, take } from 'rxjs/operators';
6
+ import { expect, expectTypeOf, test } from 'vitest';
7
+ const countReducer = (state = 0, event) => {
8
+ switch (event.type) {
9
+ case 'count/incrementAmount':
10
+ return state + event.payload;
11
+ case 'count/increment':
12
+ return state + 1;
13
+ default:
14
+ return state;
15
+ }
16
+ };
17
+ const createCountSlice = () => createStoreSlice({
18
+ name: 'count',
19
+ initialState: 0,
20
+ reducers: {
21
+ increment: (state, _) => state + 1,
22
+ incrementAmount: (state, event) => state + event.payload,
23
+ },
24
+ });
25
+ const createAppendSlice = () => createStoreSlice({
26
+ name: 'append',
27
+ initialState: '',
28
+ reducers: {
29
+ appendA: (state, _) => state + 'A',
30
+ },
31
+ });
32
+ mtest('a new store emits the initial state its reducers declare', ({ expect }) => {
33
+ const store = createStore({ count: countReducer });
34
+ expect(store.state$).toBeObservable('0', { 0: { count: 0 } });
35
+ });
36
+ mtest('a store reduces an event that was sent to it', ({ expect }) => {
37
+ const store = createStore({ count: countReducer });
38
+ store.send({ type: 'count/increment', payload: undefined });
39
+ expect(store.state$).toBeObservable('1', { 1: { count: 1 } });
40
+ });
41
+ mtest('a store reduces the payload of an event that carries one', ({ expect }) => {
42
+ const store = createStore({ count: countReducer });
43
+ store.send({ type: 'count/incrementAmount', payload: 5 });
44
+ expect(store.state$).toBeObservable('5', { 5: { count: 5 } });
45
+ });
46
+ mtest('a store ignores an event none of its reducers handle', ({ expect }) => {
47
+ const store = createStore({ count: countReducer });
48
+ store.send({ type: 'count/start', payload: undefined });
49
+ expect(store.state$).toBeObservable('0', { 0: { count: 0 } });
50
+ });
51
+ test('createStoreSlice namespaces the event type under the slice name', () => {
52
+ const slice = createCountSlice();
53
+ expect(slice.name).toBe('count');
54
+ expect(slice.eventCreators.increment()).toEqual({
55
+ type: 'count/increment',
56
+ payload: undefined,
57
+ });
58
+ });
59
+ test('createStoreSlice builds a creator that carries its payload', () => {
60
+ const slice = createCountSlice();
61
+ expect(slice.eventCreators.incrementAmount(5)).toEqual({
62
+ type: 'count/incrementAmount',
63
+ payload: 5,
64
+ });
65
+ });
66
+ test('createStoreSlice produces a reducer that handles its own events', () => {
67
+ const slice = createCountSlice();
68
+ const reducer = slice.reducer;
69
+ expect(reducer(0, slice.eventCreators.increment())).toBe(1);
70
+ expect(reducer(0, slice.eventCreators.incrementAmount(5))).toBe(5);
71
+ });
72
+ test('createStoreSlice provides a mapper that reads its slice out of the whole state', () => {
73
+ const slice = createCountSlice();
74
+ expect(slice.stateToCount({ count: 3 })).toBe(3);
75
+ });
76
+ test('createStoreSlice types a no-payload creator as taking no argument', () => {
77
+ const slice = createCountSlice();
78
+ expectTypeOf(slice.eventCreators.increment).toEqualTypeOf();
79
+ });
80
+ test('createStoreSlice types a creator from the event its reducer declares', () => {
81
+ const slice = createCountSlice();
82
+ expectTypeOf(slice.eventCreators.incrementAmount).toEqualTypeOf();
83
+ });
84
+ test('createStoreSlice rejects a payload of the wrong type', () => {
85
+ const slice = createCountSlice();
86
+ // @ts-expect-error the incrementAmount payload is a number, not a string
87
+ slice.eventCreators.incrementAmount('5');
88
+ });
89
+ mtest('a store combines the slices it was built from', ({ expect }) => {
90
+ const countSlice = createCountSlice();
91
+ const appendSlice = createAppendSlice();
92
+ const store = createStore({
93
+ count: countSlice.reducer,
94
+ append: appendSlice.reducer,
95
+ });
96
+ store.send(countSlice.eventCreators.increment());
97
+ store.send(appendSlice.eventCreators.appendA());
98
+ expect(store.state$).toBeObservable('1', { 1: { count: 1, append: 'A' } });
99
+ });
100
+ mtest('a registered effect turns one event into another', ({ expect }) => {
101
+ const store = createStore({ count: countReducer });
102
+ store.registerEffect((event$) => event$.pipe(filterByType('count/increment'), map(() => ({ type: 'count/incrementAmount', payload: 2 }))));
103
+ store.send({ type: 'count/increment', payload: undefined });
104
+ expect(store.state$).toBeObservable('3', { 3: { count: 3 } });
105
+ });
106
+ mtest('an effect keeps running after it has recovered from an error', ({ expect }) => {
107
+ const store = createStore({ count: countReducer });
108
+ store.registerEffect((event$) => event$.pipe(filterByType('count/incrementAmount'), map((event) => {
109
+ if (event.payload === 0) {
110
+ throw new Error('error');
111
+ }
112
+ return event;
113
+ }), catchError(() => of({ type: 'count/increment', payload: undefined }))));
114
+ store.send({ type: 'count/incrementAmount', payload: 0 });
115
+ store.send({ type: 'count/incrementAmount', payload: 0 });
116
+ expect(store.state$).toBeObservable('2', { 2: { count: 2 } });
117
+ });
118
+ mtest('an event emitted by one effect feeds the next', ({ expect }) => {
119
+ const store = createStore({ count: countReducer });
120
+ store.registerEffect((event$) => event$.pipe(filterByType('count/start'), map(() => ({ type: 'count/incrementTwo', payload: undefined }))));
121
+ store.registerEffect((event$) => event$.pipe(filterByType('count/incrementTwo'), map(() => ({ type: 'count/incrementAmount', payload: 2 }))));
122
+ store.send({ type: 'count/start', payload: undefined });
123
+ expect(store.state$).toBeObservable('2', { 2: { count: 2 } });
124
+ });
125
+ mtest('an effect can read the current state', ({ expect }) => {
126
+ const store = createStore({
127
+ count: (state = 1, event) => countReducer(state, event),
128
+ });
129
+ store.registerEffect((event$, state$) => event$.pipe(filterByType('count/start'), mergeMap(() => state$), map(({ count }) => ({ type: 'count/incrementAmount', payload: count }))));
130
+ store.send({ type: 'count/start', payload: undefined });
131
+ expect(store.state$).toBeObservable('2', { 2: { count: 2 } });
132
+ });
133
+ mtest('a source effect emits events of its own accord', ({ expect }) => {
134
+ const store = createStore({
135
+ count: (state = 2, event) => countReducer(state, event),
136
+ });
137
+ store.registerSourceEffect((state$) => {
138
+ const stateLimit$ = state$.pipe(take(1));
139
+ return of(0, 1).pipe(mergeMap((count) => stateLimit$.pipe(map((state) => state.count + count))), map((count) => ({ type: 'count/incrementAmount', payload: count })));
140
+ });
141
+ store.send({ type: 'count/start', payload: undefined });
142
+ store.send({ type: 'count/start', payload: undefined });
143
+ expect(store.state$).toBeObservable('9', { 9: { count: 9 } });
144
+ });
145
+ test('exportState hands back the state as a plain object', () => {
146
+ const countSlice = createCountSlice();
147
+ const appendSlice = createAppendSlice();
148
+ const store = createStore({
149
+ count: countSlice.reducer,
150
+ append: appendSlice.reducer,
151
+ });
152
+ store.send(countSlice.eventCreators.increment());
153
+ store.send(appendSlice.eventCreators.appendA());
154
+ expect(store.exportState()).toEqual({ count: 1, append: 'A' });
155
+ });
156
+ mtest('importState replaces the whole state, and events reduce on top of it', ({ expect }) => {
157
+ const countSlice = createCountSlice();
158
+ const appendSlice = createAppendSlice();
159
+ const store = createStore({
160
+ count: countSlice.reducer,
161
+ append: appendSlice.reducer,
162
+ });
163
+ store.importState({ count: 2, append: 'B' });
164
+ store.send(countSlice.eventCreators.increment());
165
+ store.send(appendSlice.eventCreators.appendA());
166
+ expect(store.state$).toBeObservable('1', { 1: { count: 3, append: 'BA' } });
167
+ });
168
+ mtest('in rehydrate mode events sent before the import are replayed after it', ({ expect }) => {
169
+ const countSlice = createCountSlice();
170
+ const appendSlice = createAppendSlice();
171
+ const store = createStore({ count: countSlice.reducer, append: appendSlice.reducer }, { rehydrateMode: true });
172
+ store.send(countSlice.eventCreators.increment());
173
+ store.send(appendSlice.eventCreators.appendA());
174
+ store.importState({ count: 2, append: 'B' });
175
+ expect(store.state$).toBeObservable('1', { 1: { count: 3, append: 'BA' } });
176
+ });
177
+ test('identityReducer hands back the very state it was given', () => {
178
+ const state = { count: 1 };
179
+ expect(identityReducer(state, { type: 'count/increment', payload: undefined })).toBe(state);
180
+ });
package/type.d.ts ADDED
@@ -0,0 +1,61 @@
1
+ import type { DomainEvent } from '@dungarees/core/event.ts';
2
+ import type { JsonObject, ObjectWithStringLiteralKey, Serializable } from '@dungarees/core/type-util.ts';
3
+ import type { Observable } from 'rxjs';
4
+ export type Store<ALL_STATE, ALL_EVENT extends DomainEvent> = StateReadable<ALL_STATE> & EventReceiver<ALL_EVENT> & EffectRegistry<ALL_STATE, ALL_EVENT>;
5
+ export type StoreImportExport<ALL_STATE> = {
6
+ importState: (state: ALL_STATE) => void;
7
+ exportState: () => ALL_STATE;
8
+ };
9
+ export type StoreWithImport<ALL_STATE, ALL_EVENT extends DomainEvent> = Store<ALL_STATE, ALL_EVENT> & StoreImportExport<ALL_STATE>;
10
+ export type StateReadable<ALL_STATE> = {
11
+ state$: Observable<ALL_STATE>;
12
+ };
13
+ export type EventReceiver<ALL_EVENT extends DomainEvent> = {
14
+ send: (event: ALL_EVENT) => void;
15
+ };
16
+ export type EffectRegistry<ALL_STATE, ALL_EVENT extends DomainEvent> = {
17
+ registerEffect: <EVENT_OUT extends ALL_EVENT>(effect: EffectFunction<ALL_STATE, ALL_EVENT, EVENT_OUT>) => void;
18
+ registerSourceEffect: <EVENT_OUT extends ALL_EVENT>(sourceEffect: SourceEffectFunction<ALL_STATE, EVENT_OUT>) => void;
19
+ };
20
+ export type Reducer<STATE, EVENT extends DomainEvent = DomainEvent> = (state: STATE | undefined, event: EVENT) => STATE;
21
+ export type ReducersObject<ALL_STATE extends JsonObject, ALL_EVENT extends DomainEvent> = {
22
+ [KEY in keyof ALL_STATE]: Reducer<ALL_STATE[KEY], ALL_EVENT>;
23
+ };
24
+ export type NamespacedState<SLICE> = SLICE extends {
25
+ name: infer NAMESPACE extends string;
26
+ reducer: Reducer<infer STATE>;
27
+ } ? ObjectWithStringLiteralKey<NAMESPACE, STATE> : never;
28
+ export type NamespacedStoreEvent<NAMESPACE extends string, SUB_TYPE extends string, PAYLOAD extends Serializable = undefined> = DomainEvent<`${NAMESPACE}/${SUB_TYPE}`, PAYLOAD>;
29
+ export type CaseReducer<STATE, EVENT extends DomainEvent = DomainEvent> = (state: STATE, event: EVENT) => STATE;
30
+ export type CaseReducersObject<STATE> = Record<string, CaseReducer<STATE, never>>;
31
+ export type StoreSliceConfig<STATE, CASE_REDUCERS extends CaseReducersObject<STATE>, NAMESPACE extends string = string> = {
32
+ name: NAMESPACE;
33
+ initialState: STATE;
34
+ reducers: CASE_REDUCERS;
35
+ };
36
+ export type StoreEventCreator<EVENT extends DomainEvent> = EVENT extends {
37
+ type: infer TYPE extends string;
38
+ payload: infer PAYLOAD extends Serializable;
39
+ } ? undefined extends PAYLOAD ? () => EVENT : (payload: PAYLOAD) => DomainEvent<TYPE, PAYLOAD> : never;
40
+ export type StoreEventCreators<STATE, CASE_REDUCERS extends CaseReducersObject<STATE>> = CASE_REDUCERS extends Record<infer KEY extends string, CaseReducer<STATE, never>> ? {
41
+ [K in KEY]: CASE_REDUCERS[K] extends (state: never, event: infer EVENT extends DomainEvent) => unknown ? StoreEventCreator<EVENT> : StoreEventCreator<DomainEvent>;
42
+ } : never;
43
+ export type StoreSlice<STATE = unknown, CASE_REDUCERS extends CaseReducersObject<STATE> = CaseReducersObject<STATE>, NAMESPACE extends string = string> = {
44
+ name: NAMESPACE;
45
+ reducer: Reducer<STATE>;
46
+ eventCreators: StoreEventCreators<STATE, CASE_REDUCERS>;
47
+ } & StateMapper<STATE, NAMESPACE>;
48
+ export type StateMapper<STATE, NAMESPACE extends string> = {
49
+ [KEY in keyof ObjectWithStringLiteralKey<NAMESPACE, STATE> as `stateTo${Capitalize<KEY>}`]: (allState: ObjectWithStringLiteralKey<NAMESPACE, STATE>) => STATE;
50
+ };
51
+ export type StoreError = {
52
+ message: string;
53
+ stack: string;
54
+ };
55
+ export type EffectFunction<STATE, EVENT_IN extends DomainEvent, EVENT_OUT extends DomainEvent = EVENT_IN> = (event$: Observable<EVENT_IN>, state$: Observable<STATE>) => Observable<EVENT_OUT>;
56
+ export type SourceEffectFunction<STATE, EVENT_OUT extends DomainEvent> = (state$: Observable<STATE>) => Observable<EVENT_OUT>;
57
+ export type Effect<STATE, EVENT_IN extends DomainEvent, EFFECTS extends string, SOURCE_EFFECTS extends string = never, EVENT_OUT extends DomainEvent = EVENT_IN> = {
58
+ [KEY in EFFECTS]: EffectFunction<STATE, EVENT_IN, EVENT_OUT>;
59
+ } & {
60
+ [KEY in SOURCE_EFFECTS]: SourceEffectFunction<STATE, EVENT_OUT>;
61
+ };
package/type.js ADDED
@@ -0,0 +1 @@
1
+ export {};