Skip to content

Commit

Permalink
WIP: basic serialization/deserialization of Store2 working
Browse files Browse the repository at this point in the history
  • Loading branch information
mhevery committed Aug 2, 2024
1 parent 4f6045f commit 6a6b609
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 37 deletions.
67 changes: 45 additions & 22 deletions packages/qwik/src/core/v2/shared/shared-serialization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,11 @@ import { Slot } from '../../render/jsx/slot.public';
import {
fastSkipSerialize,
getProxyFlags,
getSubscriptionManager,
unwrapProxy,
getSubscriptionManager
} from '../../state/common';
import { _CONST_PROPS, _VAR_PROPS } from '../../state/constants';
import { getOrCreateProxy, isStore } from '../../state/store';
import { Task, isTask, type ResourceReturnInternal } from '../../use/use-task';
import { EMPTY_OBJ } from '../../util/flyweight';
import { throwErrorAndStop } from '../../util/log';
import { ELEMENT_ID } from '../../util/markers';
import { isPromise } from '../../util/promises';
Expand All @@ -43,7 +42,7 @@ import {
Signal2,
type EffectSubscriptions,
} from '../signal/v2-signal';
import { Store2, unwrapStore2 } from '../signal/v2-store';
import { Store2, createStore2, getStoreHandler2, unwrapStore2, type StoreHandler } from '../signal/v2-store';
import type { SymbolToChunkResolver } from '../ssr/ssr-types';
import type { fixMeAny } from './types';

