@angular-architects/ngrx-toolkit 19.2.3 → 19.4.0
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/eslint.config.cjs +43 -0
- package/jest.config.ts +22 -0
- package/ng-package.json +7 -0
- package/package.json +4 -21
- package/project.json +37 -0
- package/redux-connector/docs/README.md +131 -0
- package/redux-connector/index.ts +6 -0
- package/redux-connector/ng-package.json +5 -0
- package/redux-connector/src/lib/create-redux.ts +102 -0
- package/redux-connector/src/lib/model.ts +89 -0
- package/redux-connector/src/lib/rxjs-interop/redux-method.ts +66 -0
- package/redux-connector/src/lib/signal-redux-store.ts +59 -0
- package/redux-connector/src/lib/util.ts +22 -0
- package/{index.d.ts → src/index.ts} +36 -4
- package/src/lib/assertions/assertions.ts +9 -0
- package/{lib/devtools/features/with-disabled-name-indicies.d.ts → src/lib/devtools/features/with-disabled-name-indicies.ts} +5 -1
- package/{lib/devtools/features/with-glitch-tracking.d.ts → src/lib/devtools/features/with-glitch-tracking.ts} +6 -1
- package/{lib/devtools/features/with-mapper.d.ts → src/lib/devtools/features/with-mapper.ts} +7 -1
- package/src/lib/devtools/internal/current-action-names.ts +1 -0
- package/src/lib/devtools/internal/default-tracker.ts +60 -0
- package/src/lib/devtools/internal/devtools-feature.ts +37 -0
- package/src/lib/devtools/internal/devtools-syncer.service.ts +202 -0
- package/src/lib/devtools/internal/glitch-tracker.service.ts +61 -0
- package/src/lib/devtools/internal/models.ts +29 -0
- package/{lib/devtools/provide-devtools-config.d.ts → src/lib/devtools/provide-devtools-config.ts} +16 -4
- package/src/lib/devtools/rename-devtools-name.ts +21 -0
- package/src/lib/devtools/tests/action-name.spec.ts +48 -0
- package/src/lib/devtools/tests/basic.spec.ts +111 -0
- package/src/lib/devtools/tests/connecting.spec.ts +37 -0
- package/src/lib/devtools/tests/helpers.spec.ts +43 -0
- package/src/lib/devtools/tests/naming.spec.ts +216 -0
- package/src/lib/devtools/tests/provide-devtools-config.spec.ts +25 -0
- package/src/lib/devtools/tests/types.spec.ts +19 -0
- package/src/lib/devtools/tests/update-state.spec.ts +29 -0
- package/src/lib/devtools/tests/with-devtools.spec.ts +5 -0
- package/src/lib/devtools/tests/with-glitch-tracking.spec.ts +272 -0
- package/src/lib/devtools/tests/with-mapper.spec.ts +69 -0
- package/src/lib/devtools/update-state.ts +38 -0
- package/{lib/devtools/with-dev-tools-stub.d.ts → src/lib/devtools/with-dev-tools-stub.ts} +2 -1
- package/src/lib/devtools/with-devtools.ts +81 -0
- package/src/lib/flattening-operator.ts +42 -0
- package/src/lib/immutable-state/deep-freeze.ts +43 -0
- package/src/lib/immutable-state/is-dev-mode.ts +6 -0
- package/src/lib/immutable-state/tests/with-immutable-state.spec.ts +260 -0
- package/src/lib/immutable-state/with-immutable-state.ts +115 -0
- package/src/lib/mutation/http-mutation.spec.ts +473 -0
- package/src/lib/mutation/http-mutation.ts +172 -0
- package/src/lib/mutation/mutation.ts +26 -0
- package/src/lib/mutation/rx-mutation.spec.ts +594 -0
- package/src/lib/mutation/rx-mutation.ts +208 -0
- package/src/lib/shared/prettify.ts +3 -0
- package/{lib/shared/signal-store-models.d.ts → src/lib/shared/signal-store-models.ts} +8 -4
- package/src/lib/shared/throw-if-null.ts +7 -0
- package/src/lib/storage-sync/features/with-indexed-db.ts +81 -0
- package/src/lib/storage-sync/features/with-local-storage.ts +58 -0
- package/src/lib/storage-sync/internal/indexeddb.service.ts +124 -0
- package/src/lib/storage-sync/internal/local-storage.service.ts +19 -0
- package/src/lib/storage-sync/internal/models.ts +62 -0
- package/src/lib/storage-sync/internal/session-storage.service.ts +18 -0
- package/src/lib/storage-sync/tests/indexeddb.service.spec.ts +99 -0
- package/src/lib/storage-sync/tests/with-storage-async.spec.ts +308 -0
- package/src/lib/storage-sync/tests/with-storage-sync.spec.ts +268 -0
- package/src/lib/storage-sync/with-storage-sync.ts +233 -0
- package/src/lib/with-call-state.spec.ts +42 -0
- package/src/lib/with-call-state.ts +195 -0
- package/src/lib/with-conditional.spec.ts +125 -0
- package/{lib/with-conditional.d.ts → src/lib/with-conditional.ts} +31 -7
- package/src/lib/with-data-service.spec.ts +564 -0
- package/src/lib/with-data-service.ts +433 -0
- package/src/lib/with-feature-factory.spec.ts +69 -0
- package/{lib/with-feature-factory.d.ts → src/lib/with-feature-factory.ts} +32 -4
- package/src/lib/with-mutations.spec.ts +537 -0
- package/src/lib/with-mutations.ts +146 -0
- package/src/lib/with-pagination.spec.ts +90 -0
- package/src/lib/with-pagination.ts +353 -0
- package/src/lib/with-redux.spec.ts +258 -0
- package/src/lib/with-redux.ts +387 -0
- package/src/lib/with-reset.spec.ts +112 -0
- package/src/lib/with-reset.ts +62 -0
- package/src/lib/with-undo-redo.spec.ts +287 -0
- package/src/lib/with-undo-redo.ts +199 -0
- package/src/test-setup.ts +8 -0
- package/tsconfig.json +29 -0
- package/tsconfig.lib.json +17 -0
- package/tsconfig.lib.prod.json +9 -0
- package/tsconfig.spec.json +17 -0
- package/fesm2022/angular-architects-ngrx-toolkit-redux-connector.mjs +0 -119
- package/fesm2022/angular-architects-ngrx-toolkit-redux-connector.mjs.map +0 -1
- package/fesm2022/angular-architects-ngrx-toolkit.mjs +0 -1787
- package/fesm2022/angular-architects-ngrx-toolkit.mjs.map +0 -1
- package/lib/assertions/assertions.d.ts +0 -2
- package/lib/devtools/internal/current-action-names.d.ts +0 -1
- package/lib/devtools/internal/default-tracker.d.ts +0 -13
- package/lib/devtools/internal/devtools-feature.d.ts +0 -24
- package/lib/devtools/internal/devtools-syncer.service.d.ts +0 -35
- package/lib/devtools/internal/glitch-tracker.service.d.ts +0 -18
- package/lib/devtools/internal/models.d.ts +0 -24
- package/lib/devtools/rename-devtools-name.d.ts +0 -7
- package/lib/devtools/update-state.d.ts +0 -15
- package/lib/devtools/with-devtools.d.ts +0 -24
- package/lib/immutable-state/deep-freeze.d.ts +0 -11
- package/lib/immutable-state/is-dev-mode.d.ts +0 -1
- package/lib/immutable-state/with-immutable-state.d.ts +0 -60
- package/lib/shared/throw-if-null.d.ts +0 -1
- package/lib/storage-sync/features/with-indexed-db.d.ts +0 -2
- package/lib/storage-sync/features/with-local-storage.d.ts +0 -3
- package/lib/storage-sync/internal/indexeddb.service.d.ts +0 -29
- package/lib/storage-sync/internal/local-storage.service.d.ts +0 -8
- package/lib/storage-sync/internal/models.d.ts +0 -45
- package/lib/storage-sync/internal/session-storage.service.d.ts +0 -8
- package/lib/storage-sync/with-storage-sync.d.ts +0 -45
- package/lib/with-call-state.d.ts +0 -58
- package/lib/with-data-service.d.ts +0 -109
- package/lib/with-pagination.d.ts +0 -98
- package/lib/with-redux.d.ts +0 -147
- package/lib/with-reset.d.ts +0 -29
- package/lib/with-undo-redo.d.ts +0 -31
- package/redux-connector/index.d.ts +0 -2
- package/redux-connector/src/lib/create-redux.d.ts +0 -13
- package/redux-connector/src/lib/model.d.ts +0 -40
- package/redux-connector/src/lib/rxjs-interop/redux-method.d.ts +0 -14
- package/redux-connector/src/lib/signal-redux-store.d.ts +0 -11
- package/redux-connector/src/lib/util.d.ts +0 -5
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { computed, DestroyRef, inject, Injector, signal } from '@angular/core';
|
|
2
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
3
|
+
import {
|
|
4
|
+
catchError,
|
|
5
|
+
defer,
|
|
6
|
+
EMPTY,
|
|
7
|
+
finalize,
|
|
8
|
+
Observable,
|
|
9
|
+
Subject,
|
|
10
|
+
tap,
|
|
11
|
+
} from 'rxjs';
|
|
12
|
+
|
|
13
|
+
import { concatOp, FlatteningOperator } from '../flattening-operator';
|
|
14
|
+
import { Mutation, MutationResult, MutationStatus } from './mutation';
|
|
15
|
+
|
|
16
|
+
export type Operation<Parameter, Result> = (param: Parameter) => Result;
|
|
17
|
+
|
|
18
|
+
export interface RxMutationOptions<Parameter, Result> {
|
|
19
|
+
operation: Operation<Parameter, Observable<Result>>;
|
|
20
|
+
onSuccess?: (result: Result, param: Parameter) => void;
|
|
21
|
+
onError?: (error: unknown, param: Parameter) => void;
|
|
22
|
+
operator?: FlatteningOperator;
|
|
23
|
+
injector?: Injector;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Creates a mutation that leverages RxJS.
|
|
28
|
+
*
|
|
29
|
+
* For each mutation the following options can be defined:
|
|
30
|
+
* - `operation`: A function that defines the mutation logic. It returns an Observable.
|
|
31
|
+
* - `onSuccess`: A callback that is called when the mutation is successful.
|
|
32
|
+
* - `onError`: A callback that is called when the mutation fails.
|
|
33
|
+
* - `operator`: An optional wrapper of an RxJS flattening operator. By default `concat` sematics are used.
|
|
34
|
+
* - `injector`: An optional Angular injector to use for dependency injection.
|
|
35
|
+
*
|
|
36
|
+
* The `operation` is the only mandatory option.
|
|
37
|
+
*
|
|
38
|
+
* The returned mutation can be called as an async function and returns a Promise.
|
|
39
|
+
* This promise informs about whether the mutation was successful, failed, or aborted
|
|
40
|
+
* (due to switchMap or exhaustMap semantics).
|
|
41
|
+
*
|
|
42
|
+
* The mutation also provides several Signals such as error, status or isPending (see below).
|
|
43
|
+
*
|
|
44
|
+
* Example usage without Store:
|
|
45
|
+
*
|
|
46
|
+
* ```typescript
|
|
47
|
+
* const counterSignal = signal(0);
|
|
48
|
+
*
|
|
49
|
+
* const increment = rxMutation({
|
|
50
|
+
* operation: (param: Param) => {
|
|
51
|
+
* return calcSum(this.counterSignal(), param.value);
|
|
52
|
+
* },
|
|
53
|
+
* operator: concatOp,
|
|
54
|
+
* onSuccess: (result) => {
|
|
55
|
+
* this.counterSignal.set(result);
|
|
56
|
+
* },
|
|
57
|
+
* onError: (error) => {
|
|
58
|
+
* console.error('Error occurred:', error);
|
|
59
|
+
* },
|
|
60
|
+
* });
|
|
61
|
+
*
|
|
62
|
+
* const error = increment.error;
|
|
63
|
+
* const isPending = increment.isPending;
|
|
64
|
+
* const status = increment.status;
|
|
65
|
+
* const value = increment.value;
|
|
66
|
+
* const hasValue = increment.hasValue;
|
|
67
|
+
*
|
|
68
|
+
* async function incrementCounter() {
|
|
69
|
+
* const result = await increment({ value: 1 });
|
|
70
|
+
* if (result.status === 'success') {
|
|
71
|
+
* console.log('Success:', result.value);
|
|
72
|
+
* }
|
|
73
|
+
* if (result.status === 'error') {
|
|
74
|
+
* console.log('Error:', result.error);
|
|
75
|
+
* }
|
|
76
|
+
* if (result.status === 'aborted') {
|
|
77
|
+
* console.log('Operation aborted');
|
|
78
|
+
* }
|
|
79
|
+
* }
|
|
80
|
+
*
|
|
81
|
+
* function calcSum(a: number, b: number): Observable<number> {
|
|
82
|
+
* return of(result).pipe(delay(500));
|
|
83
|
+
* }
|
|
84
|
+
* ```
|
|
85
|
+
*
|
|
86
|
+
* @param options
|
|
87
|
+
* @returns the actual mutation function along tracking data as properties/methods
|
|
88
|
+
*/
|
|
89
|
+
export function rxMutation<Parameter, Result>(
|
|
90
|
+
optionsOrOperation:
|
|
91
|
+
| RxMutationOptions<Parameter, Result>
|
|
92
|
+
| Operation<Parameter, Observable<Result>>,
|
|
93
|
+
): Mutation<Parameter, Result> {
|
|
94
|
+
const inputSubject = new Subject<{
|
|
95
|
+
param: Parameter;
|
|
96
|
+
resolve: (result: MutationResult<Result>) => void;
|
|
97
|
+
}>();
|
|
98
|
+
|
|
99
|
+
const options =
|
|
100
|
+
typeof optionsOrOperation === 'function'
|
|
101
|
+
? { operation: optionsOrOperation }
|
|
102
|
+
: optionsOrOperation;
|
|
103
|
+
|
|
104
|
+
const flatteningOp = options.operator ?? concatOp;
|
|
105
|
+
|
|
106
|
+
const destroyRef = options.injector?.get(DestroyRef) ?? inject(DestroyRef);
|
|
107
|
+
|
|
108
|
+
const callCount = signal(0);
|
|
109
|
+
const errorSignal = signal<unknown>(undefined);
|
|
110
|
+
const idle = signal(true);
|
|
111
|
+
const isPending = computed(() => callCount() > 0);
|
|
112
|
+
const value = signal<Result | undefined>(undefined);
|
|
113
|
+
const isSuccess = computed(() => !idle() && !isPending() && !errorSignal());
|
|
114
|
+
|
|
115
|
+
const hasValue = function (
|
|
116
|
+
this: Mutation<Parameter, Result>,
|
|
117
|
+
): this is Mutation<Exclude<Parameter, undefined>, Result> {
|
|
118
|
+
return typeof value() !== 'undefined';
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const status = computed<MutationStatus>(() => {
|
|
122
|
+
if (idle()) {
|
|
123
|
+
return 'idle';
|
|
124
|
+
}
|
|
125
|
+
if (callCount() > 0) {
|
|
126
|
+
return 'pending';
|
|
127
|
+
}
|
|
128
|
+
if (errorSignal()) {
|
|
129
|
+
return 'error';
|
|
130
|
+
}
|
|
131
|
+
return 'success';
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const initialInnerStatus: MutationStatus = 'idle';
|
|
135
|
+
let innerStatus: MutationStatus = initialInnerStatus;
|
|
136
|
+
|
|
137
|
+
inputSubject
|
|
138
|
+
.pipe(
|
|
139
|
+
flatteningOp.rxJsOperator((input) =>
|
|
140
|
+
defer(() => {
|
|
141
|
+
callCount.update((c) => c + 1);
|
|
142
|
+
idle.set(false);
|
|
143
|
+
return options.operation(input.param).pipe(
|
|
144
|
+
tap((result: Result) => {
|
|
145
|
+
options.onSuccess?.(result, input.param);
|
|
146
|
+
innerStatus = 'success';
|
|
147
|
+
errorSignal.set(undefined);
|
|
148
|
+
value.set(result);
|
|
149
|
+
}),
|
|
150
|
+
catchError((error: unknown) => {
|
|
151
|
+
options.onError?.(error, input.param);
|
|
152
|
+
errorSignal.set(error);
|
|
153
|
+
value.set(undefined);
|
|
154
|
+
innerStatus = 'error';
|
|
155
|
+
return EMPTY;
|
|
156
|
+
}),
|
|
157
|
+
finalize(() => {
|
|
158
|
+
callCount.update((c) => c - 1);
|
|
159
|
+
|
|
160
|
+
if (innerStatus === 'success') {
|
|
161
|
+
input.resolve({
|
|
162
|
+
status: 'success',
|
|
163
|
+
value: value() as Result,
|
|
164
|
+
});
|
|
165
|
+
} else if (innerStatus === 'error') {
|
|
166
|
+
input.resolve({
|
|
167
|
+
status: 'error',
|
|
168
|
+
error: errorSignal(),
|
|
169
|
+
});
|
|
170
|
+
} else {
|
|
171
|
+
input.resolve({
|
|
172
|
+
status: 'aborted',
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
innerStatus = initialInnerStatus;
|
|
177
|
+
}),
|
|
178
|
+
);
|
|
179
|
+
}),
|
|
180
|
+
),
|
|
181
|
+
takeUntilDestroyed(destroyRef),
|
|
182
|
+
)
|
|
183
|
+
.subscribe();
|
|
184
|
+
|
|
185
|
+
const mutationFn = (param: Parameter) => {
|
|
186
|
+
return new Promise<MutationResult<Result>>((resolve) => {
|
|
187
|
+
if (callCount() > 0 && flatteningOp.exhaustSemantics) {
|
|
188
|
+
resolve({
|
|
189
|
+
status: 'aborted',
|
|
190
|
+
});
|
|
191
|
+
} else {
|
|
192
|
+
inputSubject.next({
|
|
193
|
+
param,
|
|
194
|
+
resolve,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const mutation = mutationFn as Mutation<Parameter, Result>;
|
|
201
|
+
mutation.status = status;
|
|
202
|
+
mutation.isPending = isPending;
|
|
203
|
+
mutation.error = errorSignal;
|
|
204
|
+
mutation.value = value;
|
|
205
|
+
mutation.hasValue = hasValue;
|
|
206
|
+
mutation.isSuccess = isSuccess;
|
|
207
|
+
return mutation;
|
|
208
|
+
}
|
|
@@ -14,13 +14,17 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { Signal } from '@angular/core';
|
|
16
16
|
import { EntityId } from '@ngrx/signals/entities';
|
|
17
|
+
|
|
18
|
+
// withEntites models
|
|
17
19
|
export type EntityState<Entity> = {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
entityMap: Record<EntityId, Entity>;
|
|
21
|
+
ids: EntityId[];
|
|
20
22
|
};
|
|
23
|
+
|
|
21
24
|
export type EntityComputed<Entity> = {
|
|
22
|
-
|
|
25
|
+
entities: Signal<Entity[]>;
|
|
23
26
|
};
|
|
27
|
+
|
|
24
28
|
export type NamedEntityComputed<Entity, Collection extends string> = {
|
|
25
|
-
|
|
29
|
+
[K in keyof EntityComputed<Entity> as `${Collection}${Capitalize<K>}`]: EntityComputed<Entity>[K];
|
|
26
30
|
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { inject } from '@angular/core';
|
|
2
|
+
import { getState, patchState } from '@ngrx/signals';
|
|
3
|
+
import { IndexedDBService } from '../internal/indexeddb.service';
|
|
4
|
+
import {
|
|
5
|
+
AsyncMethods,
|
|
6
|
+
AsyncStorageStrategy,
|
|
7
|
+
AsyncStoreForFactory,
|
|
8
|
+
SYNC_STATUS,
|
|
9
|
+
} from '../internal/models';
|
|
10
|
+
import { SyncConfig } from '../with-storage-sync';
|
|
11
|
+
|
|
12
|
+
export function withIndexedDB<
|
|
13
|
+
State extends object,
|
|
14
|
+
>(): AsyncStorageStrategy<State> {
|
|
15
|
+
function factory(
|
|
16
|
+
{ key, parse, select, stringify }: Required<SyncConfig<State>>,
|
|
17
|
+
store: AsyncStoreForFactory<State>,
|
|
18
|
+
useStubs: boolean,
|
|
19
|
+
): AsyncMethods {
|
|
20
|
+
if (useStubs) {
|
|
21
|
+
return {
|
|
22
|
+
clearStorage: () => Promise.resolve(),
|
|
23
|
+
readFromStorage: () => Promise.resolve(),
|
|
24
|
+
writeToStorage: () => Promise.resolve(),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const indexeddbService = inject(IndexedDBService);
|
|
29
|
+
|
|
30
|
+
function warnOnSyncing(mode: 'read' | 'write'): void {
|
|
31
|
+
if (store[SYNC_STATUS]() === 'syncing') {
|
|
32
|
+
const prettyMode = mode === 'read' ? 'Reading' : 'Writing';
|
|
33
|
+
console.warn(
|
|
34
|
+
`${prettyMode} to Store (${key}) happened during an ongoing synchronization process.`,
|
|
35
|
+
'Please ensure that the store is not in syncing state via `store.whenSynced()`.',
|
|
36
|
+
'Alternatively, you can disable the autoSync by passing `autoSync: false` in the config.',
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
/**
|
|
43
|
+
* Removes the item stored in storage.
|
|
44
|
+
*/
|
|
45
|
+
async clearStorage(): Promise<void> {
|
|
46
|
+
warnOnSyncing('write');
|
|
47
|
+
store[SYNC_STATUS].set('syncing');
|
|
48
|
+
patchState(store, {});
|
|
49
|
+
await indexeddbService.clear(key);
|
|
50
|
+
store[SYNC_STATUS].set('synced');
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Reads item from storage and patches the state.
|
|
55
|
+
*/
|
|
56
|
+
async readFromStorage(): Promise<void> {
|
|
57
|
+
warnOnSyncing('read');
|
|
58
|
+
store[SYNC_STATUS].set('syncing');
|
|
59
|
+
const stateString = await indexeddbService.getItem(key);
|
|
60
|
+
if (stateString) {
|
|
61
|
+
patchState(store, parse(stateString));
|
|
62
|
+
}
|
|
63
|
+
store[SYNC_STATUS].set('synced');
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Writes selected portion to storage.
|
|
68
|
+
*/
|
|
69
|
+
async writeToStorage(): Promise<void> {
|
|
70
|
+
warnOnSyncing('write');
|
|
71
|
+
store[SYNC_STATUS].set('syncing');
|
|
72
|
+
const slicedState = select(getState(store)) as State;
|
|
73
|
+
await indexeddbService.setItem(key, stringify(slicedState));
|
|
74
|
+
store[SYNC_STATUS].set('synced');
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
factory.type = 'async' as const;
|
|
79
|
+
|
|
80
|
+
return factory;
|
|
81
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { inject, Type } from '@angular/core';
|
|
2
|
+
import { getState, patchState } from '@ngrx/signals';
|
|
3
|
+
import { LocalStorageService } from '../internal/local-storage.service';
|
|
4
|
+
import { SyncStorageStrategy, SyncStoreForFactory } from '../internal/models';
|
|
5
|
+
import { SessionStorageService } from '../internal/session-storage.service';
|
|
6
|
+
import { SyncConfig } from '../with-storage-sync';
|
|
7
|
+
|
|
8
|
+
export function withLocalStorage<
|
|
9
|
+
State extends object,
|
|
10
|
+
>(): SyncStorageStrategy<State> {
|
|
11
|
+
return createSyncMethods<State>(LocalStorageService);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function withSessionStorage<State extends object>() {
|
|
15
|
+
return createSyncMethods<State>(SessionStorageService);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function createSyncMethods<State extends object>(
|
|
19
|
+
Storage: Type<LocalStorageService | SessionStorageService>,
|
|
20
|
+
): SyncStorageStrategy<State> {
|
|
21
|
+
function factory(
|
|
22
|
+
{ key, parse, select, stringify }: Required<SyncConfig<State>>,
|
|
23
|
+
store: SyncStoreForFactory<State>,
|
|
24
|
+
useStubs: boolean,
|
|
25
|
+
) {
|
|
26
|
+
if (useStubs) {
|
|
27
|
+
return {
|
|
28
|
+
clearStorage: () => undefined,
|
|
29
|
+
readFromStorage: () => undefined,
|
|
30
|
+
writeToStorage: () => undefined,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const storage = inject(Storage);
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
clearStorage(): void {
|
|
38
|
+
storage.clear(key);
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
readFromStorage(): void {
|
|
42
|
+
const stateString = storage.getItem(key);
|
|
43
|
+
|
|
44
|
+
if (stateString) {
|
|
45
|
+
patchState(store, parse(stateString));
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
writeToStorage() {
|
|
50
|
+
const slicedState = select(getState(store)) as State;
|
|
51
|
+
storage.setItem(key, stringify(slicedState));
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
factory.type = 'sync' as const;
|
|
56
|
+
|
|
57
|
+
return factory;
|
|
58
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
export const keyPath = 'ngrxToolkitKeyPath';
|
|
4
|
+
|
|
5
|
+
export const dbName = 'ngrxToolkitDb';
|
|
6
|
+
|
|
7
|
+
export const storeName = 'ngrxToolkitStore';
|
|
8
|
+
|
|
9
|
+
export const VERSION: number = 1 as const;
|
|
10
|
+
|
|
11
|
+
@Injectable({ providedIn: 'root' })
|
|
12
|
+
export class IndexedDBService {
|
|
13
|
+
/**
|
|
14
|
+
* write to indexedDB
|
|
15
|
+
* @param key
|
|
16
|
+
* @param data
|
|
17
|
+
*/
|
|
18
|
+
async setItem(key: string, data: string): Promise<void> {
|
|
19
|
+
const db = await this.openDB();
|
|
20
|
+
|
|
21
|
+
const tx = db.transaction(storeName, 'readwrite');
|
|
22
|
+
|
|
23
|
+
const store = tx.objectStore(storeName);
|
|
24
|
+
|
|
25
|
+
store.put({
|
|
26
|
+
[keyPath]: key,
|
|
27
|
+
value: data,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
tx.oncomplete = (): void => {
|
|
32
|
+
db.close();
|
|
33
|
+
resolve();
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
tx.onerror = (): void => {
|
|
37
|
+
db.close();
|
|
38
|
+
reject();
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* read from indexedDB
|
|
45
|
+
* @param key
|
|
46
|
+
*/
|
|
47
|
+
async getItem(key: string): Promise<string | null> {
|
|
48
|
+
const db = await this.openDB();
|
|
49
|
+
|
|
50
|
+
const tx = db.transaction(storeName, 'readonly');
|
|
51
|
+
|
|
52
|
+
const store = tx.objectStore(storeName);
|
|
53
|
+
|
|
54
|
+
const request = store.get(key);
|
|
55
|
+
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
request.onsuccess = (): void => {
|
|
58
|
+
db.close();
|
|
59
|
+
// localStorage(sessionStorage) returns null if the key does not exist
|
|
60
|
+
// Similarly, indexedDB should return null
|
|
61
|
+
if (request.result === undefined) {
|
|
62
|
+
resolve(null);
|
|
63
|
+
}
|
|
64
|
+
resolve(request.result?.['value']);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
request.onerror = (): void => {
|
|
68
|
+
db.close();
|
|
69
|
+
reject();
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* delete indexedDB
|
|
76
|
+
* @param key
|
|
77
|
+
*/
|
|
78
|
+
async clear(key: string): Promise<void> {
|
|
79
|
+
const db = await this.openDB();
|
|
80
|
+
|
|
81
|
+
const tx = db.transaction(storeName, 'readwrite');
|
|
82
|
+
|
|
83
|
+
const store = tx.objectStore(storeName);
|
|
84
|
+
|
|
85
|
+
const request = store.delete(key);
|
|
86
|
+
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
request.onsuccess = (): void => {
|
|
89
|
+
db.close();
|
|
90
|
+
resolve();
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
request.onerror = (): void => {
|
|
94
|
+
db.close();
|
|
95
|
+
reject();
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* open indexedDB
|
|
102
|
+
*/
|
|
103
|
+
private async openDB(): Promise<IDBDatabase> {
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
const request = indexedDB.open(dbName, VERSION);
|
|
106
|
+
|
|
107
|
+
request.onupgradeneeded = () => {
|
|
108
|
+
const db = request.result;
|
|
109
|
+
|
|
110
|
+
if (!db.objectStoreNames.contains(storeName)) {
|
|
111
|
+
db.createObjectStore(storeName, { keyPath });
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
request.onsuccess = (): void => {
|
|
116
|
+
resolve(request.result);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
request.onerror = (): void => {
|
|
120
|
+
reject(request.error);
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core';
|
|
2
|
+
import {} from './models';
|
|
3
|
+
|
|
4
|
+
@Injectable({
|
|
5
|
+
providedIn: 'root',
|
|
6
|
+
})
|
|
7
|
+
export class LocalStorageService {
|
|
8
|
+
getItem(key: string): string | null {
|
|
9
|
+
return localStorage.getItem(key);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
setItem(key: string, data: string): void {
|
|
13
|
+
return localStorage.setItem(key, data);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
clear(key: string): void {
|
|
17
|
+
return localStorage.removeItem(key);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Signal, WritableSignal } from '@angular/core';
|
|
2
|
+
import { EmptyFeatureResult, WritableStateSource } from '@ngrx/signals';
|
|
3
|
+
import { SyncConfig } from '../with-storage-sync';
|
|
4
|
+
|
|
5
|
+
export type SyncMethods = {
|
|
6
|
+
clearStorage(): void;
|
|
7
|
+
readFromStorage(): void;
|
|
8
|
+
writeToStorage(): void;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type SyncFeatureResult = EmptyFeatureResult & {
|
|
12
|
+
methods: SyncMethods;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type SyncStoreForFactory<State extends object> =
|
|
16
|
+
WritableStateSource<State>;
|
|
17
|
+
|
|
18
|
+
export type SyncStorageStrategy<State extends object> = ((
|
|
19
|
+
config: Required<SyncConfig<State>>,
|
|
20
|
+
store: SyncStoreForFactory<State>,
|
|
21
|
+
useStubs: boolean,
|
|
22
|
+
) => SyncMethods) & { type: 'sync' };
|
|
23
|
+
|
|
24
|
+
export type AsyncMethods = {
|
|
25
|
+
clearStorage(): Promise<void>;
|
|
26
|
+
readFromStorage(): Promise<void>;
|
|
27
|
+
writeToStorage(): Promise<void>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* AsyncFeatureResult is used as the public interface that users interact with
|
|
32
|
+
* when calling `withIndexedDB`. It intentionally omits the internal SYNC_STATUS
|
|
33
|
+
* property to avoid TypeScript error TS4058 (return type of public method
|
|
34
|
+
* includes private type).
|
|
35
|
+
*
|
|
36
|
+
* For internal implementation, we use AsyncStoreForFactory which includes
|
|
37
|
+
* the SYNC_STATUS property needed for state management.
|
|
38
|
+
*/
|
|
39
|
+
export const SYNC_STATUS = Symbol('SYNC_STATUS');
|
|
40
|
+
export type SyncStatus = 'idle' | 'syncing' | 'synced';
|
|
41
|
+
|
|
42
|
+
// Keeping it internal avoids TS4058 error
|
|
43
|
+
export type InternalAsyncProps = AsyncFeatureResult['props'] & {
|
|
44
|
+
[SYNC_STATUS]: WritableSignal<SyncStatus>;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type AsyncFeatureResult = EmptyFeatureResult & {
|
|
48
|
+
methods: AsyncMethods;
|
|
49
|
+
props: {
|
|
50
|
+
isSynced: Signal<boolean>;
|
|
51
|
+
whenSynced: () => Promise<void>;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type AsyncStoreForFactory<State extends object> =
|
|
56
|
+
WritableStateSource<State> & InternalAsyncProps;
|
|
57
|
+
|
|
58
|
+
export type AsyncStorageStrategy<State extends object> = ((
|
|
59
|
+
config: Required<SyncConfig<State>>,
|
|
60
|
+
store: AsyncStoreForFactory<State>,
|
|
61
|
+
useStubs: boolean,
|
|
62
|
+
) => AsyncMethods) & { type: 'async' };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
@Injectable({
|
|
4
|
+
providedIn: 'root',
|
|
5
|
+
})
|
|
6
|
+
export class SessionStorageService {
|
|
7
|
+
getItem(key: string): string | null {
|
|
8
|
+
return sessionStorage.getItem(key);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
setItem(key: string, data: string): void {
|
|
12
|
+
return sessionStorage.setItem(key, data);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
clear(key: string): void {
|
|
16
|
+
return sessionStorage.removeItem(key);
|
|
17
|
+
}
|
|
18
|
+
}
|