Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
d16155d
zulipVersion [nfc]: Put methods on the prototype.
chrisbobbe Dec 24, 2020
4e833a4
replaceRevive: Copy remotedev-serialize's tests, commented out.
chrisbobbe Dec 22, 2020
b9674c9
replaceRevive tests [nfc]: Rewire to import remotedev-serialize as we…
chrisbobbe Dec 22, 2020
b9d3c91
replaceRevive tests [nfc]: Stop using unhandled `Seq.of` alias.
chrisbobbe Dec 22, 2020
3e7ed55
replaceRevive tests: Declare a variable properly(-ish).
chrisbobbe Dec 22, 2020
666dc26
replaceRevive tests [nfc]: Update snapshots.
chrisbobbe Dec 22, 2020
edfc504
replaceRevive tests [nfc]: Add a line that Jest wants to add.
chrisbobbe Dec 22, 2020
64d25ae
replaceRevive tests: Uncomment.
chrisbobbe Dec 22, 2020
69834ac
replaceRevive tests [nfc]: Bring closer to our style.
chrisbobbe Dec 22, 2020
84054e7
replaceRevive tests [nfc]: Fix `new-cap` violations.
chrisbobbe Dec 22, 2020
52da22f
replaceRevive tests [nfc]: Fix `no-shadow` violations.
chrisbobbe Dec 22, 2020
a71000e
replaceRevive tests: Fix `valid-expect` violation.
chrisbobbe Dec 22, 2020
d5a9357
replaceRevive tests [nfc]: Move jest/no-conditional-expect, add comme…
chrisbobbe Dec 22, 2020
ad1ca60
replaceRevive tests: Remove all tests that used refs.
chrisbobbe Dec 22, 2020
5130b74
store [nfc]: Inline remotedev-serialize (1/x).
chrisbobbe Dec 18, 2020
0c89032
replaceRevive tests: Start testing exports of store.js.
chrisbobbe Dec 22, 2020
cc1398e
replaceRevive tests: Test `stringify` and `parse` with our own data t…
chrisbobbe Dec 24, 2020
53038b6
replaceRevive tests [nfc]: Start type-checking.
chrisbobbe Dec 24, 2020
cc43d85
replaceRevive tests [nfc]: Remove some unnecessary levels of nesting.
chrisbobbe Dec 24, 2020
2d617f6
store [nfc]: Inline remotedev-serialize (2/x).
chrisbobbe Dec 18, 2020
d4fcb1c
store [nfc]: Inline remotedev-serialize (3/x).
chrisbobbe Dec 18, 2020
85c1606
store: Remove replace/revive handlers for Immutable things we don't use.
chrisbobbe Dec 18, 2020
5c61196
store [nfc]: Inline remotedev-serialize (4/x).
chrisbobbe Dec 18, 2020
b0fee36
store [nfc]: Inline remotedev-serialize (5/x).
chrisbobbe Dec 18, 2020
b9b3633
store [nfc]: Inline remotedev-serialize (6/x).
chrisbobbe Dec 18, 2020
ed7a213
store [nfc]: Inline remotedev-serialize (7/x).
chrisbobbe Dec 18, 2020
d91001b
store [nfc]: Inline remotedev-serialize (8/x).
chrisbobbe Dec 18, 2020
a0dcf4f
store [nfc]: Inline remotedev-serialize (9/x).
chrisbobbe Dec 18, 2020
27598f9
store [nfc]: Inline remotedev-serialize (10/x).
chrisbobbe Dec 18, 2020
460f27b
store [nfc]: Make `customReplacer` and `customReviver` non-arrow func…
chrisbobbe Dec 18, 2020
1376e1e
store [nfc]: Inline remotedev-serialize (11/x).
chrisbobbe Dec 18, 2020
713c95a
store [nfc]: Inline remotedev-serialize (12/x).
chrisbobbe Dec 18, 2020
e2701d6
store [nfc]: Inline remotedev-serialize (13/x).
chrisbobbe Dec 18, 2020
0144876
store [nfc]: Inline remotedev-serialize (14/x).
chrisbobbe Dec 18, 2020
25f147f
store [nfc]: Inline remotedev-serialize (15/x).
chrisbobbe Dec 18, 2020
8d3dd1e
store [nfc]: Inline remotedev-serialize (16/x).
chrisbobbe Dec 18, 2020
5838c65
store [nfc]: Inline remotedev-serialize (17/x).
chrisbobbe Dec 18, 2020
757bd40
deps: Remove remotedev-serialize.
chrisbobbe Dec 18, 2020
8f6a63a
store: Opt out of several `jsan` features we don't need.
chrisbobbe Dec 18, 2020
c3f1820
store: Use JSON instead of `jsan` for replace/revive logic.
chrisbobbe Dec 18, 2020
6a52572
deps: Remove `jsan`.
chrisbobbe Dec 18, 2020
231d24b
store [nfc]: Use `origValue.toJSON()` instead of `.toObject()`.
chrisbobbe Dec 18, 2020
bf8837c
store: Save a bit of work when serializing `Immutable.Map`s.
chrisbobbe Dec 18, 2020
f985ef0
store: Add some invariants to `stringify`.
chrisbobbe Dec 24, 2020
44aa9e1
replaceRevive [nfc]: Move replace-revive logic into its own file.
chrisbobbe Dec 24, 2020
410f219
replaceRevive [nfc]: Identify the Flow bug causing the fixmes.
gnprice Dec 29, 2020
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
2 changes: 1 addition & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
src/emoji/__tests__/data-test.js