Expand Down Expand Up @@ -114,14 +113,6 @@ class DeserializationHandler implements ProxyHandler<object> {
propValue === SerializationConstant.VNode_CHAR
? container.element.ownerDocument
: vnode_locate(container.rootVNode, propValue.substring(1));
} else if (typeCode === SerializationConstant.Store_VALUE) {
// Special case of Store.
// Stores are proxies, Proxies need to get their target eagerly. So we can't use inflate()
// because that would not allow us to get a hold of the target.
const target = container.$getObjectById$(propValue.substring(1)) as {
[SerializationConstant.Store_CHAR]: string | undefined;
};
propValue = getOrCreateProxy(target, container);
} else if (typeCode === SerializationConstant.DerivedSignal_VALUE && !Array.isArray(target)) {
// Special case of derived signal. We need to create a [_CONST_PROPS] property.
return wrapDeserializerProxy(
Expand Down Expand Up @@ -274,6 +265,21 @@ const inflate = (container: DomContainer, target: any, needsInflationData: strin
inflateQRL(container, target[SERIALIZABLE_STATE][0]);
break;
case SerializationConstant.Store_VALUE:
const storeHandler = getStoreHandler2(target)!;
storeHandler.$container$ = container;
storeHandler.$target$ = container.$getObjectById$(restInt());
storeHandler.$flags$ = restInt();
const effectProps = rest.substring(restIdx).split('|');
if (effectProps.length) {
const effects: Record<string, EffectSubscriptions[]> = (storeHandler.$effects$ = {});
for (let i = 0; i < effectProps.length; i++) {
const effect = effectProps[i];
const idx = effect.indexOf(';');
const prop = effect.substring(0, idx);
const effectStr = effect.substring(idx + 1);
deserializeSignal2Effect(0, effectStr.split(';'), container, effects[prop] = [])
}
}
break;
case SerializationConstant.Signal_VALUE:
deserializeSignal2(target as Signal2<unknown>, container, rest, false, false);
Expand Down Expand Up @@ -413,6 +419,8 @@ const allocate = <T>(value: string): any => {
return new Uint8Array(decodedLength);
case SerializationConstant.PropsProxy_VALUE:
return createPropsProxy(null!, null);
case SerializationConstant.Store_VALUE:
return createStore2(null, EMPTY_OBJ, 0);
default:
throw new Error('unknown allocate type: ' + value.charCodeAt(0));
}
Expand Down Expand Up @@ -633,9 +641,6 @@ export const createSerializationContext = (
// As we walk the object graph we insert newly discovered objects which need to be scanned here.
const discoveredValues: unknown[] = [rootObj];
// discoveredValues.push = (...value: unknown[]) => {
// if (isSignal(value[0])) {
// debugger;
// }
// Array.prototype.push.apply(discoveredValues, value);
// };
// let count = 100;
Expand All @@ -648,7 +653,7 @@ export const createSerializationContext = (
const isRoot = obj === rootObj;
// For root objects we pretend we have not seen them to force scan.
const id = $wasSeen$(obj);
const unwrapObj = unwrapProxy(obj);
const unwrapObj = unwrapStore2(obj);
if (unwrapObj !== obj) {
discoveredValues.push(unwrapObj);
} else if (id === undefined || isRoot) {
Expand Down Expand Up @@ -803,12 +808,13 @@ function serialize(serializationContext: SerializationContext): void {
}
};

const writeObjectValue = (value: unknown, idx: number) => {
const writeObjectValue = (value: {}, idx: number) => {
// Objects are the only way to create circular dependencies.
// So the first thing to to is to see if we have a circular dependency.
// (NOTE: For root objects we need to serialize them regardless if we have seen
// them before, otherwise the root object reference will point to itself.)
const seen = depth <= 1 ? undefined : serializationContext.$wasSeen$(value);
let storeHandler: null | StoreHandler<any> = null;
if (fastSkipSerialize(value as object)) {
writeString(SerializationConstant.UNDEFINED_CHAR);
} else if (typeof seen === 'number' && seen >= 0) {
Expand All @@ -820,9 +826,18 @@ function serialize(serializationContext: SerializationContext): void {
const varId = $addRoot$(varProps);
const constProps = value[_CONST_PROPS];
const constId = $addRoot$(constProps);
writeString(SerializationConstant.PropsProxy_CHAR + varId + ' ' + constId);
} else if (isStore(value)) {
writeString(SerializationConstant.Store_CHAR + $addRoot$(unwrapProxy(value)));
writeString(SerializationConstant.PropsProxy_CHAR + varId + '|' + constId);
} else if ((storeHandler = getStoreHandler2(value))) {
let store = SerializationConstant.Store_CHAR + $addRoot$(storeHandler.$target$) + ' ' + storeHandler.$flags$;
const effects = storeHandler.$effects$;
if (effects) {
let sep = ' ';
for (const propName in effects) {
store += sep + propName + serializeEffectSubs($addRoot$, effects[propName])
sep = '|';
}
}
writeString(store);
} else if (isObjectLiteral(value)) {
if (isResource(value)) {
serializationContext.$resources$.add(value);
Expand Down Expand Up @@ -931,7 +946,7 @@ function serialize(serializationContext: SerializationContext): void {
const out = btoa(buf).replace(/=+$/, '');
writeString(SerializationConstant.Uint8Array_CHAR + out);
} else {
throw new Error('implement: ' + JSON.stringify(value));
throw new Error('implement');
}
};

Expand Down Expand Up @@ -1088,13 +1103,21 @@ function deserializeSignal2(
computedSignal.$computeQrl$ = parseQRL(parts[idx++]) as fixMeAny;
}
signal.$untrackedValue$ = container.$getObjectById$(parts[idx++]);
if (idx < parts.length) {
const effects = signal.$effects$ || (signal.$effects$ = []);
idx = deserializeSignal2Effect(idx, parts, container, effects);
}
}

function deserializeSignal2Effect(idx: number, parts: string[], container: DomContainer, effects: EffectSubscriptions[]) {
while (idx < parts.length) {
// idx == 1 is the attribute name
const effect = parts[idx++]
.split(' ')
.map((obj, idx) => (idx == 1 ? obj : container.$getObjectById$(obj)));
(signal.$effects$ || (signal.$effects$ = [])).push(effect as fixMeAny);
effects.push(effect as fixMeAny);
}
return idx;
}

function setSerializableDataRootId($addRoot$: (value: any) => number, obj: object, value: any) {
Expand Down
33 changes: 21 additions & 12 deletions packages/qwik/src/core/v2/signal/v2-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,20 @@ export type Store2<T> = T & {
__BRAND__: 'Store';
};

let _lastTarget: undefined | StoreHandler<object>;
let _lastHandler: undefined | StoreHandler<any>;

export const getStoreTarget2 = <T extends object>(value: T): T | null => {
_lastTarget = undefined as any;
return typeof value === 'object' && value && STORE in value // this implicitly sets the `_lastTarget` as a side effect.
? (_lastTarget!.$target$ as T)
export const getStoreHandler2 = <T extends object>(value: T): StoreHandler<T> | null => {
_lastHandler = undefined as any;
return typeof value === 'object' && value && STORE in value // this implicitly sets the `_lastHandler` as a side effect.
? (_lastHandler!)
: null;
};

export const getStoreTarget2 = <T extends object>(value: T): T | null => {
const handler = getStoreHandler2(value);
return handler ? handler.$target$ : null;
};

export const unwrapStore2 = <T>(value: T): T => {
return (getStoreTarget2(value as fixMeAny) as T) || value;
};
Expand All @@ -48,6 +53,13 @@ export const isStore2 = <T extends object>(value: T): value is Store2<T> => {
return value instanceof Store;
};

export function createStore2<T extends object>(container: Container2 | null | undefined, obj: T & Record<string, unknown>, flags: Store2Flags) {
return new Proxy(
new Store(),
new StoreHandler<T>(obj, flags, container || null)
) as Store2<T>;
}

export const getOrCreateStore2 = <T extends object>(
obj: T,
flags: Store2Flags,
Expand All @@ -56,10 +68,7 @@ export const getOrCreateStore2 = <T extends object>(
if (isSerializableObject(obj)) {
let store: Store2<T> | undefined = storeWeakMap.get(obj) as Store2<T> | undefined;
if (!store) {
store = new Proxy(
new Store(),
new StoreHandler<T>(obj, flags, container || null)
) as Store2<T>;
store = createStore2<T>(container, obj, flags);
storeWeakMap.set(obj, store as any);
}
return store as Store2<T>;
Expand All @@ -75,7 +84,7 @@ class Store {

export const Store2 = Store;

class StoreHandler<T extends Record<string | symbol, any>> implements ProxyHandler<T> {
export class StoreHandler<T extends Record<string | symbol, any>> implements ProxyHandler<T> {
$effects$: null | Record<string, EffectSubscriptions[]> = null;
constructor(
public $target$: T,
Expand Down Expand Up @@ -138,7 +147,7 @@ class StoreHandler<T extends Record<string | symbol, any>> implements ProxyHandl
// But when effect is scheduled in needs to be able to know which signals
// to unsubscribe from. So we need to store the reference from the effect back
// to this signal.
ensureContains(effectSubscriber, this);
ensureContains(effectSubscriber, this.$target$);
DEBUG && log('read->sub', pad('\n' + this.toString(), ' '));
}
}
Expand Down Expand Up @@ -173,7 +182,7 @@ class StoreHandler<T extends Record<string | symbol, any>> implements ProxyHandl

has(_: T, p: string | symbol) {
if (p === STORE) {
_lastTarget = this;
_lastHandler = this;
return true;
}
return Object.prototype.hasOwnProperty.call(this.$target$, p);
Expand Down
6 changes: 3 additions & 3 deletions packages/qwik/src/core/v2/tests/use-store.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ const debug = true; //true;
Error.stackTraceLimit = 100;

describe.each([
// { render: ssrRenderToDom }, //
{ render: domRender }, //
{ render: ssrRenderToDom }, //
// { render: domRender }, //
])('$render.name: useStore', ({ render }) => {
it('should render value', async () => {
const Cmp = component$(() => {
Expand Down Expand Up @@ -60,7 +60,7 @@ describe.each([
</Component>
);
});
it('should update deep value', async () => {
it.only('should update deep value', async () => {
const Counter = component$(() => {
const count = useStore({ obj: { count: 123 } });
return <button onClick$={() => count.obj.count++}>Count: {count.obj.count}!</button>;
Expand Down

0 comments on commit 6a6b609

Please sign in to comment.