Skip to content

Commit 01172da

Browse files
refactor: withStorageSync
- modify `withStorageSync` so that it is not breaking - Improve the typing, where `withIndexedDB` returns asynchronous methods - Add and improve tests - Deprecate `withIndexeddb` in favour of `withIndexedDB`
1 parent 399903d commit 01172da

16 files changed

+935
-484
lines changed

apps/demo/src/app/todo-indexeddb-sync/synced-todo-store.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import {
2+
withIndexedDB,
3+
withStorageSync,
4+
} from '@angular-architects/ngrx-toolkit';
5+
import { inject } from '@angular/core';
16
import { getState, patchState, signalStore, withMethods } from '@ngrx/signals';
27
import {
38
removeEntity,
@@ -6,11 +11,6 @@ import {
611
withEntities,
712
} from '@ngrx/signals/entities';
813
import { AddTodo, Todo, TodoService } from '../shared/todo.service';
9-
import {
10-
withIndexeddb,
11-
withStorageSync,
12-
} from '@angular-architects/ngrx-toolkit';
13-
import { inject } from '@angular/core';
1414

1515
export const SyncedTodoStore = signalStore(
1616
{ providedIn: 'root' },
@@ -19,7 +19,7 @@ export const SyncedTodoStore = signalStore(
1919
{
2020
key: 'todos-indexeddb',
2121
},
22-
withIndexeddb()
22+
withIndexedDB()
2323
),
2424
withMethods((store, todoService = inject(TodoService)) => {
2525
let currentId = 0;

libs/ngrx-toolkit/jest.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export default {
22
displayName: 'ngrx-toolkit',
3-
setupFiles: ['fake-indexeddb/auto', 'core-js'],
3+
setupFiles: ['core-js'],
44
preset: '../../jest.preset.js',
55
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
66
coverageDirectory: '../../coverage/libs/ngrx-toolkit',

libs/ngrx-toolkit/src/index.ts

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,43 @@
1-
export { withDevToolsStub } from './lib/devtools/with-dev-tools-stub';
2-
export { withDevtools } from './lib/devtools/with-devtools';
31
export { withDisabledNameIndices } from './lib/devtools/features/with-disabled-name-indicies';
4-
export { withMapper } from './lib/devtools/features/with-mapper';
52
export { withGlitchTracking } from './lib/devtools/features/with-glitch-tracking';
6-
export { patchState, updateState } from './lib/devtools/update-state';
7-
export { renameDevtoolsName } from './lib/devtools/rename-devtools-name';
3+
export { withMapper } from './lib/devtools/features/with-mapper';
84
export {
95
provideDevtoolsConfig,
106
ReduxDevtoolsConfig,
117
} from './lib/devtools/provide-devtools-config';
8+
export { renameDevtoolsName } from './lib/devtools/rename-devtools-name';
9+
export { patchState, updateState } from './lib/devtools/update-state';
10+
export { withDevToolsStub } from './lib/devtools/with-dev-tools-stub';
11+
export { withDevtools } from './lib/devtools/with-devtools';
1212

1313
export {
14-
withRedux,
15-
payload,
16-
noPayload,
17-
createReducer,
1814
createEffects,
15+
createReducer,
16+
noPayload,
17+
payload,
18+
withRedux,
1919
} from './lib/with-redux';
2020

2121
export * from './lib/with-call-state';
22-
export * from './lib/with-undo-redo';
2322
export * from './lib/with-data-service';
2423
export * from './lib/with-pagination';
25-
export { withReset, setResetState } from './lib/with-reset';
24+
export { setResetState, withReset } from './lib/with-reset';
25+
export * from './lib/with-undo-redo';
2626

27-
export { withLocalStorage } from './lib/storage-sync/features/with-local-storage';
28-
export { withSessionStorage } from './lib/storage-sync/features/with-session-storage';
29-
export { withIndexeddb } from './lib/storage-sync/features/with-indexeddb';
27+
export { withImmutableState } from './lib/immutable-state/with-immutable-state';
28+
export { withIndexedDB } from './lib/storage-sync/features/with-indexed-db';
29+
30+
/**
31+
* @deprecated Use {@link withIndexedDB} instead.
32+
*/
33+
export { withIndexedDB as withIndexeddb } from './lib/storage-sync/features/with-indexed-db';
34+
export {
35+
withLocalStorage,
36+
withSessionStorage,
37+
} from './lib/storage-sync/features/with-local-storage';
3038
export {
31-
withStorageSync,
3239
SyncConfig,
40+
withStorageSync,
3341
} from './lib/storage-sync/with-storage-sync';
34-
export { withImmutableState } from './lib/immutable-state/with-immutable-state';
42+
export { emptyFeature, withConditional } from './lib/with-conditional';
3543
export { withFeatureFactory } from './lib/with-feature-factory';
36-
export { withConditional, emptyFeature } from './lib/with-conditional';
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

libs/ngrx-toolkit/src/lib/storage-sync/features/with-indexeddb.ts

Lines changed: 0 additions & 3 deletions
This file was deleted.
Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,58 @@
1+
import { inject, Type } from '@angular/core';
2+
import { getState, patchState } from '@ngrx/signals';
13
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';
27

3-
export const withLocalStorage = () => LocalStorageService;
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+
}

libs/ngrx-toolkit/src/lib/storage-sync/features/with-session-storage.ts

Lines changed: 0 additions & 3 deletions
This file was deleted.

libs/ngrx-toolkit/src/lib/storage-sync/internal/indexeddb.service.ts

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
11
import { Injectable } from '@angular/core';
2-
import {
3-
IndexeddbService,
4-
PROMISE_NOOP,
5-
WithIndexeddbSyncFeatureResult,
6-
} from './models';
72

83
export const keyPath = 'ngrxToolkitKeyPath';
94

@@ -14,7 +9,7 @@ export const storeName = 'ngrxToolkitStore';
149
export const VERSION: number = 1 as const;
1510

1611
@Injectable({ providedIn: 'root' })
17-
export class IndexedDBService implements IndexeddbService {
12+
export class IndexedDBService {
1813
/**
1914
* write to indexedDB
2015
* @param key
@@ -102,15 +97,6 @@ export class IndexedDBService implements IndexeddbService {
10297
});
10398
}
10499

105-
/** return stub */
106-
getStub(): Pick<WithIndexeddbSyncFeatureResult, 'methods'>['methods'] {
107-
return {
108-
clearStorage: PROMISE_NOOP,
109-
readFromStorage: PROMISE_NOOP,
110-
writeToStorage: PROMISE_NOOP,
111-
};
112-
}
113-
114100
/**
115101
* open indexedDB
116102
*/
Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { Injectable } from '@angular/core';
2-
import { NOOP, StorageService, WithStorageSyncFeatureResult } from './models';
2+
import {} from './models';
33

44
@Injectable({
55
providedIn: 'root',
66
})
7-
export class LocalStorageService implements StorageService {
7+
export class LocalStorageService {
88
getItem(key: string): string | null {
99
return localStorage.getItem(key);
1010
}
@@ -16,13 +16,4 @@ export class LocalStorageService implements StorageService {
1616
clear(key: string): void {
1717
return localStorage.removeItem(key);
1818
}
19-
20-
/** return stub */
21-
getStub(): Pick<WithStorageSyncFeatureResult, 'methods'>['methods'] {
22-
return {
23-
clearStorage: NOOP,
24-
readFromStorage: NOOP,
25-
writeToStorage: NOOP,
26-
};
27-
}
2819
}
Lines changed: 39 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,49 @@
1-
import { EmptyFeatureResult } from '@ngrx/signals';
2-
import { Type } from '@angular/core';
3-
4-
export interface StorageService {
5-
clear(key: string): void;
6-
7-
getItem(key: string): string | null;
8-
9-
setItem(key: string, data: string): void;
10-
11-
getStub(): Pick<WithStorageSyncFeatureResult, 'methods'>['methods'];
12-
}
13-
14-
export interface IndexeddbService {
15-
clear(key: string): Promise<void>;
16-
17-
getItem(key: string): Promise<string | null>;
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+
};
1810

19-
setItem(key: string, data: string): Promise<void>;
11+
export type SyncFeatureResult = EmptyFeatureResult & {
12+
methods: SyncMethods;
13+
};
2014

21-
getStub(): Pick<WithIndexeddbSyncFeatureResult, 'methods'>['methods'];
22-
}
15+
export type SyncStoreForFactory<State extends object> =
16+
WritableStateSource<State>;
2317

24-
export type StorageServiceFactory =
25-
| Type<IndexeddbService>
26-
| Type<StorageService>;
18+
export type SyncStorageStrategy<State extends object> = ((
19+
config: Required<SyncConfig<State>>,
20+
store: SyncStoreForFactory<State>,
21+
useStubs: boolean
22+
) => SyncMethods) & { type: 'sync' };
2723

28-
export type WithIndexeddbSyncFeatureResult = EmptyFeatureResult & {
29-
methods: {
30-
clearStorage(): Promise<void>;
31-
readFromStorage(): Promise<void>;
32-
writeToStorage(): Promise<void>;
33-
};
24+
export type AsyncMethods = {
25+
clearStorage(): Promise<void>;
26+
readFromStorage(): Promise<void>;
27+
writeToStorage(): Promise<void>;
3428
};
3529

36-
export type WithStorageSyncFeatureResult = EmptyFeatureResult & {
37-
methods: {
38-
clearStorage(): void;
39-
readFromStorage(): void;
40-
writeToStorage(): void;
30+
export const SYNC_STATUS = Symbol('SYNC_STATUS');
31+
export type SyncStatus = 'idle' | 'syncing' | 'synced';
32+
33+
export type AsyncFeatureResult = EmptyFeatureResult & {
34+
methods: AsyncMethods;
35+
props: {
36+
isSynced: Signal<boolean>;
37+
whenSynced: () => Promise<void>;
38+
[SYNC_STATUS]: WritableSignal<SyncStatus>;
4139
};
4240
};
4341

44-
export const NOOP = () => void true;
42+
export type AsyncStoreForFactory<State extends object> =
43+
WritableStateSource<State> & AsyncFeatureResult['props'];
4544

46-
export const PROMISE_NOOP = () => Promise.resolve();
45+
export type AsyncStorageStrategy<State extends object> = ((
46+
config: Required<SyncConfig<State>>,
47+
store: AsyncStoreForFactory<State>,
48+
useStubs: boolean
49+
) => AsyncMethods) & { type: 'async' };

0 commit comments

Comments
 (0)