Skip to content

Commit

Permalink
wip: use-store.spec passes in CSR mode
Browse files Browse the repository at this point in the history
  • Loading branch information
mhevery committed Aug 2, 2024
1 parent 289ab11 commit 4f6045f
Show file tree
Hide file tree
Showing 7 changed files with 131 additions and 45 deletions.
39 changes: 39 additions & 0 deletions packages/qwik/src/core/debug.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { isQrl } from "../server/prefetch-strategy";
import { isJSXNode } from "./render/jsx/jsx-runtime";
import { isTask } from "./use/use-task";
import { vnode_isVNode, vnode_toString } from "./v2/client/vnode";
import { isSignal2 } from "./v2/signal/v2-signal";
import { isStore2 } from "./v2/signal/v2-store";

const stringifyPath: any[] = [];
export function qwikDebugToString(value: any): any {
Expand All @@ -27,6 +30,10 @@ export function qwikDebugToString(value: any): any {
} else {
return value.map(qwikDebugToString);
}
} else if (isStore2(value) || isSignal2(value)) {
return value.toString();
} else if (isJSXNode(value)) {
return jsxToString(value);
} else if (isTask(value)) {
return `Task(${qwikDebugToString(value.$qrl$)})`
} else if (isQrl(value)) {
Expand All @@ -43,4 +50,36 @@ export function qwikDebugToString(value: any): any {

export const pad = (text: string, prefix: string) => {
return String(text).split('\n').map((line, idx) => (idx ? prefix : '') + line).join('\n');
}

export const jsxToString = (value: any): string => {
if (isJSXNode(value)) {
let type = value.type;
if (typeof type === 'function') {
type = type.name || 'Component';
}
let str = '<' + value.type;
if (value.props) {
for (const [key, val] of Object.entries(value.props)) {
str += ' ' + key + '=' + qwikDebugToString(val);
}
const children = value.children;
if (children != null) {
str += '>';
if (Array.isArray(children)) {
children.forEach((child) => {
str += jsxToString(child);
});
} else {
str += jsxToString(children);
}
str += '</' + value.type + '>';
} else {
str += '/>';
}
}
return str;
} else {
return String(value);
}
}
2 changes: 1 addition & 1 deletion packages/qwik/src/core/util/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export const isHtmlElement = (node: unknown): node is Element => {

export const isSerializableObject = (v: unknown): v is Record<string, unknown> => {
const proto = Object.getPrototypeOf(v);
return proto === Object.prototype || proto === null;
return proto === Object.prototype || proto === Array.prototype || proto === null;
};

export const isObject = (v: unknown): v is object => {
Expand Down
2 changes: 1 addition & 1 deletion packages/qwik/src/core/v2/shared/scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ import { EffectSubscriptionsProp, isSignal2, type EffectSubscriptions } from '..
import { serializeAttribute } from '../../render/execute-component';

// Turn this on to get debug output of what the scheduler is doing.
const DEBUG: boolean = false;
const DEBUG: boolean = true;

export const enum ChoreType {
/// MASKS defining three levels of sorting
Expand Down
4 changes: 2 additions & 2 deletions packages/qwik/src/core/v2/shared/shared-serialization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
} from '../../state/common';
import { _CONST_PROPS, _VAR_PROPS } from '../../state/constants';
import { getOrCreateProxy, isStore } from '../../state/store';
import { isTask, Task, type ResourceReturnInternal } from '../../use/use-task';
import { Task, isTask, type ResourceReturnInternal } from '../../use/use-task';
import { throwErrorAndStop } from '../../util/log';
import { ELEMENT_ID } from '../../util/markers';
import { isPromise } from '../../util/promises';
Expand All @@ -43,9 +43,9 @@ import {
Signal2,
type EffectSubscriptions,
} from '../signal/v2-signal';
import { Store2, unwrapStore2 } from '../signal/v2-store';
import type { SymbolToChunkResolver } from '../ssr/ssr-types';
import type { fixMeAny } from './types';
import { Store2, unwrapStore2 } from '../signal/v2-store';

const deserializedProxyMap = new WeakMap<object, unknown>();

Expand Down
3 changes: 3 additions & 0 deletions packages/qwik/src/core/v2/signal/v2-signal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ export class Signal2<T = any> implements ISignal2<T> {
throw new TypeError('Cannot coerce a Signal, use `.value` instead');
}
}

toString() {
return (
`[${this.constructor.name}${(this as any).$invalid$ ? ' INVALID' : ''} ${String(this.$untrackedValue$)}]` +
Expand All @@ -226,6 +227,8 @@ export const ensureContainsEffect = (array: EffectSubscriptions[], effect: Effec
return;
}
}
console.log('array', array);
console.log('array.push', array.push);
array.push(effect);
};

Expand Down
111 changes: 78 additions & 33 deletions packages/qwik/src/core/v2/signal/v2-store.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import { pad, qwikDebugToString } from "../../debug";
import { assertDefined, assertTrue } from "../../error/assert";
import { tryGetInvokeContext } from "../../use/use-core";
import type { VNode } from "../client/types";
import type { Container2, fixMeAny } from "../shared/types";
import { EffectProperty, ensureContains, ensureContainsEffect, triggerEffects, type EffectSubscriptions } from "./v2-signal";

const DEBUG = false;
import { pad, qwikDebugToString } from '../../debug';
import { assertDefined, assertTrue } from '../../error/assert';
import { tryGetInvokeContext } from '../../use/use-core';
import { isObject, isSerializableObject } from '../../util/types';
import type { VNode } from '../client/types';
import type { Container2, fixMeAny } from '../shared/types';
import {
EffectProperty,
ensureContains,
ensureContainsEffect,
triggerEffects,
type EffectSubscriptions,
} from './v2-signal';

const DEBUG = true;

// eslint-disable-next-line no-console
const log = (...args: any[]) => console.log('STORE', ...(args).map(qwikDebugToString));

const log = (...args: any[]) => console.log('STORE', ...args.map(qwikDebugToString));

const storeWeakMap = new WeakMap<object, Store2<object>>();

Expand All @@ -22,33 +28,44 @@ export const enum Store2Flags {
}

export type Store2<T> = T & {
__BRAND__: 'Store'
__BRAND__: 'Store';
};

let _lastTarget: undefined | StoreHandler<object>;

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 : null;
}
return typeof value === 'object' && value && STORE in value // this implicitly sets the `_lastTarget` as a side effect.
? (_lastTarget!.$target$ as T)
: null;
};

export const unwrapStore2 = <T>(value: T): T => {
return getStoreTarget2(value as fixMeAny) as T || value;
}
return (getStoreTarget2(value as fixMeAny) as T) || value;
};

