Skip to content

Commit 991cec0

Browse files
committed
fix(serdes): handle cyclic Store deser
I can't figure out how to reproduce the issue, but the Insights app was triggering it and this is obviously the right approach.
1 parent fdc30ee commit 991cec0

File tree

3 files changed

+39
-28
lines changed

3 files changed

+39
-28
lines changed

.changeset/tidy-chefs-tickle.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@qwik.dev/core': patch
3+
---
4+
5+
fix: During deserialization, stores now correctly handle cyclic references to themselves

packages/qwik/src/core/shared/serdes/allocate.ts

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { TypeIds, _constants, type Constants, parseQRL, deserializeData, resolvers } from './index';
1+
import { TypeIds, _constants, type Constants, parseQRL, resolvers } from './index';
22
import type { DomContainer } from '../../client/dom-container';
33
import type { ElementVNode, VNode } from '../../client/vnode-impl';
44
import { vnode_isVNode, ensureMaterialized, vnode_getNode, vnode_locate } from '../../client/vnode';
@@ -17,12 +17,14 @@ import { qError, QError } from '../error/error';
1717
import { JSXNodeImpl, createPropsProxy } from '../jsx/jsx-runtime';
1818
import type { DeserializeContainer } from '../types';
1919
import { _UNINITIALIZED } from '../utils/constants';
20+
import { needsInflation } from './deser-proxy';
21+
22+
export const pendingStoreTargents = new Map<object, { t: TypeIds; v: unknown }>();
2023

2124
export const allocate = (container: DeserializeContainer, typeId: number, value: unknown): any => {
22-
if (typeId === TypeIds.Plain) {
23-
return value;
24-
}
2525
switch (typeId) {
26+
case TypeIds.Plain:
27+
return value;
2628
case TypeIds.RootRef:
2729
return container.$getObjectById$(value as number);
2830
case TypeIds.ForwardRef:
@@ -84,24 +86,23 @@ export const allocate = (container: DeserializeContainer, typeId: number, value:
8486
return new AsyncComputedSignalImpl(container as any, null!);
8587
case TypeIds.SerializerSignal:
8688
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);
89+
case TypeIds.Store: {
90+
const data = value as [TypeIds, unknown];
91+
// We need to allocate the store first, before we inflate its data, because the data can
92+
// reference the store itself (circular)
93+
// Note: the actual store data will be inflated in inflate()
94+
const t = data[0] as TypeIds;
95+
const v = data[1];
96+
const storeValue = allocate(container, t, v);
97+
const store = getOrCreateStore(storeValue, StoreFlags.NONE, container as DomContainer);
98+
if (needsInflation(t)) {
99+
pendingStoreTargents.set(store, { t, v });
100+
}
101+
// We must store the reference so it doesn't get deserialized again in inflate()
102+
data[0] = TypeIds.Plain;
103+
data[1] = storeValue;
104+
return store;
105+
}
105106
case TypeIds.URLSearchParams:
106107
return new URLSearchParams(value as string);
107108
case TypeIds.FormData:

packages/qwik/src/core/shared/serdes/inflate.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import type { QRLInternal } from '../qrl/qrl-class';
2424
import type { DeserializeContainer, HostElement } from '../types';
2525
import { ChoreType } from '../util-chore-type';
2626
import { _CONST_PROPS, _VAR_PROPS } from '../utils/constants';
27+
import { pendingStoreTargents } from './allocate';
2728
import { deserializeData, inflateQRL, resolvers, TypeIds } from './index';
2829

2930
export const inflate = (
@@ -84,15 +85,19 @@ export const inflate = (
8485
(target as any)[SERIALIZABLE_STATE][0] = (data as any[])[0];
8586
break;
8687
case TypeIds.Store: {
88+
// Inflate the store target
89+
const store = target as object;
90+
const storeTarget = pendingStoreTargents.get(store);
91+
if (storeTarget) {
92+
pendingStoreTargents.delete(store);
93+
inflate(container, store, storeTarget.t, storeTarget.v);
94+
}
8795
/**
88-
* Note that cycles between stores and their targets can cause this inflation to happen on
89-
* already inflated stores, but that's ok because the flags and effects are still the same.
90-
*
91-
* Also note that we don't do anything with the innerstores we added during serialization,
92-
* because they are already inflated in the first step of inflate().
96+
* Note that we don't do anything with the innerstores we added during serialization, because
97+
* they are already inflated in the deserialize of the data, above.
9398
*/
9499
const [, flags, effects] = data as unknown[];
95-
const storeHandler = getStoreHandler(target as object)!;
100+
const storeHandler = getStoreHandler(store)!;
96101
storeHandler.$flags$ = flags as StoreFlags;
97102
storeHandler.$effects$ = effects as any;
98103
break;

0 commit comments

Comments
 (0)