Storage & Config Services
These two services handle persistent data in the application. storeService provides async, robust key-value storage backed by IndexedDB (or SQLite on native), suitable for large data like cached objects and session recovery data. configService provides synchronous, lightweight per-element configuration backed by localStorage, suitable for small UI preferences. Understanding when to use each is important for both correctness and performance.
storeService
Introduction
storeService wraps @ionic/storage (which uses IndexedDB, SQLite, or localStorage depending on the platform) with a mutex mechanism that prevents concurrent read/write races on the same key. It exists because the application frequently performs rapid Set followed by Get on the same key (e.g. during login when the device ID is saved and immediately read back), and without serialisation these operations can interleave and return stale data.
When to use
- Persisting session-related data that must survive page reloads (e.g. device UUID, cached sync state).
- Storing large or structured data that does not fit in
localStorage(IndexedDB has much higher quotas). - Any async storage operation where you need guaranteed ordering.
Do not use storeService for small, synchronous UI preferences -- use configService for those.
How it works
storeService.Set('key', value)
│
▼
_AddCompleted('key') ◄── waits for any in-progress operation on same key
│
▼
JSON.stringify(value)
│
▼
Ionic Storage.set('key', serialized)
│
▼
_DelCompleted('key') ◄── releases the lock, notifying next waiter
Mutex mechanism: Each key has an independent lock implemented with RxJS Subject. When an operation starts, _AddCompleted(key) checks if another operation is in progress on the same key. If so, it waits for the Subject to emit (signalling completion). Then it creates a new Subject for this operation. When the operation finishes, _DelCompleted(key) calls next() and complete() on the Subject, releasing any waiters.
Lazy initialisation: The underlying Storage instance is created on the first call to Get, Set, or Del via an internal Initialize() method. This means the storage driver is not loaded until it is actually needed.
import { storeService } from '@unpispas/upp-base';
constructor(private store: storeService) {}
Methods
| Method | Signature | Description |
|---|---|---|
Get(key) | (key: string): Promise<any | null> | Retrieves a value by key. The stored JSON string is parsed before returning. Returns null if the key does not exist or JSON parsing fails. Acquires the key's mutex before reading. |
Set(key, value) | (key: string, value: any): Promise<void> | Stores a value (serialised to JSON). Acquires the key's mutex before writing. Throws an Error with key name and byte count on failure (e.g. storage quota exceeded). |
Del(key) | (key: string): Promise<void> | Removes a key. After removal, performs a verification read. If the key still exists (a known driver bug on some platforms), it overwrites the value with null as a fallback. |
Usage examples
Saving and retrieving user preferences
// Save structured data
await this.store.Set('user-prefs', { theme: 'dark', lang: 'ES', zoom: 1.2 });
// Retrieve it later (possibly in another component or after reload)
const prefs = await this.store.Get('user-prefs');
if (prefs) {
this.applyTheme(prefs.theme);
this.setLanguage(prefs.lang);
}
Persisting the device UUID during identification
// In identificationService.DeviceId():
const deviceId = await this.store.Get('upp-stored-deviceuuid-v2');
if (!deviceId) {
// Calculate fingerprint, register with server, then persist
const newId = await this.calculateAndRegisterDevice();
await this.store.Set('upp-stored-deviceuuid-v2', newId);
}
Safe deletion with verification
await this.store.Del('cached-sync-data');
// The Del method internally verifies the key was actually removed
// and overwrites with null if the driver failed to delete it
Common patterns
Using storeService for session recovery
// Before logging out, persist enough state to recover the session
await this.store.Set('session-recovery', {
deviceId: this.state.device,
lastPlace: this.state.place,
timestamp: Date.now()
});
// On app startup, check for a recoverable session
const recovery = await this.store.Get('session-recovery');
if (recovery && (Date.now() - recovery.timestamp < 86400000)) {
// Session is less than 24 hours old -- attempt recovery
await this.recoverSession(recovery);
}
Gotchas and tips
- All values are JSON-serialized: you can store objects, arrays, strings, numbers, and booleans. You cannot store
undefined, functions, or circular references. - The mutex is per-key, not global: operations on different keys run concurrently. Only operations on the same key are serialised.
Delhas a fallback: on some storage drivers,remove()does not actually delete the key. The service works around this by overwriting withnull. This means a "deleted" key returnsnullfromGet, but may still physically exist in storage.- Storage initialisation is async: the first
Get/Set/Delcall includes the time to initialise the storage driver. Subsequent calls are faster. - Error on quota exceeded:
Setthrows anErrorwith the key name and data size if the write fails. Callers should handle this for large data.
configService
Introduction
configService provides per-element persistent configuration using localStorage. Unlike storeService (which is async and uses IndexedDB), configService is synchronous and lightweight -- designed for small settings like view preferences, feature flags, and UI state. It organises configuration into named "elements" (e.g. VIEW, EDITOR), each stored as a single JSON object in localStorage.
When to use
- Small UI preferences that need to survive page reloads: kiosk mode, POS mode, theme choice, column widths, panel states.
- Per-feature configuration where each feature has a few boolean or string settings.
- Situations where synchronous reads are important -- you cannot
awaitin a getter or in template binding expressions, but you can read fromconfigServicesynchronously.
Do not use configService for large data, structured caches, or data that needs IndexedDB quotas -- use storeService for those.
How it works
configService.getConfiguration('VIEW')
│
▼
new Configuration('VIEW', configService)
│ calls config.getElement('VIEW')
│ │
│ ▼
│ localStorage.getItem('upp_cfg_VIEW')
│ JSON.parse(...)
│ │
▼ ▼
Configuration instance (cached in _served Map)
│
▼ .set('kiosk', true)
updates in-memory data
│ if autoCommit:
▼
configService.setElement('VIEW', data)
│
▼
localStorage.setItem('upp_cfg_VIEW', JSON.stringify(data))
│
▼
OnUpdated.next()
The configService maintains a cache of Configuration instances (one per element name). Calling getConfiguration('VIEW') twice returns the same instance. Each Configuration holds its data in memory and syncs to localStorage on every set() call (when autoCommit is true) or on explicit commit().
import { configService, Configuration, ConfigValue } from '@unpispas/upp-base';
constructor(private config: configService) {}
Types
type ConfigValue = string | number | boolean | object | null;
configService methods
| Method | Signature | Description |
|---|---|---|
getConfiguration(element) | (element: string): Configuration | Returns a Configuration instance for the named element. Instances are cached -- the same object is returned on repeated calls with the same element name. |
setElement(element, data) | (element: string, data: object): void | Low-level: serialises and stores the entire element object in localStorage under key upp_cfg_{element}. Normally called by Configuration.commit(). |
getElement(element) | (element: string): object | null | Low-level: retrieves and parses an element from localStorage. Returns null if the key does not exist or parsing fails. |
Configuration class
Each Configuration instance represents one named element's settings.
| Member | Type | Description |
|---|---|---|
get(item) | (item: string): ConfigValue | Returns the value for a key, or undefined if not set. |
set(item, value) | (item: string, value: ConfigValue): void | Sets a key's value. If value is null, the key is removed from the element. Auto-commits to localStorage if autoCommit is true. |
commit() | (): void | Persists all pending changes to localStorage. Only writes if there are actual changes (tracked by an internal dirty flag). |
autoCommit | boolean | Whether set() automatically calls commit(). Default: true. Set to false for batch updates. |
OnUpdated | Observable<void> | Emits after each successful commit(). Subscribe to react to configuration changes from elsewhere. |
Storage format
Data is stored in localStorage with keys prefixed upp_cfg_. Each element is a single JSON object:
Key: upp_cfg_VIEW
Value: {"kiosk":true,"ispos":false}
Key: upp_cfg_EDITOR
Value: {"showGrid":true,"gridSize":50}
Usage examples
Reading and writing a single setting
const viewConfig = this.config.getConfiguration('VIEW');
// Read a setting (synchronous)
const isKiosk = viewConfig.get('kiosk') as boolean;
// Write a setting (auto-committed to localStorage immediately)
viewConfig.set('kiosk', true);
Batch updates to avoid multiple writes
const viewConfig = this.config.getConfiguration('VIEW');
// Disable auto-commit for batch operations
viewConfig.autoCommit = false;
viewConfig.set('kiosk', true);
viewConfig.set('ispos', false);
viewConfig.set('scrollbar', true);
viewConfig.commit(); // single write to localStorage
// Re-enable auto-commit for future changes
viewConfig.autoCommit = true;
Reacting to configuration changes
const viewConfig = this.config.getConfiguration('VIEW');
viewConfig.OnUpdated.subscribe(() => {
// Another part of the application changed the VIEW config
this.refreshSettings();
});
Removing a setting
const viewConfig = this.config.getConfiguration('VIEW');
viewConfig.set('obsoleteSetting', null); // removed from the element
Common patterns
viewService using configService for persistence
viewService uses configService internally to persist the Kiosk and IsPOS flags:
// Inside viewService
get Kiosk(): boolean {
return this.configuration.get(UppViewConfiguration.kiosk) as boolean || this.platform._isKiosk;
}
set Kiosk(value: boolean) {
this.configuration.set(UppViewConfiguration.kiosk, value);
this.platform._isKiosk = value;
}
This pattern -- reading from configService and pushing the value to another service -- is common for settings that affect both persistent state and runtime behaviour.
Feature-specific configuration
// Each feature creates its own configuration element
const gridConfig = this.config.getConfiguration('PRODUCT_GRID');
const columns = gridConfig.get('columns') as number || 4;
const showPrices = gridConfig.get('showPrices') as boolean ?? true;
Gotchas and tips
configServiceis synchronous: all reads and writes happen immediately. There is noawait. This makes it ideal for template bindings and getters.localStoragehas a ~5 MB limit: do not store large data. Each element should be a small JSON object (a few kilobytes at most).Configurationinstances are singletons per element:getConfiguration('VIEW')always returns the same object. This means changes made anywhere are visible everywhere.autoCommitistrueby default: everyset()call writes tolocalStorage. For batch operations, disable it temporarily.set(item, null)removes the key: this is the way to delete a configuration key from an element.get()returnsundefinedfor missing keys: notnull. Use nullish coalescing (??) or a fallback value.
Choosing between storeService and configService
| Criteria | storeService | configService |
|---|---|---|
| Storage backend | IndexedDB / SQLite | localStorage |
| API | Async (Promise-based) | Synchronous |
| Capacity | Large (hundreds of MB) | Small (~5 MB total) |
| Use case | Session data, caches, large objects | UI preferences, feature flags |
| Thread safety | Per-key mutex | None needed (synchronous) |
| Data format | Any JSON-serializable value | ConfigValue (string, number, boolean, object, null) |
Related services
| Service | Relationship |
|---|---|
viewService | Uses configService to persist VIEW preferences (kiosk, ispos). |
identificationService | Uses storeService to persist the device UUID. |
preloadService | Could use storeService for image cache metadata. |
logsService | Uses localStorage directly for the campaign key (a simple string). |