UNPKG

5.78 kBJavaScriptView Raw
1// @flow
2import * as R from "ramda";
3
4import {
5 noop,
6 parseJsonIfNeeded,
7} from "@applicaster/zapp-react-native-utils/functionUtils";
8
9export type StorageI = {
10 setItem: (string, string, ?string) => Promise<any>,
11 getItem: (string, ?string) => Promise<any>,
12 getAllItems: () => Promise<any>,
13};
14
15export const STORAGE_TYPES = ["localStorage", "sessionStorage"];
16export const DEFAULT_NAMESPACE = "applicaster.v2";
17
18const NAMESPACE_SEPARATOR = "_::_";
19
20/**
21 * returns a key with the namespace, if provided
22 * @param {String} key
23 * @param {?String} namespace
24 * @returns {String}
25 */
26export function applyNamespaceToKeyName(
27 key: string,
28 namespace: ?string
29): string {
30 return namespace ? `${namespace}${NAMESPACE_SEPARATOR}${key}` : key;
31}
32
33/**
34 * this function will try to invoke a function,
35 * run JSON.parse on its result, then resolve. If any error is caught
36 * it will reject with the error. JSON parsing won't yield an error since we
37 * use a method which will return its input if the parsing fails
38 * @param {Function} fn function to run
39 * @returns {Promise}
40 */
41function tryAndResolve(fn: (any) => any): Promise<any> {
42 try {
43 return R.compose(
44 (value) => Promise.resolve(value),
45 parseJsonIfNeeded,
46 fn
47 )();
48 } catch (error) {
49 return Promise.reject(error);
50 }
51}
52
53/**
54 * Gets a storage object from the window global object for web environment
55 * if the storage type required is not implemented for some reason, a warning will
56 * be sent, and a noop function will be returned. Will otherwise return an object with
57 * methods to interact with the storage
58 * @param {string} type of storage (localStorage or sessionStorage)
59 * @returns {Object} storageObject
60 * @returns {Function} storageObject.callMethod: invokes methods on the storage object
61 * @returns {Function} storageObject.getKeys: returns all existing keys in the storage object
62 */
63function getStorageObject(type: string): (any) => void | Promise<any> {
64 const storage = R.prop(type, window);
65
66 if (!R.includes(type, STORAGE_TYPES) || !storage) {
67 // eslint-disable-next-line no-console
68 console.warn(`Storage type ${type} does not exist in this environment`);
69 return {
70 callMethod: noop,
71 };
72 }
73
74 /**
75 * calls a method on the storage object
76 * @param {String} method to invoke
77 * @param {Array} args array of arguments to use when invoking the method
78 * @returns response of the method call
79 */
80 function callMethod(method: string, ...args: [any]): Promise<any> {
81 return storage[method](...args);
82 }
83
84 /**
85 * returns the list of keys in the storage object
86 * @returns {Array<String>}
87 */
88 function getKeys() {
89 // the test mock doesn't have exactly the same implementation for returning
90 // allkeys, hence this check
91 if (process.env.NODE_ENV === "test") {
92 return storage.keys;
93 }
94 return R.keys(storage);
95 }
96
97 return {
98 callMethod,
99 getKeys,
100 };
101}
102
103/**
104 * returns an object to interact with a storage module (localStorage or sessionStorage)
105 * @param {String} type of storage
106 * @returns {Object}
107 */
108export function getStorageModule(type: string): StorageI {
109 const Storage = getStorageObject(type);
110
111 /**
112 * sets an item in storage. Returns a promise which will
113 * resolve to true if the value was set, or reject with an Error if not
114 * @param {String} key
115 * @param {String} value (already stringified)
116 * @param {?String} namespace
117 * @returns {Promise<Boolean|Error>}
118 */
119 function setItem(
120 key: string,
121 value: string,
122 namespace: ?string = DEFAULT_NAMESPACE
123 ): Promise<boolean | Error> {
124 const keyName = applyNamespaceToKeyName(
125 key,
126 namespace || DEFAULT_NAMESPACE
127 );
128
129 return tryAndResolve(() => {
130 Storage.callMethod("setItem", keyName, value);
131 return true;
132 });
133 }
134
135 /**
136 * Retrieves an item from storage. Returns a promise which will resolve
137 * with the value if it exists, or reject with an Error if not.
138 * @param {String} key
139 * @param {?String} namespace
140 * @returns {Promise<Any|Error>}
141 */
142 function getItem(
143 key: string,
144 namespace: ?string = DEFAULT_NAMESPACE
145 ): Promise<any> {
146 const keyName = applyNamespaceToKeyName(
147 key,
148 namespace || DEFAULT_NAMESPACE
149 );
150 return tryAndResolve(() => Storage.callMethod("getItem", keyName));
151 }
152
153 /**
154 * retrieves all items in storage, belonging to the namespace if provided
155 * @param {?String} namespace
156 * @returns {Promise<Object|Error>}
157 */
158 function getAllItems(namespace: ?string = null): Promise<string> {
159 return tryAndResolve(() => {
160 const keys = Storage.getKeys();
161
162 const filteredKeys = R.filter(
163 R.ifElse(
164 () => namespace,
165 (R.contains || R.includes)(`${namespace}${NAMESPACE_SEPARATOR}`),
166 R.T
167 ),
168 keys
169 );
170
171 const getValuesForKeys = R.map((key) =>
172 Storage.callMethod("getItem", key)
173 );
174
175 const parseNamespace = (value, namespacedKey) => {
176 const [namespace, key] = R.split(NAMESPACE_SEPARATOR, namespacedKey);
177 return { namespace, key, value };
178 };
179
180 const reduceStorageProperties = (storageProperties, entry) => {
181 const { key, value } = entry;
182 return R.assoc(key, value, storageProperties);
183 };
184
185 const groupEntriesByNamespace = R.compose(
186 R.mapObjIndexed(R.reduce(reduceStorageProperties, {})),
187 R.groupBy(R.prop("namespace")),
188 R.values,
189 R.mapObjIndexed(parseNamespace)
190 );
191
192 return Promise.all(getValuesForKeys(filteredKeys))
193 .then(R.zipObj(filteredKeys))
194 .then(groupEntriesByNamespace)
195 .then(R.assoc("zapp", R.__, {}));
196 });
197 }
198
199 return {
200 setItem,
201 getItem,
202 getAllItems,
203 };
204}