UNPKG

ngx-webstorage

Version:

Local and session storage services and decorators for Angular

666 lines (639 loc) 26.4 kB
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