Skip to content

Commit

Permalink
change usage of JSON.parse and JSON.stringify to use the safe version…
Browse files Browse the repository at this point in the history
…s that retain Map and Set
  • Loading branch information
jmeistrich committed Oct 27, 2023
1 parent 271b8da commit 0ac9fd5
Show file tree
Hide file tree
Showing 6 changed files with 34 additions and 25 deletions.
5 changes: 4 additions & 1 deletion index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,10 @@ export { ObservablePrimitiveClass } from './src/ObservablePrimitive';
// Internal:
import { get, getProxy, observableFns, peek, set } from './src/ObservableObject';
import { ensureNodeValue, findIDKey, getNode, globalState, optimized, setNodeValue, symbolDelete } from './src/globals';
import { getPathType, setAtPath, initializePathType } from './src/helpers';
import { getPathType, setAtPath, initializePathType, safeParse, safeStringify, clone } from './src/helpers';

export const internal = {
clone,
ensureNodeValue,
findIDKey,
get,
Expand All @@ -70,6 +71,8 @@ export const internal = {
observableFns,
optimized,
peek,
safeParse,
safeStringify,
set,
setAtPath,
setNodeValue,
Expand Down
18 changes: 12 additions & 6 deletions src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,12 +227,12 @@ export function initializePathType(pathType: TypeAtPath): any {
function replacer(_: string, value: any) {
if (value instanceof Map) {
return {
dataType: 'Map',
__LSType: 'Map',
value: Array.from(value.entries()), // or with spread: value: [...value]
};
} else if (value instanceof Set) {
return {
dataType: 'Set',
__LSType: 'Set',
value: Array.from(value), // or with spread: value: [...value]
};
} else {
Expand All @@ -241,16 +241,22 @@ function replacer(_: string, value: any) {
}

function reviver(_: string, value: any) {
if (typeof value === 'object' && value !== null) {
if (value.dataType === 'Map') {
if (typeof value === 'object' && value) {
if (value.__LSType === 'Map') {
return new Map(value.value);
} else if (value.dataType === 'Set') {
} else if (value.__LSType === 'Set') {
return new Set(value.value);
}
}
return value;
}

export function safeStringify(value: any) {
return JSON.stringify(value, replacer);
}
export function safeParse(value: any) {
return JSON.parse(value, reviver);
}
export function clone(value: any) {
return JSON.parse(JSON.stringify(value, replacer), reviver);
return safeParse(safeStringify(value));
}
10 changes: 6 additions & 4 deletions src/persist-plugins/async-storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import type {
ObservablePersistenceConfigLocalGlobalOptions,
PersistMetadata,
} from '@legendapp/state';
import { isArray, setAtPath } from '@legendapp/state';
import { isArray, setAtPath, internal } from '@legendapp/state';
import type { AsyncStorageStatic } from '@react-native-async-storage/async-storage';

const MetadataSuffix = '__m';

let AsyncStorage: AsyncStorageStatic;

const { safeParse, safeStringify } = internal;

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

Expand All @@ -33,7 +35,7 @@ export class ObservablePersistAsyncStorage implements ObservablePersistLocal {
const values = await AsyncStorage.multiGet(tables);

values.forEach(([table, value]) => {
this.data[table] = value ? JSON.parse(value) : undefined;
this.data[table] = value ? safeParse(value) : undefined;
});
}
} catch (e) {
Expand All @@ -48,7 +50,7 @@ export class ObservablePersistAsyncStorage implements ObservablePersistLocal {
try {
return (async () => {
const value = await AsyncStorage.getItem(table);
this.data[table] = value ? JSON.parse(value) : undefined;
this.data[table] = value ? safeParse(value) : undefined;
})();
} catch {
console.error('[legend-state] ObservablePersistLocalAsyncStorage failed to parse', table);
Expand Down Expand Up @@ -92,7 +94,7 @@ export class ObservablePersistAsyncStorage implements ObservablePersistLocal {
const v = this.data[table];

if (v !== undefined && v !== null) {
return AsyncStorage.setItem(table, JSON.stringify(v));
return AsyncStorage.setItem(table, safeStringify(v));
} else {
return AsyncStorage.removeItem(table);
}
Expand Down
10 changes: 2 additions & 8 deletions src/persist-plugins/firebase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,9 @@ import {
startAt,
update,
} from 'firebase/database';
const { symbolDelete, getPathType } = internal;
const { symbolDelete, getPathType, clone } = internal;
const { observablePersistConfiguration } = internalPersist;

function clone(obj: any) {
return obj === undefined || obj === null ? obj : JSON.parse(JSON.stringify(obj));
}
function getDateModifiedKey(dateModifiedKey: string | undefined) {
return dateModifiedKey || observablePersistConfiguration.remoteOptions?.dateModifiedKey || '@';
}
Expand Down Expand Up @@ -615,10 +612,7 @@ class ObservablePersistFirebaseBase implements ObservablePersistRemoteClass {
saveState.numSavesPending.set((v) => v + 1);

if (pendingSaves.size > 0) {
const batches = JSON.parse(JSON.stringify(this._constructBatchesForSave(pendingSaves))) as Record<
string,
any
>[];
const batches = clone(this._constructBatchesForSave(pendingSaves)) as Record<string, any>[];

saveState.savingSaves = pendingSaves;

Expand Down
8 changes: 5 additions & 3 deletions src/persist-plugins/local-storage.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import type { Change, ObservablePersistLocal, PersistMetadata } from '@legendapp/state';
import { setAtPath } from '@legendapp/state';
import { setAtPath, internal } from '@legendapp/state';

const MetadataSuffix = '__m';

const { safeParse, safeStringify } = internal;

class ObservablePersistLocalStorageBase implements ObservablePersistLocal {
private data: Record<string, any> = {};
private storage: Storage | undefined;
Expand All @@ -14,7 +16,7 @@ class ObservablePersistLocalStorageBase implements ObservablePersistLocal {
if (this.data[table] === undefined) {
try {
const value = this.storage.getItem(table);
this.data[table] = value ? JSON.parse(value) : undefined;
this.data[table] = value ? safeParse(value) : undefined;
} catch {
console.error('[legend-state] ObservablePersistLocalStorage failed to parse', table);
}
Expand Down Expand Up @@ -56,7 +58,7 @@ class ObservablePersistLocalStorageBase implements ObservablePersistLocal {
const v = this.data[table];

if (v !== undefined && v !== null) {
this.storage.setItem(table, JSON.stringify(v));
this.storage.setItem(table, safeStringify(v));
} else {
this.storage.removeItem(table);
}
Expand Down
8 changes: 5 additions & 3 deletions src/persist-plugins/mmkv.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type { Change, ObservablePersistLocal, PersistMetadata, PersistOptionsLocal } from '@legendapp/state';
import { setAtPath } from '@legendapp/state';
import { internal, setAtPath } from '@legendapp/state';
import { MMKV } from 'react-native-mmkv';

const symbolDefault = Symbol();
const MetadataSuffix = '__m';

const { safeParse, safeStringify } = internal;

export class ObservablePersistMMKV implements ObservablePersistLocal {
private data: Record<string, any> = {};
private storages = new Map<symbol | string, MMKV>([
Expand All @@ -21,7 +23,7 @@ export class ObservablePersistMMKV implements ObservablePersistLocal {
if (this.data[table] === undefined) {
try {
const value = storage.getString(table);
this.data[table] = value ? JSON.parse(value) : undefined;
this.data[table] = value ? safeParse(value) : undefined;
} catch {
console.error('[legend-state] MMKV failed to parse', table);
}
Expand Down Expand Up @@ -77,7 +79,7 @@ export class ObservablePersistMMKV implements ObservablePersistLocal {
const v = this.data[table];
if (v !== undefined) {
try {
storage.set(table, JSON.stringify(v));
storage.set(table, safeStringify(v));
} catch (err) {
console.error(err);
}
Expand Down

0 comments on commit 0ac9fd5

Please sign in to comment.