ngx-webstorage
Version:
Local and session storage services and decorators for Angular
666 lines (639 loc) • 26.4 kB
JavaScript
import { filter, switchMap, distinctUntilChanged, shareReplay, map } from 'rxjs/operators';
import * as i0 from '@angular/core';
import { Injectable, InjectionToken, PLATFORM_ID, Inject, Optional, untracked, signal, inject, makeEnvironmentProviders, provideAppInitializer } from '@angular/core';
import { Subject, of } from 'rxjs';
import { isPlatformBrowser } from '@angular/common';
var StorageStrategies;
(function (StorageStrategies) {
StorageStrategies["Local"] = "local_strategy";
StorageStrategies["Session"] = "session_strategy";
StorageStrategies["InMemory"] = "in_memory_strategy";
})(StorageStrategies || (StorageStrategies = {}));
class CompatHelper {
static isStorageAvailable(storage) {
let available = true;
try {
if (typeof storage === 'object') {
storage.setItem('test-storage', 'foobar');
storage.removeItem('test-storage');
}
else
available = false;
}
catch (e) {
available = false;
}
return available;
}
}
/**
* Sentinel emitted on `StorageStrategy.keyChanges` when the whole storage was
* cleared (as opposed to a single key changing).
*
* At runtime the value is `null`. The public `keyChanges` type deliberately
* stays `Subject<string>` rather than `Subject<string | null>`: widening it is
* a source break for every consumer that subscribes, not just for third-party
* strategy implementors, and this release deliberately supports Angular 21
* consumers. Revisit in v23.
*
* This constant is intentionally NOT part of the public API surface
* (not re-exported from public_api.ts): it centralises the cast and the
* rationale so every emission and comparison site shares one definition.
*/
const KEY_CLEARED = null;
function noop() { }
const DefaultPrefix = 'ngx-webstorage';
const DefaultSeparator = '|';
const DefaultIsCaseSensitive = false;
class StorageKeyManager {
static { this.prefix = DefaultPrefix; }
static { this.separator = DefaultSeparator; }
static { this.isCaseSensitive = DefaultIsCaseSensitive; }
static normalize(raw) {
raw = StorageKeyManager.isCaseSensitive ? raw : raw.toLowerCase();
return `${StorageKeyManager.prefix}${StorageKeyManager.separator}${raw}`;
}
static isNormalizedKey(key) {
return key.indexOf(StorageKeyManager.prefix + StorageKeyManager.separator) === 0;
}
static setPrefix(prefix) {
StorageKeyManager.prefix = prefix;
}
static setSeparator(separator) {
StorageKeyManager.separator = separator;
}
static setCaseSensitive(enable) {
StorageKeyManager.isCaseSensitive = enable;
}
static consumeConfiguration(config) {
if (config.prefix !== undefined)
this.setPrefix(config.prefix);
if (config.separator !== undefined)
this.setSeparator(config.separator);
if (config.caseSensitive !== undefined)
this.setCaseSensitive(config.caseSensitive);
}
}
class SyncStorage {
constructor(strategy) {
this.strategy = strategy;
}
retrieve(key) {
let value;
this.strategy.get(StorageKeyManager.normalize(key)).subscribe((result) => value = typeof result === 'undefined' ? null : result);
return value;
}
store(key, value) {
this.strategy.set(StorageKeyManager.normalize(key), value).subscribe(noop);
return value;
}
clear(key) {
if (key !== undefined)
this.strategy.del(StorageKeyManager.normalize(key)).subscribe(noop);
else
this.strategy.clear().subscribe(noop);
}
getStrategyName() { return this.strategy.name; }
observe(key) {
key = StorageKeyManager.normalize(key);
return this.strategy.keyChanges.pipe(filter((changed) => changed === KEY_CLEARED || changed === key), switchMap(() => this.strategy.get(key)), distinctUntilChanged(), shareReplay({ refCount: true, bufferSize: 1 }));
}
}
class AsyncStorage {
constructor(strategy) {
this.strategy = strategy;
}
retrieve(key) {
return this.strategy.get(StorageKeyManager.normalize(key)).pipe(map((value) => typeof value === 'undefined' ? null : value));
}
store(key, value) {
return this.strategy.set(StorageKeyManager.normalize(key), value);
}
clear(key) {
return key !== undefined ? this.strategy.del(StorageKeyManager.normalize(key)) : this.strategy.clear();
}
getStrategyName() { return this.strategy.name; }
observe(key) {
key = StorageKeyManager.normalize(key);
return this.strategy.keyChanges.pipe(filter((changed) => changed === KEY_CLEARED || changed === key), switchMap(() => this.strategy.get(key)), distinctUntilChanged(), shareReplay({ refCount: true, bufferSize: 1 }));
}
}
class StrategyCacheService {
constructor() {
this.caches = {};
}
get(strategyName, key) {
return this.getCacheStore(strategyName)[key];
}
set(strategyName, key, value) {
this.getCacheStore(strategyName)[key] = value;
}
del(strategyName, key) {
delete this.getCacheStore(strategyName)[key];
}
clear(strategyName) {
this.caches[strategyName] = {};
}
getCacheStore(strategyName) {
if (strategyName in this.caches)
return this.caches[strategyName];
return this.caches[strategyName] = {};
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: StrategyCacheService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: StrategyCacheService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: StrategyCacheService, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}] });
const LOCAL_STORAGE = new InjectionToken('window_local_storage');
function getLocalStorage() {
return (typeof window !== 'undefined') ? window.localStorage : null;
}
const LocalStorageProvider = { provide: LOCAL_STORAGE, useFactory: getLocalStorage };
const SESSION_STORAGE = new InjectionToken('window_session_storage');
function getSessionStorage() {
return (typeof window !== 'undefined') ? window.sessionStorage : null;
}
const SessionStorageProvider = { provide: SESSION_STORAGE, useFactory: getSessionStorage };
class BaseSyncStorageStrategy {
constructor(storage, cache) {
this.storage = storage;
this.cache = cache;
this.keyChanges = new Subject();
}
get isAvailable() {
if (this._isAvailable === undefined)
this._isAvailable = CompatHelper.isStorageAvailable(this.storage);
return this._isAvailable;
}
get(key) {
let data = this.cache.get(this.name, key);
if (data !== undefined)
return of(data);
try {
const item = this.storage.getItem(key);
if (item !== null) {
data = JSON.parse(item);
this.cache.set(this.name, key, data);
}
}
catch (err) {
console.warn(err);
}
return of(data);
}
set(key, value) {
const data = JSON.stringify(value);
this.storage.setItem(key, data);
this.cache.set(this.name, key, value);
this.keyChanges.next(key);
return of(value);
}
del(key) {
this.storage.removeItem(key);
this.cache.del(this.name, key);
this.keyChanges.next(key);
return of(void 0);
}
clear() {
this.storage.clear();
this.cache.clear(this.name);
this.keyChanges.next(KEY_CLEARED);
return of(void 0);
}
}
class LocalStorageStrategy extends BaseSyncStorageStrategy {
static { this.strategyName = StorageStrategies.Local; }
constructor(storage, cache, platformId, zone) {
super(storage, cache);
this.storage = storage;
this.cache = cache;
this.platformId = platformId;
this.zone = zone;
this.name = LocalStorageStrategy.strategyName;
if (isPlatformBrowser(this.platformId))
this.listenExternalChanges();
}
listenExternalChanges() {
window.addEventListener('storage', (event) => this.zone.run(() => {
if (event.storageArea !== this.storage)
return;
const key = event.key;
if (key !== null)
this.cache.del(this.name, key);
else
this.cache.clear(this.name);
// A remote clear() delivers a null key, meaning "everything was cleared".
this.keyChanges.next(key ?? KEY_CLEARED);
}));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: LocalStorageStrategy, deps: [{ token: LOCAL_STORAGE }, { token: StrategyCacheService }, { token: PLATFORM_ID }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: LocalStorageStrategy }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: LocalStorageStrategy, decorators: [{
type: Injectable
}], ctorParameters: () => [{ type: undefined, decorators: [{
type: Inject,
args: [LOCAL_STORAGE]
}] }, { type: StrategyCacheService }, { type: undefined, decorators: [{
type: Inject,
args: [PLATFORM_ID]
}] }, { type: i0.NgZone }] });
class SessionStorageStrategy extends BaseSyncStorageStrategy {
static { this.strategyName = StorageStrategies.Session; }
constructor(storage, cache, platformId, zone) {
super(storage, cache);
this.storage = storage;
this.cache = cache;
this.platformId = platformId;
this.zone = zone;
this.name = SessionStorageStrategy.strategyName;
if (isPlatformBrowser(this.platformId))
this.listenExternalChanges();
}
listenExternalChanges() {
window.addEventListener('storage', (event) => this.zone.run(() => {
if (event.storageArea !== this.storage)
return;
const key = event.key;
if (key !== null)
this.cache.del(this.name, key);
else
this.cache.clear(this.name);
// A remote clear() delivers a null key, meaning "everything was cleared".
this.keyChanges.next(key ?? KEY_CLEARED);
}));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: SessionStorageStrategy, deps: [{ token: SESSION_STORAGE }, { token: StrategyCacheService }, { token: PLATFORM_ID }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: SessionStorageStrategy }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: SessionStorageStrategy, decorators: [{
type: Injectable
}], ctorParameters: () => [{ type: undefined, decorators: [{
type: Inject,
args: [SESSION_STORAGE]
}] }, { type: StrategyCacheService }, { type: undefined, decorators: [{
type: Inject,
args: [PLATFORM_ID]
}] }, { type: i0.NgZone }] });
class InMemoryStorageStrategy {
static { this.strategyName = StorageStrategies.InMemory; }
constructor(cache) {
this.cache = cache;
this.keyChanges = new Subject();
this.isAvailable = true;
this.name = InMemoryStorageStrategy.strategyName;
}
get(key) {
return of(this.cache.get(this.name, key));
}
set(key, value) {
this.cache.set(this.name, key, value);
this.keyChanges.next(key);
return of(value);
}
del(key) {
this.cache.del(this.name, key);
this.keyChanges.next(key);
return of(void 0);
}
clear() {
this.cache.clear(this.name);
this.keyChanges.next(KEY_CLEARED);
return of(void 0);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: InMemoryStorageStrategy, deps: [{ token: StrategyCacheService }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: InMemoryStorageStrategy }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: InMemoryStorageStrategy, decorators: [{
type: Injectable
}], ctorParameters: () => [{ type: StrategyCacheService, decorators: [{
type: Inject,
args: [StrategyCacheService]
}] }] });
const STORAGE_STRATEGIES = new InjectionToken('STORAGE_STRATEGIES');
const Strategies = [
{ provide: STORAGE_STRATEGIES, useClass: InMemoryStorageStrategy, multi: true },
{ provide: STORAGE_STRATEGIES, useClass: LocalStorageStrategy, multi: true },
{ provide: STORAGE_STRATEGIES, useClass: SessionStorageStrategy, multi: true },
];
const [InMemoryStorageStrategyProvider, LocalStorageStrategyProvider, SessionStorageStrategyProvider] = Strategies;
const StorageStrategyStubName = 'stub_strategy';
class StorageStrategyStub {
constructor(name) {
this.keyChanges = new Subject();
this.store = {};
this._available = true;
this.name = name || StorageStrategyStubName;
}
get isAvailable() {
return this._available;
}
get(key) {
return of(this.store[key]);
}
set(key, value) {
this.store[key] = value;
this.keyChanges.next(key);
return of(value);
}
del(key) {
delete this.store[key];
this.keyChanges.next(key);
return of(void 0);
}
clear() {
this.store = {};
this.keyChanges.next(KEY_CLEARED);
return of(void 0);
}
}
class StorageStub {
constructor() {
this.store = {};
}
get length() {
return Object.keys(this.store).length;
}
clear() {
this.store = {};
}
getItem(key) {
return this.store[key] || null;
}
key(index) {
return Object.keys(this.store)[index];
}
removeItem(key) {
delete this.store[key];
}
setItem(key, value) {
this.store[key] = value;
}
}
const InvalidStrategyError = 'invalid_strategy';
class StrategyIndex {
static { this.index = {}; }
constructor(strategies) {
this.strategies = strategies;
this.registration$ = new Subject();
if (!strategies)
strategies = [];
this.strategies = strategies.reverse()
.map((strategy, index, arr) => strategy.name)
.map((name, index, arr) => arr.indexOf(name) === index ? index : null)
.filter((index) => index !== null)
.map((index) => strategies[index]);
}
static get(name) {
if (!this.isStrategyRegistered(name))
throw Error(InvalidStrategyError);
let strategy = this.index[name];
if (!strategy.isAvailable) {
const fallback = this.index[StorageStrategies.InMemory];
if (fallback === undefined)
throw Error(InvalidStrategyError);
strategy = fallback;
}
return strategy;
}
static set(name, strategy) {
this.index[name] = strategy;
}
static clear(name) {
if (name !== undefined)
delete this.index[name];
else
this.index = {};
}
static isStrategyRegistered(name) {
return name in this.index;
}
static hasRegistredStrategies() {
return Object.keys(this.index).length > 0;
}
getStrategy(name) {
return StrategyIndex.get(name);
}
indexStrategies() {
this.strategies.forEach((strategy) => this.register(strategy.name, strategy));
}
indexStrategy(name, overrideIfExists = false) {
if (StrategyIndex.isStrategyRegistered(name) && !overrideIfExists)
return StrategyIndex.get(name);
const strategy = this.strategies.find((candidate) => candidate.name === name);
if (!strategy)
throw new Error(InvalidStrategyError);
this.register(name, strategy, overrideIfExists);
return strategy;
}
register(name, strategy, overrideIfExists = false) {
if (!StrategyIndex.isStrategyRegistered(name) || overrideIfExists) {
StrategyIndex.set(name, strategy);
this.registration$.next(name);
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: StrategyIndex, deps: [{ token: STORAGE_STRATEGIES, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: StrategyIndex, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.2", ngImport: i0, type: StrategyIndex, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}], ctorParameters: () => [{ type: undefined, decorators: [{
type: Optional
}, {
type: Inject,
args: [STORAGE_STRATEGIES]
}] }] });
class LocalStorageService extends SyncStorage {
}
function buildService$1(index) {
const strategy = index.indexStrategy(StorageStrategies.Local);
return new SyncStorage(strategy);
}
const LocalStorageServiceProvider = {
provide: LocalStorageService,
useFactory: buildService$1,
deps: [StrategyIndex]
};
class SessionStorageService extends SyncStorage {
}
function buildService(index) {
const strategy = index.indexStrategy(StorageStrategies.Session);
return new SyncStorage(strategy);
}
const SessionStorageServiceProvider = {
provide: SessionStorageService,
useFactory: buildService,
deps: [StrategyIndex]
};
/**
* Per-strategy-instance, per-key revision signals.
*
* Angular 22 made OnPush the default change detection strategy for every
* component, and these decorators install a plain property accessor: reading it
* from a template registers no dependency, so a storage change that happens
* outside the component never marks the view dirty and the binding goes stale.
* Under zoneless it is worse — no tick is scheduled at all.
*
* Reading a signal inside the getter makes the consuming view a reactive
* consumer of that signal, so bumping it on a `keyChanges` notification
* schedules change detection in zoned and zoneless applications alike, with no
* change required in consumer code.
*
* Keyed by strategy instance (not by name) so that a bootstrap registering a
* fresh strategy instance gets fresh signals rather than bumping another
* instance's. Note this is only about correctness of the fan-out, not about
* releasing memory: strategy instances are effectively immortal anyway — the
* static StrategyIndex keeps the registered instance, and Local/Session
* strategies register a window 'storage' listener that is never removed — so
* each instance permanently retains its subscription and its per-key signal
* map (one small Map per instance; bounded by the decorated keys ever read).
*/
const revisions = new WeakMap();
function revisionOf(strategy, storageKey) {
let byKey = revisions.get(strategy);
if (byKey === undefined) {
const created = new Map();
byKey = created;
revisions.set(strategy, created);
// One subscription per strategy instance, fanned out to the per-key signals.
// A null key (KEY_CLEARED) means "everything was cleared".
// The bumps run synchronously inside keyChanges.next(), i.e. inside the
// storage WRITE's call stack. `untracked` lifts Angular's reactive-context
// write guard so a write issued from a guarded context (a `computed()`, a
// template expression) updates the revision instead of throwing NG0600.
strategy.keyChanges.subscribe((changed) => untracked(() => {
if (changed === KEY_CLEARED)
created.forEach((revision) => revision.update((n) => n + 1));
else
created.get(changed)?.update((n) => n + 1);
}));
}
let revision = byKey.get(storageKey);
if (revision === undefined)
byKey.set(storageKey, revision = signal(0));
return revision;
}
class DecoratorBuilder {
static buildSyncStrategyDecorator(strategyName, prototype, propName, key, defaultValue = null) {
const rawKey = key || propName;
let storageKey;
Object.defineProperty(prototype, propName, {
get: function () {
const strategy = StrategyIndex.get(strategyName);
// Registers the reading view as a consumer; see `revisions` above.
revisionOf(strategy, getKey())();
let value;
strategy.get(getKey()).subscribe((result) => value = result);
return value === undefined ? defaultValue : value;
},
set: function (value) {
StrategyIndex.get(strategyName).set(getKey(), value).subscribe(noop);
}
});
function getKey() {
if (storageKey !== undefined)
return storageKey;
return storageKey = StorageKeyManager.normalize(rawKey);
}
}
}
function LocalStorage(key, defaultValue) {
return function (prototype, propName) {
DecoratorBuilder.buildSyncStrategyDecorator(StorageStrategies.Local, prototype, propName, key, defaultValue);
};
}
function SessionStorage(key, defaultValue) {
return function (prototype, propName) {
DecoratorBuilder.buildSyncStrategyDecorator(StorageStrategies.Session, prototype, propName, key, defaultValue);
};
}
const LIB_CONFIG = new InjectionToken('ngx_webstorage_config');
var InternalNgxWebstorageFeatureKind;
(function (InternalNgxWebstorageFeatureKind) {
InternalNgxWebstorageFeatureKind[InternalNgxWebstorageFeatureKind["Config"] = 1] = "Config";
InternalNgxWebstorageFeatureKind[InternalNgxWebstorageFeatureKind["LocalStorage"] = 2] = "LocalStorage";
InternalNgxWebstorageFeatureKind[InternalNgxWebstorageFeatureKind["SessionStorage"] = 3] = "SessionStorage";
})(InternalNgxWebstorageFeatureKind || (InternalNgxWebstorageFeatureKind = {}));
function appInit() {
const config = inject(LIB_CONFIG);
const index = inject(StrategyIndex);
return () => {
StorageKeyManager.consumeConfiguration(config);
index.indexStrategies();
};
}
/**
* Provide ngx-webstorage basic features.
*
* - You can customise the configuration with the `withConfiguration` feature.
* - You can enable the `LocalStorage` features with the `withLocalStorage` feature.
* - You can enable the `SessionStorage` features with the `withSessionStorage` feature.
*
* @default config { prefix: 'ngx-webstorage', separator: '|', caseSensitive: false }
*/
function provideNgxWebstorage(...features) {
const { configProvider, featureProviders } = parseFeatures(features);
return makeEnvironmentProviders([
configProvider,
InMemoryStorageStrategyProvider,
provideAppInitializer(() => {
const initializerFn = (appInit)();
return initializerFn();
}),
...featureProviders,
]);
}
function parseFeatures(features) {
let configProvider;
const featureProviders = [];
const parsedFeatures = new Set();
for (const feature of features) {
if (parsedFeatures.has(feature.kind))
throw new Error(`Feature ${feature.kind} is already provided.`);
if (feature.kind === InternalNgxWebstorageFeatureKind.Config) {
configProvider = feature.providers[0];
}
else
featureProviders.push(...feature.providers);
parsedFeatures.add(feature.kind);
}
return {
configProvider: configProvider ?? {
provide: LIB_CONFIG,
useValue: { prefix: DefaultPrefix, separator: DefaultSeparator, caseSensitive: DefaultIsCaseSensitive }
},
featureProviders
};
}
function makeNgxWebstorageFeature(kind, providers) {
return { kind, providers };
}
function withNgxWebstorageConfig(config) {
return makeNgxWebstorageFeature(InternalNgxWebstorageFeatureKind.Config, [{ provide: LIB_CONFIG, useValue: config }]);
}
/** Provides everything necessary to use the `LocalStorage` features. */
function withLocalStorage() {
return makeNgxWebstorageFeature(InternalNgxWebstorageFeatureKind.LocalStorage, [
LocalStorageProvider,
LocalStorageServiceProvider,
LocalStorageStrategyProvider,
]);
}
function withSessionStorage() {
return makeNgxWebstorageFeature(InternalNgxWebstorageFeatureKind.SessionStorage, [
SessionStorageProvider,
SessionStorageServiceProvider,
SessionStorageStrategyProvider,
]);
}
/*
* Public API Surface of ngx-webstorage
*/
/**
* Generated bundle index. Do not edit.
*/
export { AsyncStorage, CompatHelper, InMemoryStorageStrategy, InternalNgxWebstorageFeatureKind, InvalidStrategyError, LIB_CONFIG, LOCAL_STORAGE, LocalStorage, LocalStorageService, LocalStorageStrategy, SESSION_STORAGE, STORAGE_STRATEGIES, SessionStorage, SessionStorageService, SessionStorageStrategy, StorageStrategies, StorageStrategyStub, StorageStrategyStubName, StorageStub, StrategyCacheService, StrategyIndex, SyncStorage, makeNgxWebstorageFeature, provideNgxWebstorage, withLocalStorage, withNgxWebstorageConfig, withSessionStorage };
//# sourceMappingURL=ngx-webstorage.mjs.map