Skip to content

Commit

Permalink
Merge pull request #66 from square/sbeale/64
Browse files Browse the repository at this point in the history
feat: allow for custom storage types for persisted stores
  • Loading branch information
Akolyte01 authored Jun 20, 2023
2 parents c2c916e + 2fcd8f9 commit 2be1698
Show file tree
Hide file tree
Showing 9 changed files with 318 additions and 94 deletions.
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,13 +225,13 @@ const remoteSessionToken = asyncReadable(
const session = await generateSession();
return session.token;
},
{ reloadable: true, storageType: 'SESSION_STORAGE' },
{ reloadable: true },
);

const sessionToken = persisted(
remoteSessionToken,
'SESSION_TOKEN',
{ reloadable: true }
{ reloadable: true, storageType: 'SESSION_STORAGE' }
);
```
Expand All @@ -241,6 +241,24 @@ If an external source updates the storage item of the persisted store the two va
We are also able to wipe stored data by calling `clear()` on the store. The storage item will be removed and the value of the store set to `null`.
#### persisted configuration / custom storage
Persisted stores have three built in storage types: LOCAL_STORAGE, SESSION_STORAGE, and COOKIE. These should be sufficient for most use cases, but have the disadvantage of only being able to store JSON serializable data. If more advanced behavior is required we can define a custom storage type to handle this, such as integrating [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API). All that is required is for us to provide the relevant setter/getter/deleter functions for interfacing with our storage.
*One time setup is all that is needed for custom storage...*
```javascript
configureCustomStorageType('INDEXED_DB', {
getStorageItem: (key) => /* get from IndexedDB */,
setStorageItem: (key, value) => /* persist to IndexedDB */,
removeStorageItem: (key) => /* delete from IndexedDB */,
});

