Skip to content

Commit

Permalink
feat: useTransient (#73)
Browse files Browse the repository at this point in the history
  • Loading branch information
mhevery authored Nov 5, 2021
1 parent f21a368 commit 488004b
Show file tree
Hide file tree
Showing 9 changed files with 80 additions and 24 deletions.
3 changes: 3 additions & 0 deletions src/core/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,9 @@ export function useEvent<EVENT extends {}>(expectEventType?: QEvent | string): E
// @public (undocumented)
export function useHostElement(): Element;

// @public (undocumented)
export function useTransient<OBJ, ARGS extends any[], RET>(obj: OBJ, factory: (this: OBJ, ...args: ARGS) => RET, ...args: ARGS): RET;

// @public (undocumented)
export function useURL(): URL;

Expand Down
4 changes: 2 additions & 2 deletions src/core/component/q-component-ctx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ComponentRenderQueue, visitJsxNode } from '../render/q-render';
import { AttributeMarker } from '../util/markers';
import { flattenPromiseTree } from '../util/promises';
import { QrlStyles, styleContent, styleHost } from './qrl-styles';
import { _qObject } from '../object/q-object';
import { _stateQObject } from '../object/q-object';
import { qProps } from '../props/q-props.public';

// TODO(misko): Can we get rid of this whole file, and instead teach qProps to know how to render
Expand Down Expand Up @@ -41,7 +41,7 @@ export class QComponentCtx {
if (hook) {
const values: OnHookReturn[] = await hook('qMount');
values.forEach((v) => {
props['state:' + v.state] = _qObject(v.value, v.state);
props['state:' + v.state] = _stateQObject(v.value, v.state);
});
}
} catch (e) {
Expand Down
20 changes: 15 additions & 5 deletions src/core/component/qrl-hook.public.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,16 @@ export function qHook<COMP extends QComponent, ARGS extends {} | unknown = unkno
/**
* @public
*/
export function qHook<COMP extends QComponent, ARGS extends {} | undefined = any, RET = unknown>(
hook: (props: PropsOf<COMP>, state: StateOf<COMP>, args: ARGS) => ValueOrPromise<RET>
): QHook<PropsOf<COMP>, StateOf<COMP>, ARGS, RET> {
export function qHook(hook: any, symbol?: string): any {
if (typeof hook === 'string') return hook;
if (typeof symbol === 'string') {
const match = String(hook).match(EXTRACT_IMPORT_PATH);
if (match && match[2]) {
return (match[2] + '#' + symbol) as any;
} else {
throw new Error('dynamic import not found: ' + String(hook));
}
}
const qrlFn = async (element: HTMLElement, event: Event, url: URL) => {
const isQwikInternalHook = typeof event == 'string';
// isQwikInternalHook && console.log('HOOK', event, element, url);
Expand All @@ -50,9 +57,9 @@ export function qHook<COMP extends QComponent, ARGS extends {} | undefined = any
);
};
if (qTest) {
return toDevModeQRL(qrlFn, new Error()) as any;
return toDevModeQRL(qrlFn, new Error());
}
return qrlFn as any;
return qrlFn;
}

/**
Expand All @@ -67,3 +74,6 @@ export interface QHook<
__brand__: 'QHook';
with(args: ARGS): QHook<PROPS, STATE, ARGS, RET>;
}

// https://regexr.com/68v72
const EXTRACT_IMPORT_PATH = /import\(\s*(['"])([^\1]+)\1\s*\)/;
1 change: 1 addition & 0 deletions src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export { QwikDOMAttributes, QwikJSX } from './render/jsx/types/jsx-qwik';
export type { QwikIntrinsicElements } from './render/jsx/types/jsx-qwik-elements';
export { qRender } from './render/q-render.public';
export { useEvent, useHostElement, useURL } from './use/use-core.public';
export { useTransient } from './use/use-transient.public';
//////////////////////////////////////////////////////////////////////////////////////////
// Developer Low-Level API
//////////////////////////////////////////////////////////////////////////////////////////
Expand Down
39 changes: 34 additions & 5 deletions src/core/object/q-object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,27 @@ import { safeQSubscribe } from '../use/use-core.public';
import type { QObject as IQObject } from './q-object.public';
export const Q_OBJECT_PREFIX_SEP = ':';

export function _qObject<T>(obj: T, prefix?: string, isId: boolean = false): T {
export function _qObject<T>(obj: T): T {
assertEqual(unwrapProxy(obj), obj, 'Unexpected proxy at this location');
const id = isId
? (prefix as string)
: (prefix == null ? '' : prefix + Q_OBJECT_PREFIX_SEP) + generateId();
const proxy = readWriteProxy(obj as any as IQObject<T>, id);
const proxy = readWriteProxy(obj as any as IQObject<T>, generateId());
Object.assign(proxy, obj);
return proxy;
}

export function _stateQObject<T>(obj: T, prefix: string): T {
const id = getQObjectId(obj);
if (id) {
(obj as any)[QObjectIdSymbol] = prefix + Q_OBJECT_PREFIX_SEP + id;
return obj;
} else {
return readWriteProxy(obj as any as IQObject<T>, prefix + Q_OBJECT_PREFIX_SEP + generateId());
}
}

export function _restoreQObject<T>(obj: T, id: string): T {
return readWriteProxy(obj as any as IQObject<T>, id);
}

function QObject_notifyWrite(id: string, doc: Document | null) {
if (doc) {
doc.querySelectorAll(idToComponentSelector(id)).forEach(qNotifyRender);
Expand All @@ -35,6 +46,17 @@ export function getQObjectId(obj: any): string | null {
return (obj && typeof obj === 'object' && obj[QObjectIdSymbol]) || null;
}

export function getTransient<T>(obj: any, key: any): T | null {
assertDefined(getQObjectId(obj));
return obj[QOjectTransientsSymbol].get(key);
}

export function setTransient<T>(obj: any, key: any, value: T): T {
assertDefined(getQObjectId(obj));
obj[QOjectTransientsSymbol].set(key, value);
return value;
}

function idToComponentSelector(id: string): any {
id = id.replace(/([^\w\d])/g, (_, v) => '\\' + v);
return '[q\\:obj*=' + (isStateObj(id) ? '' : '\\!') + id + ']';
Expand All @@ -61,6 +83,7 @@ export function readWriteProxy<T extends object>(target: T, id: string): T {
}

const QOjectTargetSymbol = ':target:';
const QOjectTransientsSymbol = ':transients:';
const QObjectIdSymbol = ':id:';
const QObjectDocumentSymbol = ':doc:';

Expand Down Expand Up @@ -90,13 +113,17 @@ export function wrap<T>(value: T): T {
class ReadWriteProxyHandler<T extends object> implements ProxyHandler<T> {
private id: string;
private doc: Document | null = null;
private transients: WeakMap<any, any> | null = null;
constructor(id: string) {
this.id = id;
}

get(target: T, prop: string): any {
if (prop === QOjectTargetSymbol) return target;
if (prop === QObjectIdSymbol) return this.id;
if (prop === QOjectTransientsSymbol) {
return this.transients || (this.transients = new WeakMap());
}
const value = (target as any)[prop];
QObject_notifyRead(target);
return wrap(value);
Expand All @@ -105,6 +132,8 @@ class ReadWriteProxyHandler<T extends object> implements ProxyHandler<T> {
set(target: T, prop: string, newValue: any): boolean {
if (prop === QObjectDocumentSymbol) {
this.doc = newValue;
} else if (prop == QObjectIdSymbol) {
this.id = newValue;
} else {
const unwrappedNewValue = unwrapProxy(newValue);
const oldValue = (target as any)[prop];
Expand Down
4 changes: 2 additions & 2 deletions src/core/object/q-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { assertDefined } from '../assert/assert';
import { JSON_OBJ_PREFIX } from '../json/q-json';
import { qDev } from '../util/qdev';
import { clearQProps, clearQPropsMap, QPropsContext } from '../props/q-props';
import { getQObjectId, _qObject } from './q-object';
import { getQObjectId, _restoreQObject } from './q-object';
import { qProps } from '../props/q-props.public';

export interface Store {
Expand Down Expand Up @@ -55,7 +55,7 @@ function reviveQObjects(map: Record<string, object> | null) {
for (const key in map) {
if (Object.prototype.hasOwnProperty.call(map, key)) {
const value = map[key];
map[key] = _qObject(value, key, true);
map[key] = _restoreQObject(value, key);
}
}
}
Expand Down
10 changes: 5 additions & 5 deletions src/core/object/q-store.unit.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createDocument } from '../../testing/document';
import { qProps, QProps } from '../props/q-props.public';
import { qObject, qDehydrate } from '@builder.io/qwik';
import { _qObject } from './q-object';
import { _stateQObject } from './q-object';

describe('q-element', () => {
let document: Document;
Expand All @@ -16,7 +16,7 @@ describe('q-element', () => {

it('should serialize content', () => {
const shared = qObject({ mark: 'CHILD' });
qDiv['state:'] = _qObject({ mark: 'WORKS', child: shared, child2: shared }, '');
qDiv['state:'] = _stateQObject({ mark: 'WORKS', child: shared, child2: shared }, '');

qDehydrate(document);

Expand All @@ -25,7 +25,7 @@ describe('q-element', () => {
});

it('should serialize same objects multiple times', () => {
const foo = _qObject({ mark: 'CHILD' }, 'foo');
const foo = _stateQObject({ mark: 'CHILD' }, 'foo');
qDiv['state:foo'] = foo;
qDiv.foo = foo;

Expand All @@ -36,8 +36,8 @@ describe('q-element', () => {
expect(qDiv.foo).toEqual(foo);
});
it('should serialize cyclic graphs', () => {
const foo = _qObject({ mark: 'foo', bar: {} }, 'foo');
const bar = _qObject({ mark: 'bar', foo: foo }, 'bar');
const foo = _stateQObject({ mark: 'foo', bar: {} }, 'foo');
const bar = _stateQObject({ mark: 'bar', foo: foo }, 'bar');
foo.bar = bar;
qDiv.foo = foo;

Expand Down
10 changes: 5 additions & 5 deletions src/core/props/q-props.unit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ParsedQRL } from '../import/qrl';
import { diff, test_clearqPropsCache as test_clearQPropsCache } from './q-props';
import type { QComponent } from '../component/q-component.public';
import { qObject } from '../object/q-object.public';
import { getQObjectId, _qObject } from '../object/q-object';
import { getQObjectId, _stateQObject } from '../object/q-object';
import { qDehydrate } from '../object/q-store.public';
import { qProps, QProps } from './q-props.public';

Expand Down Expand Up @@ -120,8 +120,8 @@ describe('q-element', () => {

describe('state', () => {
it('should retrieve state by name', () => {
const state1 = _qObject({ mark: 1 }, '');
const state2 = _qObject({ mark: 2 }, 'foo');
const state1 = _stateQObject({ mark: 1 }, '');
const state2 = _stateQObject({ mark: 2 }, 'foo');
qDiv['state:'] = state1;
qDiv['state:foo'] = state2;

Expand Down Expand Up @@ -167,8 +167,8 @@ describe('q-element', () => {

it('should read qrl as single function', async () => {
qDiv['on:qRender'] = 'markAsHost';
qDiv['state:'] = _qObject({ mark: 'implicit' }, '');
qDiv['state:explicit'] = _qObject({ mark: 'explicit' }, 'explicit');
qDiv['state:'] = _stateQObject({ mark: 'implicit' }, '');
qDiv['state:explicit'] = _stateQObject({ mark: 'explicit' }, 'explicit');
qDiv.isHost = 'YES';

const child = document.createElement('child');
Expand Down
13 changes: 13 additions & 0 deletions src/core/use/use-transient.public.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { getTransient, setTransient } from '../object/q-object';

/**
* @public
*/
export function useTransient<OBJ, ARGS extends any[], RET>(
obj: OBJ,
factory: (this: OBJ, ...args: ARGS) => RET,
...args: ARGS
): RET {
const existing = getTransient<RET>(obj, factory);
return existing || setTransient(obj, factory, factory.apply(obj, args));
}

0 comments on commit 488004b

Please sign in to comment.