Skip to content

Commit

Permalink
feat(storage-plugin): allow providing feature states
Browse files Browse the repository at this point in the history
This commit introduces the ability to specify states for persistence at the feature level.
Previously, you could only specify a list of states for persistence when providing the storage
plugin at the root level. This feature is only available with the standalone API, as Angular has
an `ENVIRONMENT_INITIALIZER` feature that allows us to add these states at the feature level.

The API is simple and straightforward. It introduces the `withStorageFeature` function, which
can be called along with `provideStates`. It requires a list of states to be provided. If all
states are already being persisted because the developer specified `keys: *` at the root level,
this won't do anything.
  • Loading branch information
arturovt committed Mar 13, 2024
1 parent 42a8ed3 commit 046d028
Show file tree
Hide file tree
Showing 10 changed files with 329 additions and 117 deletions.
32 changes: 32 additions & 0 deletions docs/plugins/storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,3 +304,35 @@ In the migration strategy, we define:
- `key`: The key for the item to migrate. If not specified, it takes the entire storage state.

Note: Its important to specify the strategies in the order of which they should progress.

### Feature States

We can also add states at the feature level when invoking `provideStates`, such as within `Route` providers. This is useful when we want to avoid the root level, responsible for providing the store, from being aware of any feature states. If we do not specify any states to be persisted at the root level, we should specify an empty list:

```ts
import { provideStore } from '@ngxs/store';
import { withNgxsStoragePlugin } from '@ngxs/storage-plugin';

export const appConfig: ApplicationConfig = {
providers: [provideStore([], withNgxsStoragePlugin({ keys: [] }))]
};
```

If `keys` is an empty list, it indicates that the plugin should not persist any state until it's explicitly added at the feature level.

After registering the `AnimalsState` at the feature level, we also want to persist this state in storage:

```ts
import { provideStates } from '@ngxs/store';
import { withStorageFeature } from '@ngxs/storage-plugin';

export const routes: Routes = [
{
path: 'animals',
loadComponent: () => import('./animals'),
providers: [provideStates([AnimalsState], withStorageFeature([AnimalsState]))]
}
];
```

Please note that at the root level, `keys` should not be set to `*` because `*` indicates persisting everything.
44 changes: 0 additions & 44 deletions packages/storage-plugin/internals/src/final-options.ts

This file was deleted.

1 change: 0 additions & 1 deletion packages/storage-plugin/internals/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export * from './symbols';
export * from './final-options';
export * from './storage-key';
15 changes: 14 additions & 1 deletion packages/storage-plugin/internals/src/symbols.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { InjectionToken } from '@angular/core';
import { InjectionToken, inject } from '@angular/core';

import { StorageKey } from './storage-key';

Expand Down Expand Up @@ -87,6 +87,19 @@ export interface ɵNgxsTransformedStoragePluginOptions extends NgxsStoragePlugin
keys: StorageKey[];
}

export const ɵUSER_OPTIONS = new InjectionToken<NgxsStoragePluginOptions>(
NG_DEV_MODE ? 'USER_OPTIONS' : ''
);

// Determines whether all states in the NGXS registry should be persisted or not.
export const ɵALL_STATES_PERSISTED = new InjectionToken<boolean>(
NG_DEV_MODE ? 'ALL_STATES_PERSISTED' : '',
{
providedIn: 'root',
factory: () => inject(ɵUSER_OPTIONS).keys === '*'
}
);

export const ɵNGXS_STORAGE_PLUGIN_OPTIONS =
new InjectionToken<ɵNgxsTransformedStoragePluginOptions>(
NG_DEV_MODE ? 'NGXS_STORAGE_PLUGIN_OPTIONS' : ''
Expand Down
58 changes: 58 additions & 0 deletions packages/storage-plugin/src/keys-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Injectable, Injector, inject } from '@angular/core';
import {
STORAGE_ENGINE,
StorageEngine,
StorageKey,
ɵextractStringKey,
ɵisKeyWithExplicitEngine,
ɵNGXS_STORAGE_PLUGIN_OPTIONS
} from '@ngxs/storage-plugin/internals';

interface KeyWithEngine {
key: string;
engine: StorageEngine;
}

