typeson icon indicating copy to clipboard operation
typeson copied to clipboard

Feature request: Support for Symbol-keys

Open ulrichb opened this issue 7 years ago • 5 comments

Symbol-typed keys (and their values) are also something which gets lost in a JSON round-trip.

So it would be nice if typeson.js also supports key-rivival (next to value-revival).

This would mean:

  • When iterating over object keys additionally iterate over symbols using Object.getOwnPropertySymbols() (and process also their values).
  • Provide a possibility to register symbols to be able to test for a symbol using key === registeredSymbol and to use the registeredSymbol-value to revive it again, e.g.:
const mySymbol = Symbol();
const otherSymbol = Symbol("symbol with description");

new Typeson().registerSymbolKeys({
    MySymbol: mySymbol,
    OtherSymbol: otherSymbol,
});
  • Change the symbol-keys to string-keys during encapsulate() and reverse it during revive().
    • Here we need to avoid conflicts with equally named string-keys. This could be done by prefixing/postfixing them with a fixed value, or maybe by a random value (+ test loop).

ulrichb avatar Oct 31 '18 14:10 ulrichb

Sounds like a great idea!

I think we should check stateObj.iterateSymbols to determine whether to iterate symbols (similar to how we do it with the stateObj properties iterateIn and iterateUnsetNumeric (there is also ownKeys, but we don't change behavior internally with that set or not)).

This would allow switching the state on or off depending on the context, allowing one to only need to iterate symbols within types or contexts where they are needed for revival (though one could have a type which enabled it for all objects).

We do something similar with the typeson-registry "sparseUndefined" preset, where we leverage replace to simply change state--to indicate that once we reach an array, we should allow iterating of unset numeric indexes of an array (in this case, so that we can cause each unset part of the array to be revived with undefined rather than null). We don't actually do or need to do any replacing despite our use of the replace method.

Instead of a separate method for registering symbol keys, however, I believe it should be sufficient to let types--including one generated dynamically at run-time to bake in the desired symbols--handle the serialization. (A generic one would be a good candidate for typeson-registry though, I think.)

As (plain) symbols are unique, we cannot serialize them so as to be revivable across origins, so I think the most meaningful scenario would be for a type to keep its own (probably in-memory) store of the symbols it encountered for later revival. The idea is similar to the "resurrectable" type in the typeson-registry in that we keep our own in-memory map (it supports reviving symbols too, btw, but not iterating them as keys).

One type of Symbol though which would be more meaningful to work cross-origin would be to have a type which checked any iterated symbols withSymbol.keyFor and serialized that value so that it could be used with Symbol.for upon revival.

brettz9 avatar Nov 01 '18 03:11 brettz9

Hi @brettz9,

thanks for taking a look at this proposal.

Regarding explicitly registering the symbols vs. implicitly serializing all symbols and deserializing "equivalent ones" (if I understood you correctly):

I think registering the symbol instances/values is necessary to be able to deserialize the same symbols.

Consider the following example.

// shared.js:
export const mySymbol = Symbol();

export const typeson = new Typeson().registerSymbolKeys({
    MySymbol: mySymbol,
});


// server.js:
import { mySymbol, typeson } from './shared';

const toBeSerialized = { a: 1, [mySymbol]: 2 };

sendToClient(typeson.stringify(toBeSerialized));


// client.js:
import { mySymbol, typeson } from './shared';

const deserialized = typeson.parseSync(receiveJson());

// Access should be possible with the same symbol:
const symbolValue = deserialized[mySymbol];

console.log(symbolValue); // expected: 2

Registering mySymbol (as "MySymbol") allows to deserialize the same symbol it allows to access deserialized[mySymbol] in client.js (exactly the same way as it's possible in server.js before serialization).

ulrichb avatar Nov 02 '18 11:11 ulrichb

Ahh, I see... Very interesting... So, there is a greater use case then just serializing and deserializing within the same realm, and this will work despite the symbols not literally being the same between the client and server.

But I still think a type could obviate the need for a separate registration method. Here's a utility to let you build such type (and we might want this on typeson-registry, though it should probably be added to a new "builders" folder as it builds types rather than just being one itself). The utility allows any or all symbols passed to it to be present on the object for it to be considered an object of that type.

// makeTypesonSymbolType.js

export default function makeTypesonSymbolType (typeName, symbols) {
    function generateUniqueKeyForObjectAndSymbol (obj) {
        const stringKeyNotOnObject = '...'; // Todo: Get this by checking `obj`
        return stringKeyNotOnObject;
    }

    const symbolStringMap = new Map();

    return {
        [typeName]: {
            testPlainObjects: true,
            test: (x, stateObj) => symbols.some((symbol) => symbol in x),
            replace (obj, stateObj) {
                const uniqueStrings = symbols.filter((symbol) => {
                    return symbol in obj;
                }).map((symbol) => {
                    const uniqueString = generateUniqueKeyForObjectAndSymbol(obj, symbol);
                    symbolStringMap.set(uniqueString, symbol);
                    obj[uniqueString] = obj[symbol];
                    return uniqueString;
                });
                return {obj, uniqueStrings};
            },
            revive ({obj, uniqueStrings}) {
                uniqueStrings.forEach((uniqueString) => {
                    const symbol = symbolStringMap.get(uniqueString);
                    obj[symbol] = obj[uniqueString];
                    delete obj[uniqueString];
                });
                return obj;
            }
        }
    };
}
// shared.js

import makeTypesonSymbolType from './makeTypesonSymbolType.js';
export const mySymbol = Symbol(); // eslint-disable-line symbol-description
export const typeson = new Typeson().register(makeTypesonSymbolType('mySymbolType', [mySymbol]));

I haven't tested, but I believe this should work now for such as your example if you implement generateUniqueKeyForObjectAndSymbol. (I didn't investigate/consider a performance and memory efficient algorithm to use.)

If you wanted a type which would iterate within arbitrary symbols rather than a predefined list we can check for, we'd need to change Typeson (and code would add stateObj.iterateSymbols = true; in the replace).

brettz9 avatar Nov 03 '18 09:11 brettz9

Come to think of it, while I think we should still add it to Typeson, you could probably get away with Symbol iteration as a type too, though instead iterating through any object with Object.getOwnPropertySymbols having a length and then generating unique strings for all found...

brettz9 avatar Nov 03 '18 11:11 brettz9

On further consideration, my approach probably wouldn't work well with cyclic structures, so I think the symbol iteration is something we should add...

brettz9 avatar Nov 29 '18 10:11 brettz9