diff --git a/src/boot/replaceRevive.js b/src/boot/replaceRevive.js index c9f43746500..7c1eb0e9f39 100644 --- a/src/boot/replaceRevive.js +++ b/src/boot/replaceRevive.js @@ -23,6 +23,13 @@ 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. @@ -30,65 +37,87 @@ 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): + 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;