Skip to content
109 changes: 69 additions & 40 deletions src/boot/replaceRevive.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,72 +23,101 @@ export const SERIALIZED_TYPE_FIELD_NAME: '__serializedType__' = '__serializedTyp
*/
const SERIALIZED_TYPE_FIELD_NAME_ESCAPED: '__serializedType__value' = '__serializedType__value';

/**
* Custom replacer for inventive data types JSON doesn't handle.
*
* To be passed to `JSON.stringify` as its second argument. New
* replacement logic must also appear in `reviver` so they stay in
* sync.
*/
// 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.
// When identifying what kind of thing we're working with, we
// examine `origValue` instead of `value`, just in case calling
// `toJSON` on that kind of thing would remove its identifying
// features -- which is to say, just in case 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 (typeof origValue !== 'object' || origValue === null) {
// `origValue` can't be one of our interesting data types, so,
// just return it.
return origValue;
}

// 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',
);
switch (Object.getPrototypeOf(origValue)) {
// Flow bug: https://github.com/facebook/flow/issues/6110
case (ZulipVersion.prototype: $FlowFixMe):
return { data: value.raw(), [SERIALIZED_TYPE_FIELD_NAME]: 'ZulipVersion' };
case (URL.prototype: $FlowFixMe):
return { data: origValue.toString(), [SERIALIZED_TYPE_FIELD_NAME]: 'URL' };
case (GravatarURL.prototype: $FlowFixMe):
return { data: GravatarURL.serialize(value), [SERIALIZED_TYPE_FIELD_NAME]: 'GravatarURL' };
case (UploadedAvatarURL.prototype: $FlowFixMe):
return {
data: UploadedAvatarURL.serialize(value),
[SERIALIZED_TYPE_FIELD_NAME]: 'UploadedAvatarURL',
};
case (FallbackAvatarURL.prototype: $FlowFixMe):
return {
data: FallbackAvatarURL.serialize(value),
[SERIALIZED_TYPE_FIELD_NAME]: 'FallbackAvatarURL',
};
case (Immutable.Map.prototype: $FlowFixMe):
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This bit isn't NFC because instanceof Immutable.Map would accept an Immutable.OrderedMap (the one subclass Immutable has for that class), and this won't.

... Which is a fix to a latent bug! We don't currently use OrderedMap, but if we did, the old code would cheerfully serialize it, and then deserialize it as a plain Immutable.Map.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, right, good catch—I'll make a note in the commit message and remove the [nfc] mark.

return { data: value, [SERIALIZED_TYPE_FIELD_NAME]: 'ImmutableMap' };
default: {
// If the identity of the first item in the prototype chain
// isn't good enough as a distinguishing mark, we can put some
// plain conditions here.
}
}

if (typeof value === 'object' && value !== null && SERIALIZED_TYPE_FIELD_NAME in value) {
const copy = { ...value };
// 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',
);

// Ensure that objects with a [SERIALIZED_TYPE_FIELD_NAME] property
// round-trip.
if (SERIALIZED_TYPE_FIELD_NAME in origValue) {
const copy = { ...origValue };
delete copy[SERIALIZED_TYPE_FIELD_NAME];
return {
[SERIALIZED_TYPE_FIELD_NAME]: 'Object',
data: copy,
[SERIALIZED_TYPE_FIELD_NAME_ESCAPED]: value[SERIALIZED_TYPE_FIELD_NAME],
[SERIALIZED_TYPE_FIELD_NAME_ESCAPED]: origValue[SERIALIZED_TYPE_FIELD_NAME],
};
}

return value;
return origValue;
};

/**
* Custom reviver for inventive data types JSON doesn't handle.
*
* To be passed to `JSON.parse` as its second argument. New
* reviving logic must also appear in `replacer` so they stay in
* sync.
*/
const reviver = function reviver(key, value) {
if (value !== null && typeof value === 'object' && SERIALIZED_TYPE_FIELD_NAME in value) {
const data = value.data;
Expand Down