@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.
Files changed (123) hide show
  1. package/eslint.config.cjs +43 -0
  2. package/jest.config.ts +22 -0
  3. package/ng-package.json +7 -0
  4. package/package.json +4 -21
  5. package/project.json +37 -0
  6. package/redux-connector/docs/README.md +131 -0
  7. package/redux-connector/index.ts +6 -0
  8. package/redux-connector/ng-package.json +5 -0
  9. package/redux-connector/src/lib/create-redux.ts +102 -0
  10. package/redux-connector/src/lib/model.ts +89 -0
  11. package/redux-connector/src/lib/rxjs-interop/redux-method.ts +66 -0
  12. package/redux-connector/src/lib/signal-redux-store.ts +59 -0
  13. package/redux-connector/src/lib/util.ts +22 -0
  14. package/{index.d.ts → src/index.ts} +36 -4
  15. package/src/lib/assertions/assertions.ts +9 -0
  16. package/{lib/devtools/features/with-disabled-name-indicies.d.ts → src/lib/devtools/features/with-disabled-name-indicies.ts} +5 -1
  17. package/{lib/devtools/features/with-glitch-tracking.d.ts → src/lib/devtools/features/with-glitch-tracking.ts} +6 -1
  18. package/{lib/devtools/features/with-mapper.d.ts → src/lib/devtools/features/with-mapper.ts} +7 -1
  19. package/src/lib/devtools/internal/current-action-names.ts +1 -0
  20. package/src/lib/devtools/internal/default-tracker.ts +60 -0
  21. package/src/lib/devtools/internal/devtools-feature.ts +37 -0
  22. package/src/lib/devtools/internal/devtools-syncer.service.ts +202 -0
  23. package/src/lib/devtools/internal/glitch-tracker.service.ts +61 -0
  24. package/src/lib/devtools/internal/models.ts +29 -0
  25. package/{lib/devtools/provide-devtools-config.d.ts → src/lib/devtools/provide-devtools-config.ts} +16 -4
  26. package/src/lib/devtools/rename-devtools-name.ts +21 -0
  27. package/src/lib/devtools/tests/action-name.spec.ts +48 -0
  28. package/src/lib/devtools/tests/basic.spec.ts +111 -0
  29. package/src/lib/devtools/tests/connecting.spec.ts +37 -0
  30. package/src/lib/devtools/tests/helpers.spec.ts +43 -0
  31. package/src/lib/devtools/tests/naming.spec.ts +216 -0
  32. package/src/lib/devtools/tests/provide-devtools-config.spec.ts +25 -0
  33. package/src/lib/devtools/tests/types.spec.ts +19 -0
  34. package/src/lib/devtools/tests/update-state.spec.ts +29 -0
  35. package/src/lib/devtools/tests/with-devtools.spec.ts +5 -0
  36. package/src/lib/devtools/tests/with-glitch-tracking.spec.ts +272 -0
  37. package/src/lib/devtools/tests/with-mapper.spec.ts +69 -0
  38. package/src/lib/devtools/update-state.ts +38 -0
  39. package/{lib/devtools/with-dev-tools-stub.d.ts → src/lib/devtools/with-dev-tools-stub.ts} +2 -1
  40. package/src/lib/devtools/with-devtools.ts +81 -0
  41. package/src/lib/flattening-operator.ts +42 -0
  42. package/src/lib/immutable-state/deep-freeze.ts +43 -0
  43. package/src/lib/immutable-state/is-dev-mode.ts +6 -0
  44. package/src/lib/immutable-state/tests/with-immutable-state.spec.ts +260 -0
  45. package/src/lib/immutable-state/with-immutable-state.ts +115 -0
  46. package/src/lib/mutation/http-mutation.spec.ts +473 -0
  47. package/src/lib/mutation/http-mutation.ts +172 -0
  48. package/src/lib/mutation/mutation.ts +26 -0
  49. package/src/lib/mutation/rx-mutation.spec.ts +594 -0
  50. package/src/lib/mutation/rx-mutation.ts +208 -0
  51. package/src/lib/shared/prettify.ts +3 -0
  52. package/{lib/shared/signal-store-models.d.ts → src/lib/shared/signal-store-models.ts} +8 -4
  53. package/src/lib/shared/throw-if-null.ts +7 -0
  54. package/src/lib/storage-sync/features/with-indexed-db.ts +81 -0
  55. package/src/lib/storage-sync/features/with-local-storage.ts +58 -0
  56. package/src/lib/storage-sync/internal/indexeddb.service.ts +124 -0
  57. package/src/lib/storage-sync/internal/local-storage.service.ts +19 -0
  58. package/src/lib/storage-sync/internal/models.ts +62 -0
  59. package/src/lib/storage-sync/internal/session-storage.service.ts +18 -0
  60. package/src/lib/storage-sync/tests/indexeddb.service.spec.ts +99 -0
  61. package/src/lib/storage-sync/tests/with-storage-async.spec.ts +308 -0
  62. package/src/lib/storage-sync/tests/with-storage-sync.spec.ts +268 -0
  63. package/src/lib/storage-sync/with-storage-sync.ts +233 -0
  64. package/src/lib/with-call-state.spec.ts +42 -0
  65. package/src/lib/with-call-state.ts +195 -0
  66. package/src/lib/with-conditional.spec.ts +125 -0
  67. package/{lib/with-conditional.d.ts → src/lib/with-conditional.ts} +31 -7
  68. package/src/lib/with-data-service.spec.ts +564 -0
  69. package/src/lib/with-data-service.ts +433 -0
  70. package/src/lib/with-feature-factory.spec.ts +69 -0
  71. package/{lib/with-feature-factory.d.ts → src/lib/with-feature-factory.ts} +32 -4
  72. package/src/lib/with-mutations.spec.ts +537 -0
  73. package/src/lib/with-mutations.ts +146 -0
  74. package/src/lib/with-pagination.spec.ts +90 -0
  75. package/src/lib/with-pagination.ts +353 -0
  76. package/src/lib/with-redux.spec.ts +258 -0
  77. package/src/lib/with-redux.ts +387 -0
  78. package/src/lib/with-reset.spec.ts +112 -0
  79. package/src/lib/with-reset.ts +62 -0
  80. package/src/lib/with-undo-redo.spec.ts +287 -0
  81. package/src/lib/with-undo-redo.ts +199 -0
  82. package/src/test-setup.ts +8 -0
  83. package/tsconfig.json +29 -0
  84. package/tsconfig.lib.json +17 -0
  85. package/tsconfig.lib.prod.json +9 -0
  86. package/tsconfig.spec.json +17 -0
  87. package/fesm2022/angular-architects-ngrx-toolkit-redux-connector.mjs +0 -119
  88. package/fesm2022/angular-architects-ngrx-toolkit-redux-connector.mjs.map +0 -1
  89. package/fesm2022/angular-architects-ngrx-toolkit.mjs +0 -1787
  90. package/fesm2022/angular-architects-ngrx-toolkit.mjs.map +0 -1
  91. package/lib/assertions/assertions.d.ts +0 -2
  92. package/lib/devtools/internal/current-action-names.d.ts +0 -1
  93. package/lib/devtools/internal/default-tracker.d.ts +0 -13
  94. package/lib/devtools/internal/devtools-feature.d.ts +0 -24
  95. package/lib/devtools/internal/devtools-syncer.service.d.ts +0 -35
  96. package/lib/devtools/internal/glitch-tracker.service.d.ts +0 -18
  97. package/lib/devtools/internal/models.d.ts +0 -24
  98. package/lib/devtools/rename-devtools-name.d.ts +0 -7
  99. package/lib/devtools/update-state.d.ts +0 -15
  100. package/lib/devtools/with-devtools.d.ts +0 -24
  101. package/lib/immutable-state/deep-freeze.d.ts +0 -11
  102. package/lib/immutable-state/is-dev-mode.d.ts +0 -1
  103. package/lib/immutable-state/with-immutable-state.d.ts +0 -60
  104. package/lib/shared/throw-if-null.d.ts +0 -1
  105. package/lib/storage-sync/features/with-indexed-db.d.ts +0 -2
  106. package/lib/storage-sync/features/with-local-storage.d.ts +0 -3
  107. package/lib/storage-sync/internal/indexeddb.service.d.ts +0 -29
  108. package/lib/storage-sync/internal/local-storage.service.d.ts +0 -8
  109. package/lib/storage-sync/internal/models.d.ts +0 -45
  110. package/lib/storage-sync/internal/session-storage.service.d.ts +0 -8
  111. package/lib/storage-sync/with-storage-sync.d.ts +0 -45
  112. package/lib/with-call-state.d.ts +0 -58
  113. package/lib/with-data-service.d.ts +0 -109
  114. package/lib/with-pagination.d.ts +0 -98
  115. package/lib/with-redux.d.ts +0 -147
  116. package/lib/with-reset.d.ts +0 -29
  117. package/lib/with-undo-redo.d.ts +0 -31
  118. package/redux-connector/index.d.ts +0 -2
  119. package/redux-connector/src/lib/create-redux.d.ts +0 -13
  120. package/redux-connector/src/lib/model.d.ts +0 -40
  121. package/redux-connector/src/lib/rxjs-interop/redux-method.d.ts +0 -14
  122. package/redux-connector/src/lib/signal-redux-store.d.ts +0 -11
  123. 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
+ }
@@ -0,0 +1,3 @@
1
+ export type Prettify<Type extends object> = {
2
+ [Key in keyof Type]: Type[Key];
3
+ };
@@ -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
- entityMap: Record<EntityId, Entity>;
19
- ids: EntityId[];
20
+ entityMap: Record<EntityId, Entity>;
21
+ ids: EntityId[];
20
22
  };
23
+
21
24
  export type EntityComputed<Entity> = {
22
- entities: Signal<Entity[]>;
25
+ entities: Signal<Entity[]>;
23
26
  };
27
+
24
28
  export type NamedEntityComputed<Entity, Collection extends string> = {
25
- [K in keyof EntityComputed<Entity> as `${Collection}${Capitalize<K>}`]: EntityComputed<Entity>[K];
29
+ [K in keyof EntityComputed<Entity> as `${Collection}${Capitalize<K>}`]: EntityComputed<Entity>[K];
26
30
  };
@@ -0,0 +1,7 @@
1
+ export function throwIfNull<T>(obj: T): NonNullable<T> {
2
+ if (obj === null || obj === undefined) {
3
+ throw new Error('');
4
+ }
5
+
6
+ return obj;
7
+ }
@@ -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
+ }