Skip to content

Commit 51b76e9

Browse files
committed
refactor(serdes): move parts into their own files
1 parent 74b6650 commit 51b76e9

20 files changed

+2225
-2180
lines changed

packages/qwik/src/core/client/dom-container.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,8 @@ import { QError, qError } from '../shared/error/error';
77
import { ERROR_CONTEXT, isRecoverable } from '../shared/error/error-handling';
88
import type { QRL } from '../shared/qrl/qrl.public';
99
import { _SharedContainer } from '../shared/shared-container';
10-
import {
11-
getObjectById,
12-
inflateQRL,
13-
parseQRL,
14-
preprocessState,
15-
wrapDeserializerProxy,
16-
} from '../shared/shared-serialization';
10+
import { getObjectById, inflateQRL, parseQRL, preprocessState } from '../shared/serdes/index';
11+
import { wrapDeserializerProxy } from '../shared/serdes/deser-proxy';
1712
import { QContainerValue, type HostElement, type ObjToProxyMap } from '../shared/types';
1813
import { EMPTY_ARRAY } from '../shared/utils/flyweight';
1914
import {

packages/qwik/src/core/internal.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export {
6060
preprocessState as _preprocessState,
6161
_serializationWeakRef,
6262
_serialize,
63-
} from './shared/shared-serialization';
63+
} from './shared/serdes/index';
6464
export { _CONST_PROPS, _IMMUTABLE, _VAR_PROPS, _UNINITIALIZED } from './shared/utils/constants';
6565
export { EMPTY_ARRAY as _EMPTY_ARRAY } from './shared/utils/flyweight';
6666
export { _restProps } from './shared/utils/prop';

packages/qwik/src/core/shared/qrl/qrl.unit.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { qrl } from './qrl';
33
import { describe, test, assert, assertType, expectTypeOf } from 'vitest';
44
import { $, type QRL } from './qrl.public';
55
import { useLexicalScope } from '../../use/use-lexical-scope.public';
6-
import { createSerializationContext, parseQRL, qrlToString } from '../shared-serialization';
6+
import { createSerializationContext, parseQRL, qrlToString } from '../serdes/index';
77