const customStore = persisted('defaultValue', 'indexedDbKey', {
storageType: 'INDEXED_DB',
});
```
#### persisted configuration / consent
Persisting data to storage or cookies is subject to privacy laws regarding consent in some jurisdictions. Instead of building two different data flows that depend on whether tracking consent has been given or not, you can instead configure your persisted stores to work in both cases. To do so you will need to call the `configurePersistedConsent` function and pass in a consent checker that will accept a `consent level` and return a boolean indicating whether your user has consented to that level of tracking. You can then provide a consent level when building your persisted stores that will be passed to to the checker before storing data.
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
],
"roots": [
"./test"
]
],
"resolver": "ts-jest-resolver"
},
"devDependencies": {
"@types/jest": "^27.0.2",
Expand All @@ -45,7 +46,8 @@
},
"dependencies": {
"cookie-storage": "^6.1.0",
"svelte": "^3.0.0"
"svelte": "^3.0.0",
"ts-jest-resolver": "^2.0.1"
},
"license": "ISC",
"author": "Square",
Expand Down
7 changes: 6 additions & 1 deletion src/async-stores/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@ import type {
WritableLoadable,
VisitedMap,
} from './types.js';
import { anyReloadable, getStoresArray, reloadAll, loadAll } from '../utils/index.js';
import {
anyReloadable,
getStoresArray,
reloadAll,
loadAll,
} from '../utils/index.js';
import { flagStoreCreated, getStoreTestingMode, logError } from '../config.js';

// STORES
Expand Down
18 changes: 15 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,23 @@ export type {
Stores,
StoresValues,
} from './async-stores/types.js';
export type { StorageType, StorageOptions, Persisted } from './persisted/types.js';
export type {
StorageType,
StorageOptions,
Persisted,
} from './persisted/types.js';

export { asyncClient } from './async-client/index.js';
export { asyncWritable, asyncDerived, asyncReadable } from './async-stores/index.js';
export { configurePersistedConsent, persisted } from './persisted/index.js';
export {
asyncWritable,
asyncDerived,
asyncReadable,
} from './async-stores/index.js';
export {
configureCustomStorageType,
configurePersistedConsent,
persisted,
} from './persisted/index.js';
export { derived, readable, writable } from './standard-stores/index.js';
export {
isLoadable,
Expand Down
78 changes: 45 additions & 33 deletions src/persisted/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,38 +15,52 @@ import {
removeLocalStorageItem,
} from './storage-utils.js';

type GetStorageItem = (key: string, consentLevel?: unknown) => string | null;
type SetStorageItem = (
key: string,
value: string,
consentLevel?: unknown
) => void;
type GetStorageItem = (key: string) => unknown;
type SetStorageItem = (key: string, value: unknown) => void;
type RemoveStorageItem = (key: string) => void;

const getStorageFunctions = (
type: StorageType
): {
type StorageFunctions = {
getStorageItem: GetStorageItem;
setStorageItem: SetStorageItem;
removeStorageItem: RemoveStorageItem;
} => {
return {
LOCAL_STORAGE: {
getStorageItem: getLocalStorageItem,
setStorageItem: setLocalStorageItem,
removeStorageItem: removeLocalStorageItem,
},
SESSION_STORAGE: {
getStorageItem: getSessionStorageItem,
setStorageItem: setSessionStorageItem,
removeStorageItem: removeSessionStorageItem,
},
COOKIE: {
getStorageItem: getCookie,
setStorageItem: setCookie,
removeStorageItem: removeCookie,
},
};

const builtinStorageFunctions: Record<string, StorageFunctions> = {
LOCAL_STORAGE: {
getStorageItem: getLocalStorageItem,
setStorageItem: setLocalStorageItem,
removeStorageItem: removeLocalStorageItem,
},
SESSION_STORAGE: {
getStorageItem: getSessionStorageItem,
setStorageItem: setSessionStorageItem,
removeStorageItem: removeSessionStorageItem,
},
COOKIE: {
getStorageItem: getCookie,
setStorageItem: setCookie,
removeStorageItem: removeCookie,
},
};

const customStorageFunctions: Record<string, StorageFunctions> = {};

export const configureCustomStorageType = (
type: string,
storageFunctions: StorageFunctions
): void => {
customStorageFunctions[type] = storageFunctions;
};

const getStorageFunctions = (type: StorageType | string): StorageFunctions => {
const storageFunctions = {
...builtinStorageFunctions,
...customStorageFunctions,
}[type];
if (!storageFunctions) {
throw new Error(`'${type}' is not a valid StorageType!`);
}
return storageFunctions;
};

type ConsentChecker = (consentLevel: unknown) => boolean;
Expand Down Expand Up @@ -92,20 +106,18 @@ export const persisted = <T>(
// check consent if checker provided
if (!checkConsent || checkConsent(consentLevel)) {
const storageKey = await getKey();
setStorageItem(storageKey, JSON.stringify(value), consentLevel);
setStorageItem(storageKey, value);
}
set(value);
};

const synchronize = async (set: Subscriber<T>): Promise<T> => {
const storageKey = await getKey();
const storageItem = getStorageItem(storageKey);

if (storageItem) {
const stored = JSON.parse(storageItem);
set(stored);
const stored = getStorageItem(storageKey);

return stored;
if (stored) {
set(stored as T);
return stored as T;
} else if (initial !== undefined) {
if (isLoadable(initial)) {
const $initial = await initial.load();
Expand Down
27 changes: 15 additions & 12 deletions src/persisted/storage-utils.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,40 @@
import { CookieStorage } from "cookie-storage";
import { CookieStorage } from 'cookie-storage';

const cookieStorage = new CookieStorage();

export const getLocalStorageItem = (key: string): string | null => {
return window.localStorage.getItem(key);
export const getLocalStorageItem = (key: string): unknown => {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : null;
};

export const setLocalStorageItem = (key: string, value: string): void => {
window.localStorage.setItem(key, value);
export const setLocalStorageItem = (key: string, value: unknown): void => {
window.localStorage.setItem(key, JSON.stringify(value));
};

export const removeLocalStorageItem = (key: string): void => {
window.localStorage.removeItem(key);
};

export const getSessionStorageItem = (key: string): string | null => {
return window.sessionStorage.getItem(key);
const item = window.sessionStorage.getItem(key);
return item ? JSON.parse(item) : null;
};

export const setSessionStorageItem = (key: string, value: string): void => {
window.sessionStorage.setItem(key, value);
export const setSessionStorageItem = (key: string, value: unknown): void => {
window.sessionStorage.setItem(key, JSON.stringify(value));
};

export const removeSessionStorageItem = (key: string): void => {
window.sessionStorage.removeItem(key);
};

export const getCookie = (key: string): string | null => {
return cookieStorage.getItem(key) || null;
export const getCookie = (key: string): unknown => {
const item = cookieStorage.getItem(key);
return item ? JSON.parse(item) : null;
};

export const setCookie = (key: string, value: string): void => {
cookieStorage.setItem(key, value);
export const setCookie = (key: string, value: unknown): void => {
cookieStorage.setItem(key, JSON.stringify(value));
};

export const removeCookie = (key: string): void => {
Expand Down
2 changes: 1 addition & 1 deletion src/persisted/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export type StorageType = 'LOCAL_STORAGE' | 'SESSION_STORAGE' | 'COOKIE';

export type StorageOptions = {
reloadable?: true;
storageType?: StorageType;
storageType?: StorageType | string;
consentLevel?: unknown;
};

Expand Down
Loading

0 comments on commit 2be1698

Please sign in to comment.