0

I'm designing an API in Javascript part of an Angular with typescript project.

I have involved later in this project implemented. There is a DataStorageService which I checked from git logs that essentially like this in the beginning.

@Injectable({  //Injected an instance of it to DI container in startup by angular.
  providedIn: 'root'
})
export class DataStorageService {
  setLocalStorage(keyName: string, data: string) {
    localStorage.setItem(keyName, data);
  }
  getLocalStorage(keyName: string) {
    return localStorage.getItem(keyName);
  }
  removeLocalStorage(keyName: string) {
    localStorage.removeItem(keyName);
  }

  setSessionStorage(keyName: string, data: string) {
    sessionStorage.setItem(keyName, data);
  }
  getSessionStorage(keyName: string) {
    return sessionStorage.getItem(keyName);
  }
  removeSessionStorage(keyName: string) {
    sessionStorage.removeItem(keyName);
  }
}

Thus I thought that it was just a wrapper to methods of localStorage or sessionStrage which are native Storage typed objects provided as global and having methods of

setItem(key, value)
getItem(key)
removeItem(key)
and three more
//lib.dom.d.ts
declare var sessionStorage: Storage;
declare var localStorage: Storage;

While the method names are a bit wrong and I found it unnecessary just introduce dummy wrappers I have realized that the design changed its direction to something different when I see these other methods added later on:

  getUserMedia(userId: string) {
    //...
  }

  uploadUserPhoto(file: File, userId: string) {
    //...
  }

So I have guessed that it was meant to be a gateway to storage operations over ApiService which has no business to do with local Storage.

Then later this method has introduced:

//roughly
async getSomethingFromCacheOr(keyname: string, /*additional params*/) {
    //check sessionstorage from cache
    if(this.getSessionStorage("myKey")) {
      //return
    } else { //mismatch
      let data = = this.apiService.getSomething();
      if(data) {
         //do something
         this.setSessionStorage("myKey", data); //put the cache 
         //return
      }
    }
}

I have had a requirement to implement similar functionality to this. So I have started to implement under it.

Then I have realized that I need a expirable cache, so I have implemented this functionality around getItem and setItem calls and then I have needed somehow secured cache. While implementing this again in same way I have thought that I could make them decoratables thus make it reusable and composable by other clients without copy/pasting expire and secure logics.

With this setup I have thought that I can move my method getSomething() to a separate service to use dataStorageService from there and also other clients can use some of them or compose them.