@Injectable({ providedIn: 'root' })
export class ɵNgxsStoragePluginKeysManager {
/** Store keys separately in a set so we're able to check if the key already exists. */
private readonly _keys = new Set<string>();

private readonly _injector = inject(Injector);

private readonly _keysWithEngines: KeyWithEngine[] = [];

constructor() {
const { keys } = inject(ɵNGXS_STORAGE_PLUGIN_OPTIONS);
this.addKeys(keys);
}

getKeysWithEngines() {
// Spread to prevent external code from directly modifying the internal state.
return [...this._keysWithEngines];
}

addKeys(storageKeys: StorageKey[]): void {
for (const storageKey of storageKeys) {
const key = ɵextractStringKey(storageKey);

// The user may call `withStorageFeature` with the same state multiple times.
// Let's prevent duplicating state names in the `keysWithEngines` list.
// Please note that calling provideStates multiple times with the same state is
// acceptable behavior. This may occur because the state could be necessary at the
// feature level, and different parts of the application might require its registration.
// Consequently, `withStorageFeature` may also be called multiple times.
if (this._keys.has(key)) {
continue;
}

this._keys.add(key);

const engine = ɵisKeyWithExplicitEngine(storageKey)
? this._injector.get(storageKey.engine)
: this._injector.get(STORAGE_ENGINE);

this._keysWithEngines.push({ key, engine });
}
}
}
1 change: 1 addition & 0 deletions packages/storage-plugin/src/public_api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { NgxsStoragePluginModule, withNgxsStoragePlugin } from './storage.module';
export { withStorageFeature } from './with-storage-feature';
export { NgxsStoragePlugin } from './storage.plugin';
export * from './engines';

Expand Down
31 changes: 6 additions & 25 deletions packages/storage-plugin/src/storage.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,21 @@ import {
NgModule,
ModuleWithProviders,
PLATFORM_ID,
InjectionToken,
Injector,
EnvironmentProviders,
makeEnvironmentProviders
} from '@angular/core';
import { withNgxsPlugin } from '@ngxs/store';
import { NGXS_PLUGINS } from '@ngxs/store/plugins';
import {
NgxsStoragePluginOptions,
ɵUSER_OPTIONS,
STORAGE_ENGINE,
ɵNGXS_STORAGE_PLUGIN_OPTIONS,
ɵcreateFinalStoragePluginOptions,
ɵFINAL_NGXS_STORAGE_PLUGIN_OPTIONS
NgxsStoragePluginOptions
} from '@ngxs/storage-plugin/internals';

import { NgxsStoragePlugin } from './storage.plugin';
import { engineFactory, storageOptionsFactory } from './internals';

declare const ngDevMode: boolean;

const NG_DEV_MODE = typeof ngDevMode === 'undefined' || ngDevMode;

export const USER_OPTIONS = new InjectionToken(NG_DEV_MODE ? 'USER_OPTIONS' : '');

@NgModule()
export class NgxsStoragePluginModule {
static forRoot(
Expand All @@ -40,23 +31,18 @@ export class NgxsStoragePluginModule {
multi: true
},
{
provide: USER_OPTIONS,
provide: ɵUSER_OPTIONS,
useValue: options
},
{
provide: ɵNGXS_STORAGE_PLUGIN_OPTIONS,
useFactory: storageOptionsFactory,
deps: [USER_OPTIONS]
deps: [ɵUSER_OPTIONS]
},
{
provide: STORAGE_ENGINE,
useFactory: engineFactory,
deps: [ɵNGXS_STORAGE_PLUGIN_OPTIONS, PLATFORM_ID]
},
{
provide: ɵFINAL_NGXS_STORAGE_PLUGIN_OPTIONS,
useFactory: ɵcreateFinalStoragePluginOptions,
deps: [Injector, ɵNGXS_STORAGE_PLUGIN_OPTIONS]
}
]
};
Expand All @@ -69,23 +55,18 @@ export function withNgxsStoragePlugin(
return makeEnvironmentProviders([
withNgxsPlugin(NgxsStoragePlugin),
{
provide: USER_OPTIONS,
provide: ɵUSER_OPTIONS,
useValue: options
},
{
provide: ɵNGXS_STORAGE_PLUGIN_OPTIONS,
useFactory: storageOptionsFactory,
deps: [USER_OPTIONS]
deps: [ɵUSER_OPTIONS]
},
{
provide: STORAGE_ENGINE,
useFactory: engineFactory,
deps: [ɵNGXS_STORAGE_PLUGIN_OPTIONS, PLATFORM_ID]
},
{
provide: ɵFINAL_NGXS_STORAGE_PLUGIN_OPTIONS,
useFactory: ɵcreateFinalStoragePluginOptions,
deps: [Injector, ɵNGXS_STORAGE_PLUGIN_OPTIONS]
}
]);
}
Loading

0 comments on commit 046d028

Please sign in to comment.