88
function matchProps(obj: any, properties: Record<string, any>) {
99
for (const [key, value] of Object.entries(properties)) {
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import { TypeIds, _constants, type Constants, parseQRL, deserializeData, resolvers } from './index';
2+
import type { DomContainer } from '../../client/dom-container';
3+
import type { ElementVNode, VNode } from '../../client/types';
4+
import { vnode_isVNode, ensureMaterialized, vnode_getNode, vnode_locate } from '../../client/vnode';
5+
import { AsyncComputedSignalImpl } from '../../reactive-primitives/impl/async-computed-signal-impl';
6+
import { ComputedSignalImpl } from '../../reactive-primitives/impl/computed-signal-impl';
7+
import { SerializerSignalImpl } from '../../reactive-primitives/impl/serializer-signal-impl';
8+
import { SignalImpl } from '../../reactive-primitives/impl/signal-impl';
9+
import { getOrCreateStore } from '../../reactive-primitives/impl/store';
10+
import { WrappedSignalImpl } from '../../reactive-primitives/impl/wrapped-signal-impl';
11+
import { SubscriptionData, type NodePropData } from '../../reactive-primitives/subscription-data';
12+
import { StoreFlags } from '../../reactive-primitives/types';
13+
import { createResourceReturn } from '../../use/use-resource';
14+
import { Task } from '../../use/use-task';
15+
import { componentQrl } from '../component.public';
16+
import { qError, QError } from '../error/error';
17+
import { JSXNodeImpl, createPropsProxy } from '../jsx/jsx-runtime';
18+
import type { DeserializeContainer } from '../types';
19+
import { _UNINITIALIZED } from '../utils/constants';
20+
21+
export const allocate = (container: DeserializeContainer, typeId: number, value: unknown): any => {
22+
if (typeId === TypeIds.Plain) {
23+
return value;
24+
}
25+
switch (typeId) {
26+
case TypeIds.RootRef:
27+
return container.$getObjectById$(value as number);
28+
case TypeIds.ForwardRef:
29+
if (!container.$forwardRefs$) {
30+
throw qError(QError.serializeErrorCannotAllocate, ['forward ref']);
31+
}
32+
const rootRef = container.$forwardRefs$[value as number];
33+
if (rootRef === -1) {
34+
return _UNINITIALIZED;
35+
} else {
36+
return container.$getObjectById$(rootRef);
37+
}
38+
case TypeIds.ForwardRefs:
39+
return value;
40+
case TypeIds.Constant:
41+
return _constants[value as Constants];
42+
case TypeIds.Array:
43+
return Array((value as any[]).length / 2);
44+
case TypeIds.Object:
45+
return {};
46+
case TypeIds.QRL:
47+
case TypeIds.PreloadQRL:
48+
const qrl =
49+
typeof value === 'number'
50+
? // root reference
51+
container.$getObjectById$(value)
52+
: value;
53+
return parseQRL(qrl as string);
54+
case TypeIds.Task:
55+
return new Task(-1, -1, null!, null!, null!, null);
56+
case TypeIds.Resource: {
57+
const res = createResourceReturn(
58+
container as any,
59+
// we don't care about the timeout value
60+
undefined,
61+
undefined
62+
);
63+
res.loading = false;
64+
return res;
65+
}
66+
case TypeIds.URL:
67+
return new URL(value as string);
68+
case TypeIds.Date:
69+
return new Date(value as number);
70+
case TypeIds.Regex:
71+
const idx = (value as string).lastIndexOf('/');
72+
return new RegExp((value as string).slice(1, idx), (value as string).slice(idx + 1));
73+
case TypeIds.Error:
74+
return new Error();
75+
case TypeIds.Component:
76+
return componentQrl(null!);
77+
case TypeIds.Signal:
78+
return new SignalImpl(container as any, 0);
79+
case TypeIds.WrappedSignal:
80+
return new WrappedSignalImpl(container as any, null!, null!, null!);
81+
case TypeIds.ComputedSignal:
82+
return new ComputedSignalImpl(container as any, null!);
83+
case TypeIds.AsyncComputedSignal:
84+
return new AsyncComputedSignalImpl(container as any, null!);
85+
case TypeIds.SerializerSignal:
86+
return new SerializerSignalImpl(container as any, null!);
87+
case TypeIds.Store:
88+
/**
89+
* We have a problem here: In theory, both the store and the target need to be present at
90+
* allocate time before inflation can happen. However, that makes the code really complex.
91+
* Instead, we deserialize the target here, which will already allocate and inflate this store
92+
* if there is a cycle (because the original allocation for the store didn't complete yet).
93+
* Because we have a map of target -> store, we will reuse the same store instance after
94+
* target deserialization. So in that case, we will be running inflation twice on the same
95+
* store, but that is not a problem, very little overhead and the code is way simpler.
96+
*/
97+
const storeValue = deserializeData(
98+
container,
99+
(value as any[])[0] as TypeIds,
100+
(value as any[])[1]
101+
);
102+
(value as any[])[0] = TypeIds.Plain;
103+
(value as any[])[1] = storeValue;
104+
return getOrCreateStore(storeValue, StoreFlags.NONE, container as DomContainer);
105+
case TypeIds.URLSearchParams:
106+
return new URLSearchParams(value as string);
107+
case TypeIds.FormData:
108+
return new FormData();
109+
case TypeIds.JSXNode:
110+
return new JSXNodeImpl(null!, null!, null!, null!, -1, null);
111+
case TypeIds.BigInt:
112+
return BigInt(value as string);
113+
case TypeIds.Set:
114+
return new Set();
115+
case TypeIds.Map:
116+
return new Map();
117+
case TypeIds.Promise:
118+
let resolve!: (value: any) => void;
119+
let reject!: (error: any) => void;
120+
const promise = new Promise((res, rej) => {
121+
resolve = res;
122+
reject = rej;
123+
});
124+
resolvers.set(promise, [resolve, reject]);
125+
// Don't leave unhandled promise rejections
126+
promise.catch(() => {});
127+
return promise;
128+
case TypeIds.Uint8Array:
129+
const encodedLength = (value as string).length;
130+
const blocks = encodedLength >>> 2;
131+
const rest = encodedLength & 3;
132+
const decodedLength = blocks * 3 + (rest ? rest - 1 : 0);
133+
return new Uint8Array(decodedLength);
134+
case TypeIds.PropsProxy:
135+
return createPropsProxy(null!, null);
136+
case TypeIds.VNode:
137+
return retrieveVNodeOrDocument(container, value);
138+
case TypeIds.RefVNode:
139+
const vNode = retrieveVNodeOrDocument(container, value);
140+
if (vnode_isVNode(vNode)) {
141+
/**
142+
* If we have a ref, we need to ensure the element is materialized.
143+
*
144+
* Example:
145+
*
146+
* ```
147+
* const Cmp = component$(() => {
148+
* const element = useSignal<HTMLDivElement>();
149+
*
150+
* useVisibleTask$(() => {
151+
* element.value!.innerHTML = 'I am the innerHTML content!';
152+
* });
153+
*
154+
* return (
155+
* <div ref={element} />
156+
* );
157+
* });
158+
* ```
159+
*
160+
* If we don't materialize early element with ref property, and change element innerHTML it
161+
* will be applied to a vnode tree during the lazy materialization, and it is wrong.
162+
*
163+
* Next if we rerender component it will remove applied innerHTML, because the system thinks
164+
* it is a part of the vnode tree.
165+
*/
166+
ensureMaterialized(vNode as ElementVNode);
167+
return vnode_getNode(vNode);
168+
} else {
169+
throw qError(QError.serializeErrorExpectedVNode, [typeof vNode]);
170+
}
171+
case TypeIds.SubscriptionData:
172+
return new SubscriptionData({} as NodePropData);
173+
default:
174+
throw qError(QError.serializeErrorCannotAllocate, [typeId]);
175+
}
176+
};
177+
export function retrieveVNodeOrDocument(
178+
container: DeserializeContainer,
179+
value: unknown | null
180+
): VNode | Document | undefined {
181+
return value
182+
? (container as any).rootVNode
183+
? vnode_locate((container as any).rootVNode, value as string)
184+
: undefined
185+
: container.element?.ownerDocument;
186+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { TypeIds } from './index';
2+
import type { DomContainer } from '../../client/dom-container';
3+
import { vnode_isVNode } from '../../client/vnode';
4+
import { isObject } from '../utils/types';
5+
import { allocate } from './allocate';
6+
import { inflate } from './inflate';
7+
8+
/** Arrays/Objects are special-cased so their identifiers is a single digit. */
9+
export const needsInflation = (typeId: TypeIds) =>
10+
typeId >= TypeIds.Error || typeId === TypeIds.Array || typeId === TypeIds.Object;
11+
const deserializedProxyMap = new WeakMap<object, unknown[]>();
12+
type DeserializerProxy<T extends object = object> = T & { [SERIALIZER_PROXY_UNWRAP]: object };
13+
14+
export const isDeserializerProxy = (value: unknown): value is DeserializerProxy => {
15+
return isObject(value) && SERIALIZER_PROXY_UNWRAP in value;
16+
};
17+
18+
export const SERIALIZER_PROXY_UNWRAP = Symbol('UNWRAP');
19+
/** Call this on the serialized root state */
20+
export const wrapDeserializerProxy = (container: DomContainer, data: unknown): unknown[] => {
21+
if (
22+
!Array.isArray(data) || // must be an array
23+
vnode_isVNode(data) || // and not a VNode or Slot
24+
isDeserializerProxy(data) // and not already wrapped
25+
) {
26+
return data as any;
27+
}
28+
let proxy = deserializedProxyMap.get(data);
29+
if (!proxy) {
30+
const target = Array(data.length / 2).fill(undefined);
31+
proxy = new Proxy(target, new DeserializationHandler(container, data)) as unknown[];
32+
deserializedProxyMap.set(data, proxy);
33+
}
34+
return proxy;
35+
};
36+
class DeserializationHandler implements ProxyHandler<object> {
37+
public $length$: number;
38+
39+
constructor(
40+
public $container$: DomContainer,
41+
public $data$: unknown[]
42+
) {
43+
this.$length$ = this.$data$.length / 2;
44+
}
45+
46+
get(target: unknown[], property: PropertyKey, receiver: object) {
47+
if (property === SERIALIZER_PROXY_UNWRAP) {
48+
// Note that this will only be partially filled in
49+
return target;
50+
}
51+
const i =
52+
typeof property === 'number'
53+
? property
54+
: typeof property === 'string'
55+
? parseInt(property as string, 10)
56+
: NaN;
57+
if (Number.isNaN(i) || i < 0 || i >= this.$length$) {
58+
return Reflect.get(target, property, receiver);
59+
}
60+
// The serialized data is an array with 2 values for each item
61+
const idx = i * 2;
62+
const typeId = this.$data$[idx] as number;
63+
const value = this.$data$[idx + 1];
64+
if (typeId === TypeIds.Plain) {
65+
// The value is already cached
66+
return value;
67+
}
68+
69+
const container = this.$container$;
70+
const propValue = allocate(container, typeId, value);
71+
72+
Reflect.set(target, property, propValue);
73+
this.$data$[idx] = TypeIds.Plain;
74+
this.$data$[idx + 1] = propValue;
75+
76+
/** We stored the reference, so now we can inflate, allowing cycles */
77+
if (needsInflation(typeId)) {
78+
inflate(container, propValue, typeId, value);
79+
}
80+
81+
return propValue;
82+
}
83+
84+
has(target: object, property: PropertyKey) {
85+
if (property === SERIALIZER_PROXY_UNWRAP) {
86+
return true;
87+
}
88+
return Object.prototype.hasOwnProperty.call(target, property);
89+
}
90+
91+
set(target: object, property: PropertyKey, value: any, receiver: object) {
92+
if (property === SERIALIZER_PROXY_UNWRAP) {
93+
return false;
94+
}
95+
const out = Reflect.set(target, property, value, receiver);
96+
const i = typeof property === 'number' ? property : parseInt(property as string, 10);
97+
if (Number.isNaN(i) || i < 0 || i >= this.$data$.length / 2) {
98+
return out;
99+
}
100+
const idx = i * 2;
101+
this.$data$[idx] = TypeIds.Plain;
102+
this.$data$[idx + 1] = value;
103+
return true;
104+
}
105+
}

0 commit comments

Comments
 (0)