Skip to content

Commit 3c45a3c

Browse files
committed
feat: implement WeakObject serialization
1 parent d9ca5ba commit 3c45a3c

File tree

9 files changed

+55
-60
lines changed

9 files changed

+55
-60
lines changed

Diff for: packages/docs/src/routes/api/qwik/api.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -2144,7 +2144,7 @@
21442144
}
21452145
],
21462146
"kind": "Function",
2147-
"content": "Assign a value to a Context.\n\nUse `useContextProvider()` to assign a value to a context. The assignment happens in the component's function. Once assigned, use `useContext()` in any child component to retrieve the value.\n\nContext is a way to pass stores to the child components without prop-drilling. Note that scalar values are allowed, but for reactivity you need signals or stores.\n\n\\#\\#\\# Example\n\n```tsx\n// Declare the Context type.\ninterface TodosStore {\n items: string[];\n}\n// Create a Context ID (no data is saved here.)\n// You will use this ID to both create and retrieve the Context.\nexport const TodosContext = createContextId<TodosStore>('Todos');\n\n// Example of providing context to child components.\nexport const App = component$(() => {\n useContextProvider(\n TodosContext,\n useStore<TodosStore>({\n items: ['Learn Qwik', 'Build Qwik app', 'Profit'],\n })\n );\n\n return <Items />;\n});\n\n// Example of retrieving the context provided by a parent component.\nexport const Items = component$(() => {\n const todos = useContext(TodosContext);\n return (\n <ul>\n {todos.items.map((item) => (\n <li>{item}</li>\n ))}\n </ul>\n );\n});\n\n```\n\n\n```typescript\nuseContextProvider: <STATE>(context: ContextId<STATE>, newValue: STATE) => void\n```\n\n\n<table><thead><tr><th>\n\nParameter\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\ncontext\n\n\n</td><td>\n\n[ContextId](#contextid)<!-- -->&lt;STATE&gt;\n\n\n</td><td>\n\nThe context to assign a value to.\n\n\n</td></tr>\n<tr><td>\n\nnewValue\n\n\n</td><td>\n\nSTATE\n\n\n</td><td>\n\nThe value to assign to the context.\n\n\n</td></tr>\n</tbody></table>\n**Returns:**\n\nvoid",
2147+
"content": "Assign a value to a Context.\n\nUse `useContextProvider()` to assign a value to a context. The assignment happens in the component's function. Once assigned, use `useContext()` in any child component to retrieve the value.\n\nContext is a way to pass stores to the child components without prop-drilling. Note that scalar values are allowed, but for reactivity you need signals or stores.\n\n\\#\\#\\# Example\n\n```tsx\n// Declare the Context type.\ninterface TodosStore {\n items: string[];\n}\n// Create a Context ID (no data is saved here.)\n// You will use this ID to both create and retrieve the Context.\nexport const TodosContext = createContextId<TodosStore>('Todos');\n\n// Example of providing context to child components.\nexport const App = component$(() => {\n useContextProvider(\n TodosContext,\n useStore<TodosStore>({\n items: ['Learn Qwik', 'Build Qwik app', 'Profit'],\n })\n );\n\n return <Items />;\n});\n\n// Example of retrieving the context provided by a parent component.\nexport const Items = component$(() => {\n const todos = useContext(TodosContext);\n return (\n <ul>\n {todos.items.map((item) => (\n <li>{item}</li>\n ))}\n </ul>\n );\n});\n\n```\n\n\n```typescript\nuseContextProvider: <STATE>(context: ContextId<STATE>, newValue: STATE) => void\n```\n\n\n<table><thead><tr><th>\n\nParameter\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\ncontext\n\n\n</td><td>\n\n[ContextId](#contextid)<!-- -->&lt;STATE&gt;\n\n\n</td><td>\n\nThe context to assign a value to.\n\n\n</td></tr>\n<tr><td>\n\nnewValue\n\n\n</td><td>\n\nSTATE\n\n\n</td><td>\n\n\n</td></tr>\n</tbody></table>\n**Returns:**\n\nvoid",
21482148
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-context.ts",
21492149
"mdFile": "core.usecontextprovider.md"
21502150
},

Diff for: packages/docs/src/routes/api/qwik/index.md

-2
Original file line numberDiff line numberDiff line change
@@ -8631,8 +8631,6 @@ STATE
86318631
86328632
</td><td>
86338633
8634-
The value to assign to the context.
8635-
86368634
</td></tr>
86378635
</tbody></table>
86388636
**Returns:**

