Skip to content

Commit cdef8d5

Browse files
committed
use forward refs for shallow objects
1 parent d43ce59 commit cdef8d5

File tree

13 files changed

+132
-31
lines changed

13 files changed

+132
-31
lines changed

packages/qwik-router/src/middleware/request-handler/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export { getErrorHtml, ServerError } from './error-handler';
22
export { mergeHeadersCookies } from './cookie';
33
export { AbortMessage, RedirectMessage } from './redirect-handler';
44
export { requestHandler } from './request-handler';
5+
export { RequestEvShareQData } from './request-event';
56
export { _TextEncoderStream_polyfill } from './polyfill';
67
export type {
78
CacheControl,

packages/qwik-router/src/middleware/request-handler/request-event.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export const RequestEvSharedActionId = '@actionId';
3939
export const RequestEvSharedActionFormData = '@actionFormData';
4040
export const RequestEvSharedNonce = '@nonce';
4141
export const RequestEvShareServerTiming = '@serverTiming';
42+
/** @internal */
4243
export const RequestEvShareQData = 'qData';
4344

4445
export function createRequestEvent(

packages/qwik-router/src/middleware/request-handler/resolve-request-handlers.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ export function loadersMiddleware(routeLoaders: LoaderInternal[]): RequestHandle
245245

246246
// mark skipped loaders as null
247247
for (const skippedLoader of skippedLoaders) {
248-
loaders[skippedLoader.__id] = _UNINITIALIZED;
248+
loaders[skippedLoader.__id] = null;
249249
}
250250
} else {
251251
currentLoaders = routeLoaders;
@@ -583,10 +583,10 @@ export async function renderQData(requestEv: RequestEvent) {
583583
const loaders: Record<string, unknown> = {};
584584
for (const loaderId in allLoaders) {
585585
const loader = allLoaders[loaderId];
586-
if (typeof loader === 'object' && loader !== null && SerializerSymbol in loader) {
587-
(loader as any)[SerializerSymbol] = undefined;
588-
}
589-
if (loader !== _UNINITIALIZED) {
586+
if (loader !== null) {
587+
if (typeof loader === 'object' && SerializerSymbol in loader) {
588+
(loader as any)[SerializerSymbol] = undefined;
589+
}
590590
loaders[loaderId] = loader;
591591
}
592592
}

packages/qwik-router/src/middleware/request-handler/router.api.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,11 @@ export interface RequestEventLoader<PLATFORM = QwikRouterPlatform> extends Reque
150150
resolveValue: ResolveValue;
151151
}
152152

153+
// Warning: (ae-internal-missing-underscore) The name "RequestEvShareQData" should be prefixed with an underscore because the declaration is marked as @internal
154+
//
155+
// @internal (undocumented)
156+
export const RequestEvShareQData = "qData";
157+
153158
// @public (undocumented)
154159
export type RequestHandler<PLATFORM = QwikRouterPlatform> = (ev: RequestEvent<PLATFORM>) => Promise<void> | void;
155160

packages/qwik-router/src/static/worker-thread.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { _deserialize, _serialize, _verifySerializable } from '@qwik.dev/core/internal';
22
import type { ServerRequestEvent } from '@qwik.dev/router/middleware/request-handler';
3-
import { requestHandler } from '@qwik.dev/router/middleware/request-handler';
3+
import { requestHandler, RequestEvShareQData } from '@qwik.dev/router/middleware/request-handler';
44
import { WritableStream } from 'node:stream/web';
55
import { pathToFileURL } from 'node:url';
66
import type { QwikSerializer } from '../middleware/request-handler/types';
@@ -12,7 +12,6 @@ import type {
1212
StaticWorkerRenderResult,
1313
System,
1414
} from './types';
15-
import { RequestEvShareQData } from '../middleware/request-handler/request-event';
1615

1716
export async function workerThread(sys: System) {
1817
const ssgOpts = sys.getOptions();

packages/qwik/src/core/reactive-primitives/impl/store.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { pad, qwikDebugToString } from '../../debug';
22
import { assertTrue } from '../../shared/error/assert';
33
import { tryGetInvokeContext } from '../../use/use-core';
4-
import { isSerializableObject } from '../../shared/utils/types';
4+
import { isObject, isSerializableObject } from '../../shared/utils/types';
55
import type { Container } from '../../shared/types';
66
import {
77
addQrlToSerializationCtx,
@@ -124,8 +124,7 @@ export class StoreHandler implements ProxyHandler<StoreTarget> {
124124
const flags = this.$flags$;
125125
if (
126126
flags & StoreFlags.RECURSIVE &&
127-
typeof value === 'object' &&
128-
value !== null &&
127+
isObject(value) &&
129128
!Object.isFrozen(value) &&
130129
!isStore(value) &&
131130
!Object.isFrozen(target)

packages/qwik/src/core/reactive-primitives/utils.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type { Container, HostElement } from '../shared/types';
99
import { ChoreType } from '../shared/util-chore-type';
1010
import { ELEMENT_PROPS, OnRenderProp } from '../shared/utils/markers';
1111
import { SerializerSymbol } from '../shared/utils/serialize-utils';
12+
import { isObject } from '../shared/utils/types';
1213
import type { ISsrNode, SSRContainer } from '../ssr/ssr-types';
1314
import { TaskFlags, isTask } from '../use/use-task';
1415
import { ComputedSignalImpl } from './impl/computed-signal-impl';
@@ -147,7 +148,5 @@ export const triggerEffects = (
147148
export const isSerializerObj = <T extends { [SerializerSymbol]: (obj: any) => any }, S>(
148149
obj: unknown
149150
): obj is CustomSerializable<T, S> => {
150-
return (
151-
typeof obj === 'object' && obj !== null && typeof (obj as any)[SerializerSymbol] === 'function'
152-
);
151+
return isObject(obj) && typeof (obj as any)[SerializerSymbol] === 'function';
153152
};

packages/qwik/src/core/shared/error/error.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { logErrorAndStop } from '../utils/log';
22
import { qDev } from '../utils/qdev';
3+
import { isObject } from '../utils/types';
34

45
export const codeToText = (code: number, ...parts: any[]): string => {
56
if (qDev) {
@@ -61,7 +62,7 @@ export const codeToText = (code: number, ...parts: any[]): string => {
6162
if (parts.length) {
6263
text = text.replaceAll(/{{(\d+)}}/g, (_, index) => {
6364
let v = parts[index];
64-
if (v && typeof v === 'object' && v.constructor === Object) {
65+
if (v && isObject(v) && v.constructor === Object) {
6566
v = JSON.stringify(v).slice(0, 50);
6667
}
6768
return v;

packages/qwik/src/core/shared/shared-serialization.ts

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,14 @@ import { SignalImpl } from '../reactive-primitives/impl/signal-impl';
5656
import { ComputedSignalImpl } from '../reactive-primitives/impl/computed-signal-impl';
5757
import { WrappedSignalImpl } from '../reactive-primitives/impl/wrapped-signal-impl';
5858
import { SerializerSignalImpl } from '../reactive-primitives/impl/serializer-signal-impl';
59+
import { isObject } from './utils/types';
5960

6061
const deserializedProxyMap = new WeakMap<object, unknown[]>();
6162

6263
type DeserializerProxy<T extends object = object> = T & { [SERIALIZER_PROXY_UNWRAP]: object };
6364

6465
export const isDeserializerProxy = (value: unknown): value is DeserializerProxy => {
65-
return typeof value === 'object' && value !== null && SERIALIZER_PROXY_UNWRAP in value;
66+
return isObject(value) && SERIALIZER_PROXY_UNWRAP in value;
6667
};
6768

6869
export const SERIALIZER_PROXY_UNWRAP = Symbol('UNWRAP');
@@ -887,6 +888,7 @@ async function serialize(serializationContext: SerializationContext): Promise<vo
887888
let forwardRefsId = 0;
888889
const promises: Set<Promise<unknown>> = new Set();
889890
const preloadQrls = new Set<QRLInternal>();
891+
const uninitializedRefs = new Map<unknown, number>();
890892
let parent: unknown = null;
891893
const isRootObject = () => depth === 0;
892894

@@ -960,7 +962,7 @@ async function serialize(serializationContext: SerializationContext): Promise<vo
960962
output(TypeIds.Constant, Constants.Fragment);
961963
} else if (isQrl(value)) {
962964
outputRootRef(value, () => {
963-
const qrl = qrlToString(serializationContext, value);
965+
const qrl = qrlToString(serializationContext, value, uninitializedRefs);
964966
const type = preloadQrls.has(value) ? TypeIds.PreloadQRL : TypeIds.QRL;
965967
if (isRootObject()) {
966968
output(type, qrl);
@@ -1112,6 +1114,10 @@ async function serialize(serializationContext: SerializationContext): Promise<vo
11121114
return new PromiseResult(TypeIds.SerializerSignal, resolved, resolvedValue, null, null);
11131115
});
11141116
output(TypeIds.ForwardRef, forwardRef);
1117+
} else if (result === _UNINITIALIZED && !uninitializedRefs.has(value)) {
1118+
const forwardRefId = forwardRefsId++;
1119+
uninitializedRefs.set(value, forwardRefId);
1120+
output(TypeIds.ForwardRef, forwardRefId);
11151121
} else {
11161122
depth--;
11171123
writeValue(result);
@@ -1349,6 +1355,14 @@ async function serialize(serializationContext: SerializationContext): Promise<vo
13491355
rootsLength = serializationContext.$roots$.length;
13501356
}
13511357

1358+
if (uninitializedRefs.size) {
1359+
for (const [value, forwardRefId] of uninitializedRefs) {
1360+
$writer$.write(',');
1361+
forwardRefs[forwardRefId] = serializationContext.$roots$.length;
1362+
writeValue(value);
1363+
}
1364+
}
1365+
13521366
if (forwardRefs.length) {
13531367
$writer$.write(',');
13541368
$writer$.write(TypeIds.ForwardRefs + ',');
@@ -1412,7 +1426,8 @@ function serializeWrappingFn(
14121426

14131427
export function qrlToString(
14141428
serializationContext: SerializationContext,
1415-
value: QRLInternal | SyncQRLInternal
1429+
value: QRLInternal | SyncQRLInternal,
1430+
uninitializedRefs?: Map<unknown, number>
14161431
) {
14171432
let symbol = value.$symbol$;
14181433
let chunk = value.$chunk$;
@@ -1464,8 +1479,17 @@ export function qrlToString(
14641479
if (i > 0) {
14651480
serializedReferences += ' ';
14661481
}
1482+
const captureRef = value.$captureRef$[i];
1483+
if (
1484+
isObject(captureRef) &&
1485+
uninitializedRefs &&
1486+
uninitializedRefs.has(captureRef) &&
1487+
SerializerSymbol in captureRef
1488+
) {
1489+
captureRef[SerializerSymbol] = undefined;
1490+
}
14671491
// We refer by id so every capture needs to be a root
1468-
serializedReferences += serializationContext.$addRoot$(value.$captureRef$[i]);
1492+
serializedReferences += serializationContext.$addRoot$(captureRef);
14691493
}
14701494
qrlStringInline += `[${serializedReferences}]`;
14711495
} else if (value.$capture$ && value.$capture$.length > 0) {
@@ -1692,7 +1716,7 @@ function shouldTrackObj(obj: unknown) {
16921716
return (
16931717
// THINK: Not sure if we need to keep track of functions (QRLs) Let's skip them for now.
16941718
// and see if we have a test case which requires them.
1695-
(typeof obj === 'object' && obj !== null) ||
1719+
isObject(obj) ||
16961720
/**
16971721
* We track all strings greater than 1 character, because those take at least 6 bytes to encode
16981722
* and even with 999 root objects it saves one byte per reference. Tracking more objects makes
@@ -1725,9 +1749,7 @@ function isResource<T = unknown>(value: object): value is ResourceReturnInternal
17251749

17261750
const frameworkType = (obj: any) => {
17271751
return (
1728-
(typeof obj === 'object' &&
1729-
obj !== null &&
1730-
(obj instanceof SignalImpl || obj instanceof Task || isJSXNode(obj))) ||
1752+
(isObject(obj) && (obj instanceof SignalImpl || obj instanceof Task || isJSXNode(obj))) ||
17311753
isQrl(obj)
17321754
);
17331755
};
@@ -1913,8 +1935,8 @@ const circularProofJson = (obj: unknown, indent?: string | number) => {
19131935
const seen = new WeakSet();
19141936
return JSON.stringify(
19151937
obj,
1916-
(key, value) => {
1917-
if (typeof value === 'object' && value !== null) {
1938+
(_, value) => {
1939+
if (isObject(value)) {
19181940
if (seen.has(value)) {
19191941
return `[Circular ${value.constructor.name}]`;
19201942
}
@@ -1958,7 +1980,7 @@ export const dumpState = (
19581980
if (key === undefined) {
19591981
hasRaw = true;
19601982
out.push(
1961-
`${RED}[raw${typeof value === 'object' && value ? ` ${value.constructor.name}` : ''}]${RESET} ${printRaw(value, `${prefix} `)}`
1983+
`${RED}[raw${isObject(value) ? ` ${value.constructor.name}` : ''}]${RESET} ${printRaw(value, `${prefix} `)}`
19621984
);
19631985
} else {
19641986
if (key === TypeIds.Constant) {

packages/qwik/src/core/shared/shared-serialization.unit.ts

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { $, componentQrl, noSerialize } from '@qwik.dev/core';
22
import { describe, expect, it, vi } from 'vitest';
3-
import { _fnSignal, _wrapProp } from '../internal';
3+
import { _fnSignal, _UNINITIALIZED, _wrapProp } from '../internal';
44
import { type SignalImpl } from '../reactive-primitives/impl/signal-impl';
55
import {
66
createComputedQrl,
@@ -25,7 +25,7 @@ import { EMPTY_ARRAY, EMPTY_OBJ } from './utils/flyweight';
2525
import { isQrl } from './qrl/qrl-utils';
2626
import { NoSerializeSymbol, SerializerSymbol } from './utils/serialize-utils';
2727
import { SubscriptionData } from '../reactive-primitives/subscription-data';
28-
import { StoreFlags } from '../reactive-primitives/types';
28+
import { StoreFlags, type CustomSerializable } from '../reactive-primitives/types';
2929

3030
const DEBUG = false;
3131

@@ -77,6 +77,47 @@ describe('shared-serialization', () => {
7777
(81 chars)"
7878
`);
7979
});
80+
describe('UNINITIALIZED', () => {
81+
it(title(TypeIds.Constant) + ' - UNINITIALIZED, not serialized object', async () => {
82+
const uninitializedObject = {
83+
shouldNot: 'serialize',
84+
};
85+
(uninitializedObject as unknown as CustomSerializable<any, any>)[SerializerSymbol] = () => {
86+
return _UNINITIALIZED;
87+
};
88+
expect(await dump(uninitializedObject)).toMatchInlineSnapshot(`
89+
"
90+
0 ForwardRef 0
91+
1 Constant _UNINITIALIZED
92+
2 ForwardRefs [
93+
1
94+
]
95+
(15 chars)"
96+
`);
97+
});
98+
it(title(TypeIds.Constant) + ' - UNINITIALIZED, serialized object', async () => {
99+
const uninitializedObject = {
100+
should: 'serialize',
101+
};
102+
(uninitializedObject as unknown as CustomSerializable<any, any>)[SerializerSymbol] = () => {
103+
return _UNINITIALIZED;
104+
};
105+
const qrl = inlinedQrl(() => uninitializedObject.should, 'dump_qrl', [uninitializedObject]);
106+
expect(await dump(uninitializedObject, qrl)).toMatchInlineSnapshot(`
107+
"
108+
0 ForwardRef 0
109+
1 QRL "mock-chunk#dump_qrl[0]"
110+
2 Object [
111+
String "should"
112+
String "serialize"
113+
]
114+
3 ForwardRefs [
115+
2
116+
]
117+
(69 chars)"
118+
`);
119+
});
120+
});
80121
it(title(TypeIds.Number), async () => {
81122
expect(await dump(123)).toMatchInlineSnapshot(`
82123
"
@@ -770,6 +811,39 @@ describe('shared-serialization', () => {
770811
expect(effect).toBeInstanceOf(SubscriptionData);
771812
expect(effect.data).toEqual({ $isConst$: true, $scopedStyleIdPrefix$: null });
772813
});
814+
815+
describe('UNINITIALIZED', () => {
816+
it(title(TypeIds.Constant) + ' - UNINITIALIZED, not serialized object', async () => {
817+
const uninitializedObject = {
818+
shouldNot: 'serialize',
819+
};
820+
(uninitializedObject as unknown as CustomSerializable<any, any>)[SerializerSymbol] = () => {
821+
return _UNINITIALIZED;
822+
};
823+
824+
const objs = await serialize(uninitializedObject);
825+
const effect = deserialize(objs)[0] as any;
826+
expect(effect).toBe(_UNINITIALIZED);
827+
});
828+
it(title(TypeIds.Constant) + ' - UNINITIALIZED, serialized object', async () => {
829+
const uninitializedObject = {
830+
should: 'serialize',
831+
};
832+
(uninitializedObject as unknown as CustomSerializable<any, any>)[SerializerSymbol] = () => {
833+
return _UNINITIALIZED;
834+
};
835+
const qrl = inlinedQrl(() => uninitializedObject.should, 'dump_qrl', [uninitializedObject]);
836+
const objs = await serialize(uninitializedObject, qrl);
837+
const state = deserialize(objs);
838+
delete (uninitializedObject as any)[SerializerSymbol];
839+
const deserializedObject = state[0];
840+
expect(deserializedObject).toEqual(uninitializedObject);
841+
842+
const deserializedQrl = state[1] as QRLInternal;
843+
expect(isQrl(deserializedQrl)).toBeTruthy();
844+
expect(await (deserializedQrl.getFn() as any)()).toBe(uninitializedObject.should);
845+
});
846+
});
773847
});
774848

775849
describe('special cases', () => {

packages/qwik/src/core/shared/utils/serialize-utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ export const shouldSerialize = (obj: unknown): boolean => {
9999
};
100100

101101
export const fastSkipSerialize = (obj: object): boolean => {
102-
return typeof obj === 'object' && obj && (NoSerializeSymbol in obj || noSerializeSet.has(obj));
102+
return obj && isObject(obj) && (NoSerializeSymbol in obj || noSerializeSet.has(obj));
103103
};
104104

105105
/**

packages/qwik/src/core/shared/utils/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export const isSerializableObject = (v: unknown): v is Record<string, unknown> =
99
};
1010

1111
export const isObject = (v: unknown): v is object => {
12-
return !!v && typeof v === 'object';
12+
return typeof v === 'object' && v !== null;
1313
};
1414

1515
export const isArray = (v: unknown): v is unknown[] => {

packages/qwik/src/core/use/use-core.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { QRLInternal } from '../shared/qrl/qrl-class';
55
import type { QRL } from '../shared/qrl/qrl.public';
66
import { ComputedEvent, RenderEvent, ResourceEvent, TaskEvent } from '../shared/utils/markers';
77
import { seal } from '../shared/utils/qdev';
8-
import { isArray } from '../shared/utils/types';
8+
import { isArray, isObject } from '../shared/utils/types';
99
import { setLocale } from './use-locale';
1010
import type { Container, HostElement } from '../shared/types';
1111
import { vnode_getNode, vnode_isElementVNode, vnode_isVNode, vnode_locate } from '../client/vnode';
@@ -159,7 +159,7 @@ export const newInvokeContext = (
159159
): InvokeContext => {
160160
// ServerRequestEvent has .locale, but it's not always defined.
161161
const $locale$ =
162-
locale || (typeof event === 'object' && event && 'locale' in event ? event.locale : undefined);
162+
locale || (event && isObject(event) && 'locale' in event ? event.locale : undefined);
163163
const ctx: InvokeContext = {
164164
$url$: url,
165165
$i$: 0,

0 commit comments

Comments
 (0)