# We're not allowing Prettier to touch some of our vendored code.
src/third/redux-persist
src/third/redux-persist
40 changes: 0 additions & 40 deletions flow-typed/remotedev-serialize_vx.x.x.js

This file was deleted.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@
"redux-batched-actions": "^0.3.0",
"redux-logger": "^3.0.1",
"redux-thunk": "^2.1.0",
"remotedev-serialize": "zulip/remotedev-serialize#5f9f759a4",
"reselect": "^3.0.1",
"rn-fetch-blob": "^0.11.0",
"string.fromcodepoint": "^0.2.1",
Expand Down
17 changes: 17 additions & 0 deletions src/boot/__tests__/__snapshots__/replaceRevive-test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Stringify fallbackAvatarURL 1`] = `"{\\"data\\":\\"https://zulip.example.org/avatar/1\\",\\"__serializedType__\\":\\"FallbackAvatarURL\\"}"`;

exports[`Stringify gravatarURL 1`] = `"{\\"data\\":\\"https://secure.gravatar.com/avatar/3b01d0f626dc6944ed45dbe6c86d3e30?d=identicon\\",\\"__serializedType__\\":\\"GravatarURL\\"}"`;

exports[`Stringify map 1`] = `"{\\"data\\":{\\"a\\":1,\\"b\\":2,\\"c\\":3,\\"d\\":4},\\"__serializedType__\\":\\"ImmutableMap\\"}"`;

exports[`Stringify mapWithTypeKey 1`] = `"{\\"data\\":{\\"__serializedType__\\":\\"Object\\",\\"data\\":{\\"a\\":1},\\"__serializedType__value\\":{\\"__serializedType__\\":\\"Object\\",\\"data\\":{\\"b\\":[2]},\\"__serializedType__value\\":{\\"c\\":[3]}}},\\"__serializedType__\\":\\"ImmutableMap\\"}"`;

exports[`Stringify plainObjectWithTypeKey 1`] = `"{\\"__serializedType__\\":\\"Object\\",\\"data\\":{\\"a\\":1},\\"__serializedType__value\\":{\\"__serializedType__\\":\\"Object\\",\\"data\\":{\\"b\\":[2]},\\"__serializedType__value\\":{\\"c\\":[3]}}}"`;