Diff for: packages/qwik-router/src/runtime/src/qwik-router-component.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ export const QwikRouterProvider = component$<QwikRouterProps>((props) => {
146146
{ deep: false }
147147
);
148148
const navResolver: { r?: () => void } = {};
149-
const loaderState = _weakSerialize(useStore(env.response.loaders, { deep: false }));
149+
const loaderState = useStore(_weakSerialize(env.response.loaders), { deep: false });
150150
const routeInternal = useSignal<RouteStateInternal>({
151151
type: 'initial',
152152
dest: url,

Diff for: packages/qwik-router/src/runtime/src/server-functions.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,10 @@ export const routeLoaderQrl = ((
210210
}
211211
const data = untrack(() => state[id]);
212212
if (!data && isBrowser) {
213-
throw loadClientData(location.url, iCtx.$hostElement$);
213+
// TODO: fetch only loader with current id
214+
throw loadClientData(location.url, iCtx.$hostElement$).then(
215+
(data) => (state[id] = data?.loaders[id])
216+
);
214217
}
215218
return _wrapStore(state, id);
216219
}

Diff for: packages/qwik/src/core/api.md

+5-39
Original file line numberDiff line numberDiff line change
@@ -244,9 +244,11 @@ export class _EffectData {
244244
data: NodePropData;
245245
}
246246

247+
// Warning: (ae-forgotten-export) The symbol "VNodeFlags" needs to be exported by the entry point index.d.ts
248+
//
247249
// @internal (undocumented)
248250
export type _ElementVNode = [
249-
_VNodeFlags.Element,
251+
VNodeFlags.Element,
250252
////////////// 0 - Flags
251253
_VNode | null,
252254
/////////////// 1 - Parent
@@ -872,8 +874,6 @@ export abstract class _SharedContainer implements Container {
872874
abstract handleError(err: any, $host$: HostElement): void;
873875
// (undocumented)
874876
abstract resolveContext<T>(host: HostElement, contextId: ContextId<T>): T | undefined;
875-
// (undocumented)
876-
resolveContextForHost<T>(host: HostElement, contextId: ContextId<T>): T | undefined;
877877
// Warning: (ae-forgotten-export) The symbol "SymbolToChunkResolver" needs to be exported by the entry point index.d.ts
878878
// Warning: (ae-forgotten-export) The symbol "SerializationContext" needs to be exported by the entry point index.d.ts
879879
//
@@ -1568,7 +1568,7 @@ export type TaskFn = (ctx: TaskCtx) => ValueOrPromise<void | (() => void)>;
15681568

15691569
// @internal (undocumented)
15701570
export type _TextVNode = [
1571-
_VNodeFlags.Text | _VNodeFlags.Inflated,
1571+
VNodeFlags.Text | VNodeFlags.Inflated,
15721572
// 0 - Flags
15731573
_VNode | null,
15741574
///////////////// 1 - Parent
@@ -1737,7 +1737,7 @@ export const version: string;
17371737

17381738
// @internal (undocumented)
17391739
export type _VirtualVNode = [
1740-
_VNodeFlags.Virtual,
1740+
VNodeFlags.Virtual,
17411741
///////////// 0 - Flags
17421742
_VNode | null,
17431743
/////////////// 1 - Parent
@@ -1759,40 +1759,6 @@ export type VisibleTaskStrategy = 'intersection-observer' | 'document-ready' | '
17591759
// @internal (undocumented)
17601760
export type _VNode = _ElementVNode | _TextVNode | _VirtualVNode;
17611761

1762-
// @internal
1763-
export const enum _VNodeFlags {
1764-
// (undocumented)
1765-
Deleted = 32,
1766-
// (undocumented)
1767-
Element = 1,
1768-
// (undocumented)
1769-
ELEMENT_OR_TEXT_MASK = 5,
1770-
// (undocumented)
1771-
ELEMENT_OR_VIRTUAL_MASK = 3,
1772-
// (undocumented)
1773-
Inflated = 8,
1774-
// (undocumented)
1775-
INFLATED_TYPE_MASK = 15,
1776-
// (undocumented)
1777-
NAMESPACE_MASK = 192,
1778-
// (undocumented)
1779-
NEGATED_NAMESPACE_MASK = -193,
1780-
// (undocumented)
1781-
NS_html = 0,
1782-
// (undocumented)
1783-
NS_math = 128,
1784-
// (undocumented)
1785-
NS_svg = 64,
1786-
// (undocumented)
1787-
Resolved = 16,
1788-
// (undocumented)
1789-
Text = 4,// http://www.w3.org/1999/xhtml
1790-
// (undocumented)
1791-
TYPE_MASK = 7,// http://www.w3.org/2000/svg
1792-
// (undocumented)
1793-
Virtual = 2
1794-
}
1795-
17961762
// @internal (undocumented)
17971763
export const _waitUntilRendered: (elm: Element) => Promise<void>;
17981764

Diff for: packages/qwik/src/core/shared/shared-serialization.ts

+30-10
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ import { isElement, isNode } from './utils/element';
5656
import { EMPTY_ARRAY, EMPTY_OBJ } from './utils/flyweight';
5757
import { ELEMENT_ID } from './utils/markers';
5858
import { isPromise } from './utils/promises';
59-
import { SerializerSymbol, fastSkipSerialize } from './utils/serialize-utils';
59+
import { SerializerSymbol, fastSkipSerialize, fastWeakSerialize } from './utils/serialize-utils';
6060
import { type ValueOrPromise } from './utils/types';
6161

6262
const deserializedProxyMap = new WeakMap<object, unknown[]>();
@@ -394,12 +394,22 @@ const inflate = (
394394
propsProxy[_VAR_PROPS] = data === 0 ? {} : (data as any)[0];
395395
propsProxy[_CONST_PROPS] = (data as any)[1];
396396
break;
397-
case TypeIds.EffectData: {
397+
case TypeIds.SubscriptionData: {
398398
const effectData = target as SubscriptionData;
399399
effectData.data.$scopedStyleIdPrefix$ = (data as any[])[0];
400400
effectData.data.$isConst$ = (data as any[])[1];
401401
break;
402402
}
403+
case TypeIds.WeakObject: {
404+
const objectKeys = data as string[];
405+
target = Object.fromEntries(
406+
objectKeys.map((v) =>
407+
// initialize values with null
408+
[v, null]
409+
)
410+
);
411+
break;
412+
}
403413
default:
404414
throw qError(QError.serializeErrorNotImplemented, [typeId]);
405415
}
@@ -460,6 +470,7 @@ const allocate = (container: DeserializeContainer, typeId: number, value: unknow
460470
case TypeIds.Array:
461471
return wrapDeserializerProxy(container as any, value as any[]);
462472
case TypeIds.Object:
473+
case TypeIds.WeakObject:
463474
return {};
464475
case TypeIds.QRL:
465476
const qrl = container.$getObjectById$(value as number);
@@ -541,9 +552,8 @@ const allocate = (container: DeserializeContainer, typeId: number, value: unknow
541552
} else {
542553
throw qError(QError.serializeErrorExpectedVNode, [typeof vNode]);
543554
}
544-
case TypeIds.EffectData:
555+
case TypeIds.SubscriptionData:
545556
return new SubscriptionData({} as NodePropData);
546-
547557
default:
548558
throw qError(QError.serializeErrorCannotAllocate, [typeId]);
549559
}
@@ -811,18 +821,24 @@ export const createSerializationContext = (
811821
obj instanceof RegExp ||
812822
obj instanceof Uint8Array ||
813823
obj instanceof URLSearchParams ||
824+
obj instanceof SubscriptionData ||
814825
vnode_isVNode(obj) ||
815826
(typeof FormData !== 'undefined' && obj instanceof FormData) ||
816827
// Ignore the no serialize objects
817-
fastSkipSerialize(obj as object)
828+
fastSkipSerialize(obj as object) ||
829+
// only keys will be serialized
830+
fastWeakSerialize(obj)
818831
) {
819832
// ignore
820833
} else if (obj instanceof Error) {
821834
discoveredValues.push(...Object.values(obj));
822835
} else if (isStore(obj)) {
823836
const target = getStoreTarget(obj)!;
824837
const effects = getStoreHandler(obj)!.$effects$;
825-
discoveredValues.push(target, effects);
838+
if (!fastWeakSerialize(target)) {
839+
discoveredValues.push(target);
840+
}
841+
discoveredValues.push(effects);
826842

827843
for (const prop in target) {
828844
const propValue = (target as any)[prop];
@@ -972,7 +988,7 @@ const discoverValuesForVNodeData = (vnodeData: VNodeData, discoveredValues: unkn
972988
if (isSsrAttrs(value)) {
973989
for (let i = 1; i < value.length; i += 2) {
974990
const attrValue = value[i];
975-
if (typeof attrValue === 'string') {
991+
if (attrValue == null || typeof attrValue === 'string') {
976992
continue;
977993
}
978994
discoveredValues.push(attrValue);
@@ -1155,7 +1171,7 @@ function serialize(serializationContext: SerializationContext): void {
11551171
: 0;
11561172
output(TypeIds.PropsProxy, out);
11571173
} else if (value instanceof SubscriptionData) {
1158-
output(TypeIds.EffectData, [value.data.$scopedStyleIdPrefix$, value.data.$isConst$]);
1174+
output(TypeIds.SubscriptionData, [value.data.$scopedStyleIdPrefix$, value.data.$isConst$]);
11591175
} else if (isStore(value)) {
11601176
if (isResource(value)) {
11611177
// let render know about the resource
@@ -1205,6 +1221,8 @@ function serialize(serializationContext: SerializationContext): void {
12051221
} else if (isObjectLiteral(value)) {
12061222
if (Array.isArray(value)) {
12071223
output(TypeIds.Array, value);
1224+
} else if (fastWeakSerialize(value)) {
1225+
output(TypeIds.WeakObject, Object.keys(value));
12081226
} else {
12091227
const out: any[] = [];
12101228
for (const key in value) {
@@ -1721,7 +1739,8 @@ export const enum TypeIds {
17211739
FormData,
17221740
JSXNode,
17231741
PropsProxy,
1724-
EffectData,
1742+
SubscriptionData,
1743+
WeakObject,
17251744
}
17261745
export const _typeIdNames = [
17271746
'RootRef',
@@ -1755,7 +1774,8 @@ export const _typeIdNames = [
17551774
'FormData',
17561775
'JSXNode',
17571776
'PropsProxy',
1758-
'EffectData',
1777+
'SubscriptionData',
1778+
'WeakObject',
17591779
];
17601780

17611781
export const enum Constants {

Diff for: packages/qwik/src/core/shared/shared-serialization.unit.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { $, component$, noSerialize } from '@qwik.dev/core';
1+
import { $, _weakSerialize, component$, noSerialize } from '@qwik.dev/core';
22
import { describe, expect, it, vi } from 'vitest';
33
import { _fnSignal, _wrapProp } from '../internal';
44
import { SubscriptionData, type SignalImpl } from '../signal/signal';
@@ -480,7 +480,7 @@ describe('shared-serialization', () => {
480480
it.todo(title(TypeIds.FormData));
481481
it.todo(title(TypeIds.JSXNode));
482482
it.todo(title(TypeIds.PropsProxy));
483-
it(title(TypeIds.EffectData), async () => {
483+
it(title(TypeIds.SubscriptionData), async () => {
484484
expect(await dump(new SubscriptionData({ $isConst$: true, $scopedStyleIdPrefix$: null })))
485485
.toMatchInlineSnapshot(`
486486
"
@@ -666,7 +666,7 @@ describe('shared-serialization', () => {
666666
it.todo(title(TypeIds.ComputedSignal));
667667
it.todo(title(TypeIds.SerializerSignal));
668668
// this requires a domcontainer
669-
it.skip(title(TypeIds.Store), async () => {
669+
it(title(TypeIds.Store), async () => {
670670
const objs = await serialize(createStore(null, { a: { b: true } }, StoreFlags.RECURSIVE));
671671
const store = deserialize(objs)[0] as any;
672672
expect(store).toHaveProperty('a');
@@ -676,7 +676,7 @@ describe('shared-serialization', () => {
676676
it.todo(title(TypeIds.FormData));
677677
it.todo(title(TypeIds.JSXNode));
678678
it.todo(title(TypeIds.PropsProxy));
679-
it(title(TypeIds.EffectData), async () => {
679+
it(title(TypeIds.SubscriptionData), async () => {
680680
const objs = await serialize(
681681
new SubscriptionData({ $isConst$: true, $scopedStyleIdPrefix$: null })
682682
);

Diff for: packages/qwik/src/core/shared/utils/serialize-utils.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,15 @@ export const noSerialize = <T extends object | undefined>(input: T): NoSerialize
145145
/** @internal */
146146
export const _weakSerialize = <T extends object>(input: T): Partial<T> => {
147147
weakSerializeSet.add(input);
148-
return input as any;
148+
if (isObject(input)) {
149+
for (const key in input) {
150+
const value = input[key];
151+
if (isObject(value)) {
152+
noSerializeSet.add(value);
153+
}
154+
}
155+
}
156+
return input;
149157
};
150158

151159
/**

Diff for: starters/apps/qwikrouter-test/src/routes/loaders-serialization/index.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { component$, useSignal } from "@qwik.dev/core";
22
import { routeLoader$ } from "@qwik.dev/router";
33

44
export const useTestLoader = routeLoader$(async () => {
5-
return { test: "should not serialize this" };
5+
return { test: "some test value", notUsed: "should not serialize this" };
66
});
77

88
export default component$(() => {

0 commit comments

Comments
 (0)