-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: custom serialization #7223
Conversation
🦋 Changeset detectedLatest commit: 54acbc0 The changes in this PR will be included in the next version bump. This PR includes changesets to release 5 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Looks great, but what about deserialization those objects? |
That's what the createSerialized$ does |
Does this mean we might be able to serialize functions from other libraries, like vanilla js? |
aca1de8
to
47942f4
Compare
@Varixo @thejackshelton gaah I had forgotten to push the actual commit 😅 |
Yes exactly. You need to add a symbol prop that serializes the value and then provide a deserializer that will get called lazily. |
47942f4
to
9009520
Compare
commit: |
built with Refined Cloudflare Pages Action⚡ Cloudflare Pages Deployment
|
9009520
to
103e3c1
Compare
165ac6d
to
804f01d
Compare
804f01d
to
9070c30
Compare
: obj instanceof ComputedSignal && | ||
!(obj instanceof SerializerSignal) && | ||
(obj.$invalid$ || fastSkipSerialize(obj)) | ||
? NEEDS_COMPUTATION | ||
: obj.$untrackedValue$; | ||
if (v !== NEEDS_COMPUTATION) { | ||
discoveredValues.push(v); | ||
if (obj instanceof SerializerSignal) { | ||
promises.push( | ||
(obj.$computeQrl$ as any as QRLInternal<SerializerArg<any, any>>) | ||
.resolve() | ||
.then((arg) => { | ||
let data; | ||
if ((arg as any).serialize) { | ||
data = (arg as any).serialize(v); | ||
} | ||
if (data === undefined) { | ||
data = NEEDS_COMPUTATION; | ||
} | ||
serializationResults.set(obj, data); | ||
discoveredValues.push(data); | ||
}) | ||
); | ||
} else { | ||
discoveredValues.push(v); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if we could organise it somehow? It is very complicated right now.
What about doing just
if (obj instanceof WrappedSignal) {
} else if (obj instanceof ComputedSignal) {
} else if ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very thorough PR. Awesome work ⚡
* | ||
* This function must not return a promise. | ||
*/ | ||
deserialize: (data: S | undefined, previous: T | undefined) => T; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So on first render, the previous param is -> initial data / inital (3rd argument param of useSerializer$)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
on first run, the data is initial|undef. After serialization, data is what was serialized.
If the function uses reactive scope, it will rerun and then current
will be defined and data
will be undefined.
* | ||
* If you do not provide it, the object will be serialized as `undefined`. | ||
*/ | ||
serialize?: (customObject: T) => S | Promise<S>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's an example of when you may need the deserialize function but not the serialize function?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When you're just constructing an object from scope data and you don't have any state to serialize.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
wrong account 😓
I tried the changes locally and I'm having the following issue when using props as initial data: Plus a linting issue when using the
Link to MRE: https://github.com/ianlet/qwik-custom-serder-scope-issues/blob/main/src/components/qwik-mapbox.tsx Edit: actually we won't be able to |
Also, after playing with it a little bit, I think the So, IMO it should be 2 different callbacks as it is 2 different use cases, triggered after 2 different events. |
9070c30
to
d67bb36
Compare
I addressed all the comments I think. |
@ianlet BTW even when only using mapbox on the client, this is still useful. Just make the deserialize |
d67bb36
to
5126138
Compare
I tried to use the new syntax with Also, having to handle the const mapbox = useSerializer$<
mapboxgl.Map | undefined,
SerializationState | undefined
>(() => ({
initial: {
token: token.value,
mapStyle: mapStyle.value,
center: center.value,
},
serialize: (map: mapboxgl.Map | undefined) => {
if (!map) return;
return {
token,
mapStyle: map.getStyle() || undefined,
center: map.getCenter().toArray(),
};
},
deserialize: (state: SerializationState | undefined) => {
if (isServer) return;
const s = state || { token: token.value, mapStyle: mapStyle.value, center: center.value };
return new mapboxgl.Map({
container: ref.value!,
style: s.mapStyle,
center: s.center,
});
},
update: (map: mapboxgl.Map | undefined) => {
if (!map) return;
map.setStyle(mapStyle.value);
map.setCenter(center.value);
},
})); Note: ESLint is still not happy about tracking the useTask$(({ track }) => {
const map = track(mapbox);
map?.on("load", () => console.log("MAP LOADED"));
});
|
I played more with it and if you change a value that is tracked more than once, the serialized value is only updated the first time: Playground #1 So I tried to change it to directly use the signal in the
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PR Overview
This PR introduces custom serialization support by adding two new symbols (NoSerializeSymbol and SerializerSymbol) to control object serialization and by providing a new SerializerSignal type that enables custom serialization logic. Key changes include updates to the serialization and signal modules, added unit tests demonstrating custom serialization, and comprehensive documentation updates.
Reviewed Changes
File | Description |
---|---|
packages/qwik/src/core/shared/shared-serialization.unit.ts | Adds unit tests for NoSerializeSymbol and SerializerSymbol, as well as SerializerSignal behavior. |
packages/qwik/src/core/signal/signal.ts | Introduces type alias and minor refactoring for computed and wrapped signals, including read-only value enforcement. |
packages/qwik/src/core/shared/shared-serialization.ts | Updates to support SerializerSignal in both allocation and serialization flows. |
packages/qwik/src/core/signal/signal.public.ts & signal-api.ts | Exposes new createSerializer$ and createSerializerQrl APIs with improved type definitions and internal casting. |
Documentation and changeset files | Updates to document the two new symbols and the custom serialization API. |
Copilot reviewed 32 out of 32 changed files in this pull request and generated no comments.
Comments suppressed due to low confidence (2)
packages/qwik/src/core/shared/shared-serialization.ts:312
- [nitpick] Consider expanding this comment to further clarify why SerializerSignal is always marked as invalid, ensuring future maintainers understand the rationale behind recreating the custom object during deserialization.
// The serialized signal is always invalid so it can recreate the custom object
packages/qwik/src/core/signal/signal-api.ts:32
- [nitpick] Consider refining the type casting here to reduce the double 'any' cast and make the conversion to QRLInternal<SerializerArg<T, S>> more type-safe.
return new SerializerSignal<T, S>(null, arg as any as QRLInternal<SerializerArg<T, S>>);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very excited about this change. Left few clarifing questions.
packages/eslint-plugin-qwik/tests/valid-lexical-scope/valid-no-serialize-symbol.tsx
Outdated
Show resolved
Hide resolved
d76b001
to
d7f7793
Compare
Co-authored-by: Varixo <[email protected]> Co-authored-by: Miško Hevery <[email protected]>
d7f7793
to
4d056d4
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
@ianlet Thank you for your playgrounds, the problem is that you didn't use I added an eslint rule (yey vibe coding) that warns about this. |
This PR adds symbols to mark objects as (no/yes)serializable via symbols instead of via a Set, adds a symbol prop that will get called on serialization for custom serialization, and provides a new Signal type that lazily manages a non-serializable value.
Note that this is basically the same as
useComputed$
, except that it is invalidated during SSR so that when the value gets read on the client it will always run the compute function.Also note that
useComputed$
now passes the previous value to the compute function, to keep the implementation compact. This seems like it might be useful, should we document this?Everything works, looking for comments on implementation, naming etc.
TODO