Then I have designed this.

  /**
   * Returns a handle to global localStorage
   */
  public getLocalStore(): Storage {
    return localStorage;
  }

  /**
   * Returns a handle to global sesionStorage
   */
  public getSessionStore(): Storage {
    return sessionStorage;
  }

  /** 
   * Recommended: Global/Singleton - prefer for ordinary operations
   */
  public getExpirableSecureLocalStorage(): Storable {
    return this.expirableSecureLocalStorage;
  }

  /** 
   * Recommended: Global/Singleton - prefer for ordinary operations
   */
  public getSecureLocalStorage(): Storable {
    return this.secureLocalStorage;
  }

  /** 
   * Recommended: Global/Singleton - prefer for ordinary operations
   */
  public getExpirableLocalStorage(): Storable {
    return this.expirableLocalStorage;
  }

  /**
   * Use when you need different TTL than default. 
   * @param ttl optional, in that case gets from environment.
   */
  public createExpirableSecureLocalStorage(ttl: number = this.appConfig.localStorageTTL): Storable {
    return this.createExpirableStorage(this.createSecureStorage(this.getLocalStore), ttl); 
  }

  /**
   * Use when you need different TTL than default. 
   * @param ttl optional, in that case gets from environment.
   */
  public createExpirableSecureSessionStorage(ttl: number = this.appConfig.localStorageTTL): Storable {
    return this.createExpirableStorage(this.createSecureStorage(this.getSessionStore), ttl);
  }

  /**
   * Prefer getSecureStorage for ordinary operations 
   * @param storage a storage to be decorated.
   */
  public createSecureStorage(storage: any): Storable { 
    var cipherKeyMap = this.keyMapping;
    let privateState = function() {
        let cipherKey;
        this.encrypt = function(key: string, data: string): string {
          let entry = cipherKeyMap.find(entry => entry.key === key);
          if (entry) {
            cipherKey = entry.cipherKey;
          } else {
            cipherKey = //randomly generate
            cipherKeyMap.push({"key": key, "cipherKey": cipherKey});
          }
          //encrypt
          return encryptedHex;
        };

        this.decrypt = function(key: string, data: string): string {
          let entry = cipherKeyMap.find(entry => entry.key === key);
          if (entry) {
            cipherKey = entry.cipherKey;
          } else {
            //This is the case of application state wipe out when clients use local storage to keep data encrypted by this wiped out key.
            //so clients can use both localStorage or sessionStorage without concerning application state.
            return; 
          }
          //decrypt
          return decryptedText;
        };
    }
    let state = new privateState();

    return {
      setItem(key: string, value: string) {
        storage.setItem(key, state.encrypt(key, value));
      },
      getItem(key: string) {
        let encryptedStr = storage.getItem(key);
        if(encryptedStr) {
          let decryptedStr = state.decrypt(key, encryptedStr);
          return decryptedStr;
        } else {
          return null;
        }
      },
      removeItem(key: string) {
        storage.removeItem(key);
        cipherKeyMap.splice(cipherKeyMap.findIndex(r => r.key === key), 1);
      }
    } 
  }
  
  /**
   * Use when you need different TTL than default. 
   * @param storage a storage to be decorated.
   */
  public createExpirableStorage(storage: any, ttl: number = this.appConfig.localStorageTTL): Storable {
    function setExpiry(value: string, ttl: number) {
      let item = {
        value: value, 
        expiry: new Date().getTime() + ttl
      };
      return item;
    }
    function getExpiry(keyName: string) {
      const itemStr = storage.getItem(keyName);
      if (!itemStr) return null;
      const item = JSON.parse(itemStr);
      if (new Date().getTime() > item.expiry) {
        storage.removeItem(keyName);
        return null;
      }
      return item.value
    }
    return {
      setItem(key: string, value: string) {
        storage.setItem(key, JSON.stringify(setExpiry(value, ttl)));
      },
      getItem(key: string) {
        return getExpiry(key);
      },
      removeItem(key: string) {
        storage.removeItem(key);
      }
    }
  }

/**
 * Wrapper to native Storage but only concerned with this methods in the public API which we provide.  
 * - use native localStorage or sessionStorage objects when in the need of other methods  
 * i.e length, key, clear   
 * - or just use Storage instead of this by implementing other methods in the concretes of it.   
 */
interface Storable {
  setItem(key: string, value: string): any;
  getItem(key: string): string;
  removeItem(key: string): any;
}

As you see I have tried to return an object with inherently static methods since I had no state which I mean values are persisted. Then I have needed closures to have private methods as seen from createExpirableStorage so I have just make them functions outside of return.

I had to keep ttl state, but handled by persisting it also in the local storage.

In the createSecureStorage I have needed to dynamically generate cipher keys, so I had a state to manage with closures. I have tried something but then kept cipher keys mapped by keys of key/value pairs in a global object.

While thinking how to expose which public methods to clients I have concerned with these two things:

  1. What happens when I create the instances and provide that instances to clients since these are like static methods with no own state? With this way both he methods in dataStorageServices and other services could use the same instances. So I think I would have singletons in that case, is that right?
@Injectable({
  providedIn: 'root'
})
export class DataStorageService {
  private expirableSecureLocalStorage:Storable;
  private secureLocalStorage:Storable;
  private expirableLocalStorage:Storable;
  constructor(/*...*/) {
    this.expirableSecureLocalStorage = this.createExpirableSecureLocalStorage();
    this.secureLocalStorage = this.createSecureStorage(localStorage);
    this.expirableLocalStorage = this.createExpirableStorage(localStorage)
  }
  1. What happends clients could create the objects since createExpirableStorage(storage: any, ttl: number = this.appConfig.localStorageTTL) TTL value is got from configuration to make it dynamically changable, some clients would need other TTL values? If the clients use it like below does it results excessive objects in memory or could objects incrementally grow over time? I have tried to test it but I don't have extensive Garbage Collection for closures knowledge. Would I need to put ttl and cipherKeys to the state of closures instead of keeping them globals in this case and if so how can I do that?
//with 
this.dataStorageService.createExpirableSecureLocalStorage().setItem("dynamiccreateExpirableSecureLocalStorage", "mydata");

Exposing both get and create methods seems confusing for clients so I have thought that I need to choose one of them.

How and which kind of methods should I expose this API design?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.