Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Session storage redux #164

Merged
merged 7 commits into from
Jul 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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');
});
});
Loading