Skip to main content

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

MethodSignatureDescription
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.
  • Del has a fallback: on some storage drivers, remove() does not actually delete the key. The service works around this by overwriting with null. This means a "deleted" key returns null from Get, but may still physically exist in storage.
  • Storage initialisation is async: the first Get/Set/Del call includes the time to initialise the storage driver. Subsequent calls are faster.
  • Error on quota exceeded: Set throws an Error with 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 await in a getter or in template binding expressions, but you can read from configService synchronously.

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

MethodSignatureDescription
getConfiguration(element)(element: string): ConfigurationReturns 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): voidLow-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 | nullLow-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.

MemberTypeDescription
get(item)(item: string): ConfigValueReturns the value for a key, or undefined if not set.
set(item, value)(item: string, value: ConfigValue): voidSets a key's value. If value is null, the key is removed from the element. Auto-commits to localStorage if autoCommit is true.
commit()(): voidPersists all pending changes to localStorage. Only writes if there are actual changes (tracked by an internal dirty flag).
autoCommitbooleanWhether set() automatically calls commit(). Default: true. Set to false for batch updates.
OnUpdatedObservable<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

  • configService is synchronous: all reads and writes happen immediately. There is no await. This makes it ideal for template bindings and getters.
  • localStorage has a ~5 MB limit: do not store large data. Each element should be a small JSON object (a few kilobytes at most).
  • Configuration instances are singletons per element: getConfiguration('VIEW') always returns the same object. This means changes made anywhere are visible everywhere.
  • autoCommit is true by default: every set() call writes to localStorage. 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() returns undefined for missing keys: not null. Use nullish coalescing (??) or a fallback value.

Choosing between storeService and configService

CriteriastoreServiceconfigService
Storage backendIndexedDB / SQLitelocalStorage
APIAsync (Promise-based)Synchronous
CapacityLarge (hundreds of MB)Small (~5 MB total)
Use caseSession data, caches, large objectsUI preferences, feature flags
Thread safetyPer-key mutexNone needed (synchronous)
Data formatAny JSON-serializable valueConfigValue (string, number, boolean, object, null)
ServiceRelationship
viewServiceUses configService to persist VIEW preferences (kiosk, ispos).
identificationServiceUses storeService to persist the device UUID.
preloadServiceCould use storeService for image cache metadata.
logsServiceUses localStorage directly for the campaign key (a simple string).