exports[`Stringify uploadedAvatarURL 1`] = `"{\\"data\\":\\"https://zulip.example.org/user_avatars/2/e35cdbc4771c5e4b94e705bf6ff7cca7fa1efcae.png?x=x&version=2\\",\\"__serializedType__\\":\\"UploadedAvatarURL\\"}"`;

exports[`Stringify url 1`] = `"{\\"data\\":\\"https://chat.zulip.org/\\",\\"__serializedType__\\":\\"URL\\"}"`;

exports[`Stringify zulipVersion 1`] = `"{\\"data\\":\\"3.0.0\\",\\"__serializedType__\\":\\"ZulipVersion\\"}"`;
76 changes: 76 additions & 0 deletions src/boot/__tests__/replaceRevive-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/* @flow strict-local */
/* eslint-disable id-match */
/* eslint-disable no-underscore-dangle */

import Immutable from 'immutable';

import { FallbackAvatarURL, GravatarURL, UploadedAvatarURL } from '../../utils/avatar';
import { ZulipVersion } from '../../utils/zulipVersion';
import { stringify, parse, SERIALIZED_TYPE_FIELD_NAME } from '../replaceRevive';
import * as eg from '../../__tests__/lib/exampleData';

const data = {
map: Immutable.Map({ a: 1, b: 2, c: 3, d: 4 }),
mapWithTypeKey: Immutable.Map({
a: 1,
[SERIALIZED_TYPE_FIELD_NAME]: {
b: [2],
[SERIALIZED_TYPE_FIELD_NAME]: { c: [3] },
},
}),
zulipVersion: new ZulipVersion('3.0.0'),
url: new URL('https://chat.zulip.org'),
gravatarURL: GravatarURL.validateAndConstructInstance({ email: eg.selfUser.email }),
uploadedAvatarURL: UploadedAvatarURL.validateAndConstructInstance({
realm: eg.realm,
absoluteOrRelativeUrl:
'/user_avatars/2/e35cdbc4771c5e4b94e705bf6ff7cca7fa1efcae.png?x=x&version=2',
}),
fallbackAvatarURL: FallbackAvatarURL.validateAndConstructInstance({
realm: eg.realm,
userId: 1,
}),
plainObjectWithTypeKey: {
a: 1,
[SERIALIZED_TYPE_FIELD_NAME]: {
b: [2],
[SERIALIZED_TYPE_FIELD_NAME]: { c: [3] },
},
},
};

const stringified = {};
describe('Stringify', () => {
Object.keys(data).forEach(key => {
it(key, () => {
stringified[key] = stringify(data[key]);
expect(stringified[key]).toMatchSnapshot();
});
});

test('catches an unexpectedly unhandled value with a `toJSON` method', () => {
expect(() => stringify(new Date())).toThrow();
});

test("catches a value that's definitely not serializable as-is", () => {
expect(() => stringify(() => 'foo')).toThrow();
});

test('catches an unexpectedly unhandled value of an interesting type', () => {
class Dog {
noise = 'woof';
makeNoise() {
console.log(this.noise); // eslint-disable-line no-console
}
}
expect(() => stringify(new Dog())).toThrow();
});
});

describe('Parse', () => {
Object.keys(data).forEach(key => {
it(key, () => {
expect(parse(stringified[key])).toEqual(data[key]);
});
});
});
139 changes: 139 additions & 0 deletions src/boot/replaceRevive.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/* @flow strict-local */
import invariant from 'invariant';
import Immutable from 'immutable';

import { ZulipVersion } from '../utils/zulipVersion';
import { GravatarURL, UploadedAvatarURL, FallbackAvatarURL } from '../utils/avatar';

/**
* PRIVATE: Exported only for tests.
*
* A special identifier for the type of thing to be replaced/revived.
*
* Use this in the replacer and reviver, below, to make it easier to
* be consistent between them and avoid costly typos.
*/
export const SERIALIZED_TYPE_FIELD_NAME: '__serializedType__' = '__serializedType__';

