All files / modules store.ts

85.71% Statements 48/56
40% Branches 4/10
100% Functions 8/8
85.45% Lines 47/55

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 1762x 2x   2x                   2x         10x           10x                   11x 11x                         6x             6x 6x 6x   6x 2x 2x     1x       4x                     4x             4x 4x 4x 3x     1x                   2x 2x       2x 2x 2x     2x 2x                       10x               12x   12x 12x 1x     12x 1x   1x 1x 1x                   12x     12x               11x 11x 10x 10x     11x      
import { Injectable } from '@angular/core';
import { Storage } from '@ionic/storage';
 
import { Subject } from 'rxjs';
 
/**
 * @class storeService
 * @description Provides methods to interact with the storage system, including key-value operations (`Get`, `Set`, `Del`).
 * Supports scoped keys and ensures thread-safe access with a mutex-based locking mechanism.
 */
@Injectable({
    providedIn: 'root'
})
export class storeService {
    /**
     * The underlying storage instance.
     * Initialized using the `Initialize` method.
     */    
    private _storage: Storage | null = null;
 
    /**
     * Constructor to inject the storage dependency.
     * @param storage - An instance of `@ionic/storage` for data persistence.
     */    
    constructor(private storage: Storage){
        // nothing to do
    }
 
 
    /**
     * Initializes the storage instance and generates a unique tab/session identifier.
     * @returns A promise that resolves to `true` if the storage is initialized successfully, or `false` otherwise.
     */    
    private async Initialize(): Promise <boolean> {
        this._storage = await this.storage.create();
        return true;
    }
 
    /****************************/
    /* STORAGE ACCESS           */
    /****************************/
 
    /**
     * Retrieves a value from the storage by key.
     * @param key - The key associated with the stored value.
     * @returns A promise that resolves to the stored value (parsed as JSON if applicable) or `null` if the key does not exist.
     */    
    async Get(key: string): Promise <any | null> {
        Iif (!this._storage){
            await this.Initialize();
            Iif (!this._storage){
                return null;
            }
        }
 
        await this._AddCompleted(key);
        const _data = await this._storage.get(key);
        this._DelCompleted(key);
 
        if (_data){
            try {
                return JSON.parse(_data);
            }
            catch {
                return null;    // not json encoded
            }
        }
 
        return null;
    }
 
    /**
     * Stores a value in the storage under the specified key.
     * @param key - The key under which the value will be stored.
     * @param value - The value to be stored (serialized to JSON).
     * @returns A promise that resolves once the operation is complete.
     * @throws An error if the operation fails.
     */    
    async Set(key: string, value: any): Promise <void> {
        Iif (!this._storage){
            await this.Initialize();
            Iif (!this._storage){
                return;
            }
        }
 
        try {
            await this._AddCompleted(key);
            await this._storage.set(key, JSON.stringify(value))
            this._DelCompleted(key);
        }
        catch(error: any){
            throw new Error("[STORAGE] Cannot set value in key '" + key + "' (" + value.length + " bytes) - " + error.name + "(" + error.message + ")");
        }
    }
 
    /**
     * Deletes a value from the storage by key.
     * @param key - The key associated with the value to be deleted.
     * @returns A promise that resolves once the operation is complete.
     */    
    async Del(key: string): Promise <void> {
        console.info("[STORAGE] removing key '" + key + "'");
        Iif (!this._storage){
            return;     // not intialized
        }
 
        await this._AddCompleted(key);
        await this._storage.remove(key);
        this._DelCompleted(key);
 
        // check that it has been removed (and clear if not)
        const _data = await this.Get(key);
        Iif (_data){     // not removed workarround. Clear the content
            await this.Set(key, null);
        }
    }    
 
    /****************************/
    /* SAVE ACCESS TO THE KEY   */
    /****************************/
 
    /**
     * A map of mutexes for managing concurrent access to keys.
     */    
    private _mutexes = new Map < string, Subject<void> > ();
 
    /**
     * Waits for the completion of any ongoing operation for the specified key.
     * @param key - The key to check for concurrent operations.
     * @returns A promise that resolves once the ongoing operation completes.
     */    
    private async _GetCompleted(key: string): Promise <void> {
        let _observable: any | null = null;
 
        const _subject = this._mutexes.get(key);
        if (_subject){
            _observable = _subject.asObservable();
        }
 
        if (_observable){     // writing in progress
            const _ini = performance.now();
 
            console.info("[STORE] waiting for write [" + key + "] to be completed..")
            await _observable;
            console.info("[STORE] write [" + key + "] completed in " + (performance.now() - _ini).toFixed(2) + " ms")
        }
    }
 
    /**
     * Adds a mutex lock for the specified key to prevent concurrent access.
     * @param key - The key to lock.
     * @returns A promise that resolves once the lock is added.
     */    
    private async _AddCompleted(key: string): Promise <void> {
        await this._GetCompleted(key);
 
        // the key is free: block next ones
        this._mutexes.set(key, new Subject<void>())
    }
 
    /**
     * Releases the mutex lock for the specified key.
     * @param key - The key to unlock.
     */    
    private _DelCompleted(key: string): void {
        const _subject = this._mutexes.get(key);
        if (_subject){
            _subject.next();        // notify
            _subject.complete();    // release
        }
 
        this._mutexes.delete(key);
    }
}