export const isStore2 = <T extends object>(value: T): value is Store2<T> => {
return value instanceof Store;
}
};

export const getOrCreateStore2 = <T extends object>(obj: T, flags: Store2Flags, container?: Container2 | null): Store2<T> => {
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>;
storeWeakMap.set(obj, store as any);
export const getOrCreateStore2 = <T extends object>(
obj: T,
flags: Store2Flags,
container?: Container2 | null
): Store2<T> => {
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>;
storeWeakMap.set(obj, store as any);
}
return store as Store2<T>;
}
return store as Store2<T>;
}
return obj as any;
};

class Store {
toString() {
Expand All @@ -59,8 +76,33 @@ class Store {
export const Store2 = Store;

class StoreHandler<T extends Record<string | symbol, any>> implements ProxyHandler<T> {
$effects$: null | EffectSubscriptions[] = null;
constructor(public $target$: T, public $flags$: Store2Flags, public $container$: Container2 | null) {
$effects$: null | Record<string, EffectSubscriptions[]> = null;
constructor(
public $target$: T,
public $flags$: Store2Flags,
public $container$: Container2 | null
) { }

toString() {
const flags = [];
if (this.$flags$ & Store2Flags.RECURSIVE) {
flags.push('RECURSIVE');
}
if (this.$flags$ & Store2Flags.IMMUTABLE) {
flags.push('IMMUTABLE');
}
let str = '[Store: ' + flags.join('|') + '\n';
for (const key in this.$target$) {
const value = this.$target$[key];
str += ' ' + key + ': ' + qwikDebugToString(value) + ',\n';
const effects = this.$effects$?.[key];
effects?.forEach(([effect, prop, ...subs]) => {
str += ' ' + qwikDebugToString(effect) + '\n';
str += ' ' + qwikDebugToString(prop) + '\n';
// str += ' ' + subs.map(qwikDebugToString).join(';') + '\n';
});
}
return str + ']';
}

get(_: T, p: string | symbol) {
Expand All @@ -85,7 +127,10 @@ class StoreHandler<T extends Record<string | symbol, any>> implements ProxyHandl
}
}
if (effectSubscriber) {
const effects = (this.$effects$ ||= []);
const effectsMap = (this.$effects$ ||= {});
const effects =
(Object.prototype.hasOwnProperty.call(effectsMap, p) && effectsMap[p as fixMeAny]) ||
(effectsMap[p as fixMeAny] = []);
// Let's make sure that we have a reference to this effect.
// Adding reference is essentially adding a subscription, so if the signal
// changes we know who to notify.
Expand All @@ -94,27 +139,27 @@ class StoreHandler<T extends Record<string | symbol, any>> implements ProxyHandl
// to unsubscribe from. So we need to store the reference from the effect back
// to this signal.
ensureContains(effectSubscriber, this);
DEBUG && log("read->sub", pad('\n' + this.toString(), " "))
DEBUG && log('read->sub', pad('\n' + this.toString(), ' '));
}
}
let value = target[p];
if (p === 'toString' && value === Object.prototype.toString) {
return Store.prototype.toString;
}
if (typeof value === 'object' && value !== null) {
const flags = this.$flags$;
if (flags & Store2Flags.RECURSIVE && typeof value === 'object' && value !== null) {
value = getOrCreateStore2(value, this.$flags$, this.$container$);
}
return value;
}


set(_: T, p: string | symbol, value: any): boolean {
const target = this.$target$;
const oldValue = target[p];
if (value !== oldValue) {
DEBUG && log('Signal.set', oldValue, '->', value, pad('\n' + this.toString(), " "));
DEBUG && log('Signal.set', oldValue, '->', value, pad('\n' + this.toString(), ' '));
(target as any)[p] = value;
triggerEffects(this.$container$, this, this.$effects$);
triggerEffects(this.$container$, this, this.$effects$?.[String(p)]);
}
return true;
}
Expand All @@ -137,4 +182,4 @@ class StoreHandler<T extends Record<string | symbol, any>> implements ProxyHandl
ownKeys(): ArrayLike<string | symbol> {
return Reflect.ownKeys(this.$target$);
}
}
}
15 changes: 7 additions & 8 deletions packages/qwik/src/core/v2/tests/use-store.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Fragment as Component, Fragment, Fragment as Signal } from '@builder.io/qwik';
import { Fragment as Component, Fragment, Fragment as Signal, useTask$ } from '@builder.io/qwik';
import { describe, expect, it, vi } from 'vitest';
import { advanceToNextTimerAndFlush, trigger } from '../../../testing/element-fixture';
import { domRender, ssrRenderToDom } from '../../../testing/rendering.unit-util';
Expand All @@ -8,13 +8,12 @@ import type { Signal as SignalType } from '../../state/signal';
import { untrack } from '../../use/use-core';
import { useSignal } from '../../use/use-signal';
import { useStore } from '../../use/use-store.public';
import { useTask$ } from '../../use/use-task-dollar';

const debug = true; //true;
Error.stackTraceLimit = 100;

describe.each([
{ render: ssrRenderToDom }, //
// { render: ssrRenderToDom }, //
{ render: domRender }, //
])('$render.name: useStore', ({ render }) => {
it('should render value', async () => {
Expand Down Expand Up @@ -184,16 +183,16 @@ describe.each([
</>
);
});
it.only('should allow signal to deliver value or JSX', async () => {
it('should allow signal to deliver value or JSX', async () => {
const log: string[] = [];
const Counter = component$(() => {
const count = useStore<any>({ value: 'initial' });
log.push('Counter: ' + untrack(() => count.value));
const count = useStore<any>({ jsx: 'initial' });
log.push('Counter: ' + untrack(() => count.jsx));
return (
<button
onClick$={() => (count.value = typeof count.value == 'string' ? <b>JSX</b> : 'text')}
onClick$={() => (count.jsx = typeof count.jsx == 'string' ? <b>JSX</b> : 'text')}
>
-{count.value}-
-{count.jsx}-
</button>
);
});
Expand Down

0 comments on commit 4f6045f

Please sign in to comment.