/**
* Like SERIALIZED_TYPE_FIELD_NAME, but with a distinguishing mark.
*
* Used in our strategy to ensure successful round-tripping when data
* has a key identical to SERIALIZED_TYPE_FIELD_NAME.
*/
const SERIALIZED_TYPE_FIELD_NAME_ESCAPED: '__serializedType__value' = '__serializedType__value';

// Don't make this an arrow function -- we need `this` to be a special
// value; see
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#The_replacer_parameter.
const replacer = function replacer(key, value) {
// The value at the current path before JSON.stringify called its
// `toJSON` method, if present.
//
// When identifying what kind of thing we're working with, be sure
// to examine `origValue` instead of `value`, if calling `toJSON` on
// that kind of thing would remove its identifying features -- which
// is to say, if that kind of thing has a `toJSON` method.
//
// For things that have a `toJSON` method, it may be convenient to
// set `data` to `value`, if we trust that `toJSON` gives the output
// we want to store there. And it would mean we don't discard the
// work `JSON.stringify` did by calling `toJSON`.
const origValue = this[key];
if (value instanceof ZulipVersion) {
return { data: value.raw(), [SERIALIZED_TYPE_FIELD_NAME]: 'ZulipVersion' };
} else if (origValue instanceof URL) {
return { data: origValue.toString(), [SERIALIZED_TYPE_FIELD_NAME]: 'URL' };
} else if (value instanceof GravatarURL) {
return { data: GravatarURL.serialize(value), [SERIALIZED_TYPE_FIELD_NAME]: 'GravatarURL' };
} else if (value instanceof UploadedAvatarURL) {
return {
data: UploadedAvatarURL.serialize(value),
[SERIALIZED_TYPE_FIELD_NAME]: 'UploadedAvatarURL',
};
} else if (value instanceof FallbackAvatarURL) {
return {
data: FallbackAvatarURL.serialize(value),
[SERIALIZED_TYPE_FIELD_NAME]: 'FallbackAvatarURL',
};
} else if (Immutable.Map.isMap(origValue)) {
return { data: value, [SERIALIZED_TYPE_FIELD_NAME]: 'ImmutableMap' };
}

if (typeof origValue === 'object' && origValue !== null) {
// Don't forget to handle a value's `toJSON` method, if present, as
// described above.
invariant(typeof origValue.toJSON !== 'function', 'unexpected toJSON');

// If storing an interesting data type, don't forget to handle it
// here, and in `reviver`.
const origValuePrototype = Object.getPrototypeOf(origValue);
invariant(
// Flow bug: https://github.com/facebook/flow/issues/6110
origValuePrototype === (Object.prototype: $FlowFixMe)
|| origValuePrototype === (Array.prototype: $FlowFixMe),
'unexpected class',
);
}

if (typeof value === 'object' && value !== null && SERIALIZED_TYPE_FIELD_NAME in value) {
const copy = { ...value };
delete copy[SERIALIZED_TYPE_FIELD_NAME];
return {
[SERIALIZED_TYPE_FIELD_NAME]: 'Object',
data: copy,
[SERIALIZED_TYPE_FIELD_NAME_ESCAPED]: value[SERIALIZED_TYPE_FIELD_NAME],
};
}

return value;
};

const reviver = function reviver(key, value) {
if (value !== null && typeof value === 'object' && SERIALIZED_TYPE_FIELD_NAME in value) {
const data = value.data;
switch (value[SERIALIZED_TYPE_FIELD_NAME]) {
case 'ZulipVersion':
return new ZulipVersion(data);
case 'URL':
return new URL(data);
case 'GravatarURL':
return GravatarURL.deserialize(data);
case 'UploadedAvatarURL':
return UploadedAvatarURL.deserialize(data);
case 'FallbackAvatarURL':
return FallbackAvatarURL.deserialize(data);
case 'ImmutableMap':
return Immutable.Map(data);
case 'Object':
return {
...data,
[SERIALIZED_TYPE_FIELD_NAME]: value[SERIALIZED_TYPE_FIELD_NAME_ESCAPED],
};
default:
return data;
}
}
return value;
};

