sveltejs / kit

web development, streamlined
https://kit.svelte.dev
MIT License
18.16k stars 1.84k forks source link

Allow the serialisation/deserialisation of non-POJOs #9401

Open carlosV2 opened 1 year ago

carlosV2 commented 1 year ago

Describe the problem

In my current project I'm in need of sending data back and forth the server for which I've built a series of objects that I can easily serialise and deserialise with the amazing devalue library. This includes the initial load of data via some contraption like this:

export const load = (async () => {
  const data = // ... load data. f.e. from DB
  return { data: pack(data) };
}) satisfies PageServerLoad;

In this example, the function pack is a wrapper of the stringify function from devalue with a set of object types passed to this function as reducers.

On the client side, I'm obviously running the opposite method to get the data back with the equivalent revivers.

I particularly like this option because it allows me to introduce simple functions to these objects that I can run seamlessly regardless of the environment.

At the current stage, however, I'm in need of deduplicating code by separating layout logic from page logic. More often than not, I need to use the layout data to perform some logic on the page (think about the layout loading a resource and the page validating access to this resource).

Due to the current SvelteKit behaviour I can't return any non-POJO on the layout as it will be serialised for the client so I'm forced to use my pack function. However, when I pull this date in the page, I have to first unpack it to be able to use it which feels like a waste of resources.

The same waste of resources can happen on the client side. Attempting to use the data in the client layout requires to unpack it first but so does attempting to use it in the page. With a solution proposed here, the serialisation could be handled by SvelteKit and the same result could be used as many times as needed.

Describe the proposed solution

I think it would be nice if we could add some code in the hooks.server.ts and the hooks.client.ts to allocate for this. For example:

type Reducer = (value: any) => any;
type Reviver = (value: any) => any;
type SerialiseHandle = { reducer?: Reducer, reviver?: Reviver };

export const serialisationHandles: Record<string, SerialiseHandle> = {
   MyObj: {
      reducer: (value) => value instanceof MyObj && [value.param1, value.param2],
      reviver: ([param1, param2]) => new MyObject(param1, param2)
   },
   MyOtherObject: { /* ... */ }
}

This could be replicated similarly on the client hook thus allowing for the potential serialisation/deserialisation of dedicated object (i.e. a server version of MyObj and a client version of it). Alternatively, the handles can be abstracted into a third file to keep consistency between both environments.

Alternatives considered

I tried by returning the raw object on the layout which allows for the exact object to be retrieved on the page. Then I tried to re-return the object with the custom serialisation applied but it still complained about the non-POJOs.

Right now my only options are to either pack/unpack the data or to reload it altogether which it is, in both cases, a waste of compute power.

In the client side, there is the possibility of using a context to prevent from wasting too many resources.

Importance

nice to have

Additional Information

No response

dkantereivin commented 1 year ago

This would be super useful as it's already supported by devalue (as mentioned) and this would simply expose the appropriate API. In my particular usecase, I'd like to be able to write a replacer that converts MongoDB ObjectIDs to their hex strings, so that way I don't need to manually convert them every time I want to return an object that contains an ObjectID (needless to say, this is frequent).

pkb-pmj commented 1 year ago

Just encountered the same issue with Prisma Decimal data type. I quite like the proposed solution, it would be useful and rather easy to use.

Beiri22 commented 1 year ago

This would be really nice. I'd like to use luxon as date library. The luxon DateTime type is not a simple POJO, so you need this API of devalue for it to work. I'm not sure, if the custom serializer / deserializer functions have to be put in the hooks file; or maybe better into the sveltekit configuration; but either way would be a big plus!

@Rich-Harris What do you think? Wouldn't such a feature harvest the full potential of devalue?

clavin commented 11 months ago

Adding on, I'm using the official polyfill for the upcoming Temporal standard library in my project and I want to be able to send Temporal.Instants (and such) between the server and client, which should be supported in the future anyways. Having this customization of devalue supported in SvelteKit would be handy. 😄

AndreasHald commented 7 months ago

This is very interesting - we are also using the mongodb ObjectId class heavily and this would definitely make it a lot simpler for us.

I tried to give a PR a go, but the point where I got stuck is probably why this is tricky to implement - when server side rendering pages kit uses uneval and not stringify and supporting custom types here would require a replacer and notably that the class for recreating the type is in scope when the code runs.

If you would do this in hooks.(client|server) it now becomes a significantly more complex api.

JonathonRP commented 2 months ago

Adding on, I'm using the official polyfill for the upcoming Temporal standard library in my project and I want to be able to send Temporal.Instants (and such) between the server and client, which should be supported in the future anyways. Having this customization of devalue supported in SvelteKit would be handy. 😄

Same using Temporal plain date for financial admin dashboard and selecting/filtering on date ranges and Temporal and Date are not serialized. This would be great! (Also superjson already allows this but sveltekit using devalue internally gives no access to swapping these things out)

Fyi could look at trpc implementation because they seem to have it figured out. With transformers that work for server and client (like what's being asked for) and even swapping with others (like I was posing with superjson).

AlbertMarashi commented 3 weeks ago

@Rich-Harris also need this with my surrealdb, which introduces RecordID class that gets serialised into json instead of being represented as a class on the client side, and messes with typings due to the fact of how the return data is typed