Skip to content

Commit

Permalink
Merge pull request #164 from minorgod/session-storage-redux
Browse files Browse the repository at this point in the history
Session storage redux
  • Loading branch information
jmeistrich authored Jul 28, 2023
2 parents d28b377 + e19535f commit 576e3be
Show file tree
Hide file tree
Showing 2 changed files with 233 additions and 8 deletions.
29 changes: 21 additions & 8 deletions src/persist-plugins/local-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ import { setAtPath } from '@legendapp/state';

const MetadataSuffix = '__m';

export class ObservablePersistLocalStorage implements ObservablePersistLocal {
class ObservablePersistLocalStorageBase implements ObservablePersistLocal {
private data: Record<string, any> = {};

private storage: Storage;
constructor(storage: Storage) {
this.storage = storage;
}
public getTable(table: string) {
if (typeof localStorage === 'undefined') return undefined;
if (typeof this.storage === 'undefined') return undefined;
if (this.data[table] === undefined) {
try {
const value = localStorage.getItem(table);
const value = this.storage.getItem(table);
this.data[table] = value ? JSON.parse(value) : undefined;
} catch {
console.error('[legend-state] ObservablePersistLocalStorage failed to parse', table);
Expand Down Expand Up @@ -40,7 +43,7 @@ export class ObservablePersistLocalStorage implements ObservablePersistLocal {
}
public deleteTable(table: string) {
delete this.data[table];
localStorage.removeItem(table);
this.storage.removeItem(table);
}
public deleteMetadata(table: string) {
this.deleteTable(table + MetadataSuffix);
Expand All @@ -51,14 +54,24 @@ export class ObservablePersistLocalStorage implements ObservablePersistLocal {
this.save(table);
}
private save(table: string) {
if (typeof localStorage === 'undefined') return;
if (typeof this.storage === 'undefined') return;

const v = this.data[table];

if (v !== undefined && v !== null) {
localStorage.setItem(table, JSON.stringify(v));
this.storage.setItem(table, JSON.stringify(v));
} else {
localStorage.removeItem(table);
this.storage.removeItem(table);
}
}
}
export class ObservablePersistLocalStorage extends ObservablePersistLocalStorageBase {
constructor() {
super(localStorage);
}
}
export class ObservablePersistSessionStorage extends ObservablePersistLocalStorageBase {
constructor() {
super(sessionStorage);
}
}
212 changes: 212 additions & 0 deletions tests/persist-sessionstorage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import { isArray, isObject, isString } from '../src/is';
import { observable } from '../src/observable';
import { ObservablePersistLocal } from '../src/observableInterfaces';
import { ObservablePersistSessionStorage } from '../src/persist-plugins/local-storage';
import { configureObservablePersistence } from '../src/persist/configureObservablePersistence';
import { mapPersistences, persistObservable } from '../src/persist/persistObservable';

class LocalStorageMock {
store: Record<any, any>;
constructor() {
this.store = {};
}
clear() {
this.store = {};
}
getItem(key: string) {
return this.store[key] || null;
}
setItem(key: string, value: any) {
this.store[key] = String(value);
}
removeItem(key: string) {
delete this.store[key];
}
}

function promiseTimeout(time?: number) {
return new Promise((resolve) => setTimeout(resolve, time || 0));
}

function reset() {
global.sessionStorage.clear();
const persist = mapPersistences.get(ObservablePersistSessionStorage)?.persist as ObservablePersistLocal;
if (persist) {
persist.deleteTable('jestlocal', undefined as any);
}
}

export async function recursiveReplaceStrings<T extends string | object | number | boolean>(
value: T,
replacer: (val: string) => string,
): Promise<T> {
if (isArray(value)) {
await Promise.all(
value.map((v, i) =>
recursiveReplaceStrings(v, replacer).then((val) => {
(value as any[])[i] = val;
}),
),
);
}
if (isObject(value)) {
await Promise.all(
Object.keys(value).map((k) =>
recursiveReplaceStrings((value as Record<string, any>)[k], replacer).then((val) => {
(value as Record<string, any>)[k] = val;
}),
),
);
}
if (isString(value)) {
value = await new Promise((resolve) => resolve(replacer(value as string) as T));
}

return value;
}

// @ts-expect-error This is ok to do in jest
global.sessionStorage = new LocalStorageMock();

configureObservablePersistence({
persistLocal: ObservablePersistSessionStorage,
saveTimeout: 500,
});

beforeEach(() => {
reset();
});

describe('Persist local', () => {
test('Saves to local', async () => {
reset();
const obs = observable({ test: '' });

persistObservable(obs, {
local: 'jestlocal',
});

obs.set({ test: 'hello' });

await promiseTimeout(0);

const localValue = sessionStorage.getItem('jestlocal');

// Should have saved to local storage
expect(localValue).toBe(`{"test":"hello"}`);

// obs2 should load with the same value it was just saved as
const obs2 = observable({});
persistObservable(obs2, {
local: 'jestlocal',
});

expect(obs2.get()).toEqual({ test: 'hello' });
});
test('Saves empty root object to local overwriting complex', async () => {
reset();
const obs = observable({ test: { text: 'hi' } } as { test: Record<string, any> });

persistObservable(obs, {
local: 'jestlocal',
});

obs.test.set({});

await promiseTimeout(0);

const localValue = global.sessionStorage.getItem('jestlocal');

// Should have saved to local storage
expect(localValue).toBe('{"test":{}}');

// obs2 should load with the same value it was just saved as
const obs2 = observable({});
persistObservable(obs2, {
local: 'jestlocal',
});

expect(obs2.get()).toEqual({ test: {} });
});
test('Saves empty root object to local', async () => {
reset();
const obs = observable({ test: 'hello' } as Record<string, any>);

persistObservable(obs, {
local: 'jestlocal',
});

obs.set({});

await promiseTimeout(0);

const localValue = global.sessionStorage.getItem('jestlocal');

// Should have saved to local storage
expect(localValue).toBe('{}');

// obs2 should load with the same value it was just saved as
const obs2 = observable({});
persistObservable(obs2, {
local: 'jestlocal',
});

expect(obs2).toEqual({});
});
// TODO: Put this back when adding remote persistence
// test('Loads from local with modified', () => {
// global.sessionStorage.setItem(
// 'jestlocal',
// JSON.stringify({
// test: { '@': 1000, test2: 'hi2', test3: 'hi3' },
// test4: { test5: { '@': 1001, test6: 'hi6' } },
// test7: { test8: 'hi8' },
// })
// );

// const obs = observable({
// test: { test2: '', test3: '' },
// test4: { test5: { test6: '' } },
// test7: { test8: '' },
// });

// persistObservable(obs, {
// local: 'jestlocal',
// // persistRemote: //
// remote: {},
// });

// expect(obs.get()).toEqual({
// test: { [symbolDateModified]: 1000, test2: 'hi2', test3: 'hi3' },
// test4: { test5: { [symbolDateModified]: 1001, test6: 'hi6' } },
// test7: { test8: 'hi8' },
// });
// });
});

describe('Persist primitives', () => {
test('Primitive saves to local', async () => {
const obs = observable('');

persistObservable(obs, {
local: 'jestlocal',
});

obs.set('hello');

await promiseTimeout(0);

const localValue = global.sessionStorage.getItem('jestlocal');

// Should have saved to local storage
expect(localValue).toBe('"hello"');

// obs2 should load with the same value it was just saved as
const obs2 = observable('');
persistObservable(obs2, {
local: 'jestlocal',
});

expect(obs2.get()).toEqual('hello');
});
});

0 comments on commit 576e3be

Please sign in to comment.