export const stringify = function stringify(data: mixed): string {
const result = JSON.stringify(data, replacer);
if (result === undefined) {
// Flow says that the output for JSON.stringify could be
// undefined. From MDN:
//
// `JSON.stringify()` can return `undefined` when passing in
// "pure" values like `JSON.stringify(function(){})` or
// `JSON.stringify(undefined)`.
//
// We don't expect any of those inputs, but we'd want to know if
// we get one, since it means something has gone quite wrong.
throw new Error('undefined result for stringify');
}
return result;
};

export const parse = function parse(data: string): mixed {
return JSON.parse(data, reviver);
};
55 changes: 1 addition & 54 deletions src/boot/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@ import thunkMiddleware from 'redux-thunk';
import { createLogger } from 'redux-logger';
import createActionBuffer from 'redux-action-buffer';
import Immutable from 'immutable';
import * as Serialize from 'remotedev-serialize';
import { persistStore, autoRehydrate } from '../third/redux-persist';
import type { Config } from '../third/redux-persist';

import { ZulipVersion } from '../utils/zulipVersion';
import { GravatarURL, UploadedAvatarURL, FallbackAvatarURL } from '../utils/avatar';
import { stringify, parse } from './replaceRevive';
import type { Action, GlobalState } from '../types';
import config from '../config';
import { REHYDRATE } from '../actionConstants';
Expand Down Expand Up @@ -327,58 +326,6 @@ provideLoggingContext(() => ({
serverVersion: tryGetActiveAccount(store.getState())?.zulipVersion ?? null,
}));

/**
* A special identifier used by `remotedev-serialize`.
*
* Use this in the custom replacer and reviver, below, to make it
* easier to be consistent between them and avoid costly typos.
*/
const SERIALIZED_TYPE_FIELD_NAME: '__serializedType__' = '__serializedType__';

const customReplacer = (key, value, defaultReplacer) => {
if (value instanceof ZulipVersion) {
return { data: value.raw(), [SERIALIZED_TYPE_FIELD_NAME]: 'ZulipVersion' };
} else if (value instanceof URL) {
return { data: value.toString(), [SERIALIZED_TYPE_FIELD_NAME]: 'URL' };
} else if (value instanceof GravatarURL) {
return { data: GravatarURL.serialize(value), [SERIALIZED_TYPE_FIELD_NAME]: 'GravatarURL' };
} else if (value instanceof UploadedAvatarURL) {
return {
data: UploadedAvatarURL.serialize(value),
[SERIALIZED_TYPE_FIELD_NAME]: 'UploadedAvatarURL',
};
} else if (value instanceof FallbackAvatarURL) {
return {
data: FallbackAvatarURL.serialize(value),
[SERIALIZED_TYPE_FIELD_NAME]: 'FallbackAvatarURL',
};
}
return defaultReplacer(key, value);
};

const customReviver = (key, value, defaultReviver) => {
if (value !== null && typeof value === 'object' && SERIALIZED_TYPE_FIELD_NAME in value) {
const data = value.data;
switch (value[SERIALIZED_TYPE_FIELD_NAME]) {
case 'ZulipVersion':
return new ZulipVersion(data);
case 'URL':
return new URL(data);
case 'GravatarURL':
return GravatarURL.deserialize(data);
case 'UploadedAvatarURL':
return UploadedAvatarURL.deserialize(data);
case 'FallbackAvatarURL':
return FallbackAvatarURL.deserialize(data);
default:
// Fall back to defaultReviver, below
}
}
return defaultReviver(key, value);
};

const { stringify, parse } = Serialize.immutable(Immutable, null, customReplacer, customReviver);

/**
* The config options to pass to redux-persist.
*
Expand Down
Loading